Fixed several special cases of handling multiple same-named commands gracefully. Should resolve issue94.

This commit is contained in:
Griatch 2010-09-01 21:59:13 +00:00
parent 03cc4970d0
commit 900f6da80f
9 changed files with 204 additions and 79 deletions

View file

@ -711,11 +711,24 @@ class CmdOpen(ObjManipCommand):
""" """
caller = self.caller caller = self.caller
string = "" string = ""
# check if this exit object already exists here # check if this exit object already exists. We need to
exit_obj = [obj for obj in location.contents # know what the result is before we can decide what to do;
if (obj.key.lower() == exit_name.lower() and obj.db._destination)] # so we deactivate the automatic error handling. This
if exit_obj: # 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] 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 old_destination = exit_obj.db._destination
if old_destination: if old_destination:
string = "Exit %s already exists." % exit_name string = "Exit %s already exists." % exit_name

View file

@ -67,16 +67,62 @@ class SystemNoMatch(MuxCommand):
# #
class SystemMultimatch(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 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): def func(self):
""" """
argument to cmd is a comma-separated string of argument to cmd is a comma-separated string of
all the clashing matches. 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): class SystemNoPerm(MuxCommand):
""" """

View file

@ -101,7 +101,6 @@ def get_and_merge_cmdsets(caller):
exit_cmdset = None exit_cmdset = None
local_objects_cmdsets = [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: if not caller_cmdset.no_channels:
# Make cmdsets out of all valid channels # Make cmdsets out of all valid channels
channel_cmdset = CHANNELHANDLER.get_cmdset(caller) channel_cmdset = CHANNELHANDLER.get_cmdset(caller)
@ -110,20 +109,25 @@ def get_and_merge_cmdsets(caller):
exit_cmdset = EXITHANDLER.get_cmdset(caller) exit_cmdset = EXITHANDLER.get_cmdset(caller)
location = caller.location location = caller.location
if location and not caller_cmdset.no_objs: 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_objlist = location.contents
local_objects_cmdsets = [obj.cmdset.current local_objects_cmdsets = [obj.cmdset.current
for obj in local_objlist for obj in local_objlist
if obj.cmdset.outside_access if obj.cmdset.allow_outside_access(caller)]
and obj.cmdset.allow_outside_access(caller)]
# Merge all command sets into one # Merge all command sets into one
# (the order matters, the higher-prio cmdsets are merged last) # (the order matters, the higher-prio cmdsets are merged last)
cmdset = caller_cmdset cmdset = caller_cmdset
for obj_cmdset in local_objects_cmdsets: 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: try:
old_duplicate_flag = obj_cmdset.duplicates
obj_cmdset.duplicates = True
cmdset = obj_cmdset + cmdset cmdset = obj_cmdset + cmdset
obj_cmdset.duplicates = old_duplicate_flag
except TypeError: except TypeError:
pass pass
# Exits and channels automatically has duplicates=True.
try: try:
cmdset = exit_cmdset + cmdset cmdset = exit_cmdset + cmdset
except TypeError: except TypeError:
@ -156,44 +160,103 @@ def match_command(cmd_candidates, cmdset, logged_caller=None):
if not matches or len(matches) == 1: if not matches or len(matches) == 1:
return matches return matches
# Do our damndest to resolve multiple matches # Do our damndest to resolve multiple matches ...
# 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).
# First try candidate priority to separate them
top_ranked = [] top_ranked = []
top_priority = None top_priority = None
for match in matches: for match in matches:
if top_priority == None \ prio = match[0].priority
or match[0].priority >= top_priority: if top_priority == None or prio > top_priority:
top_priority = match[0].priority top_ranked = [match]
top_priority = prio
elif top_priority == prio:
top_ranked.append(match) top_ranked.append(match)
matches = top_ranked matches = top_ranked
if not matches or len(matches) == 1: if not matches or len(matches) == 1:
return matches return matches
# still multiplies. Check if player supplied # Still multiplies. At this point we should have sorted out
# an obj name on the command line. We know they # all candidate multiples; the multiple comes from one candidate
# all have at least the same cmdname and obj_key # matching more than one command.
# at this point.
# 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: if logged_caller:
try: try:
local_objlist = logged_caller.location.contents local_objlist = logged_caller.location.contents
match = matches[0] top_ranked = []
top_ranked = [obj for obj in local_objlist candidate = matches[0][0] # all candidates should be the same
if match[0].obj_key == obj.name top_ranked.extend([(candidate, obj.cmdset.current.get(candidate.cmdname))
and any(cmd == match[0].cmdname for obj in local_objlist
for cmd in obj.cmdset.current)] if candidate.obj_key == obj.name
and any(cmd == candidate.cmdname
for cmd in obj.cmdset.current)])
if top_ranked: if top_ranked:
matches = \ matches = top_ranked
[(match[0],
obj.cmdset.current.get(match[0].cmdname))
for obj in top_ranked]
except Exception: except Exception:
logger.log_trace() logger.log_trace()
if not matches or len(matches) == 1:
return matches
# 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 # regardless what we have at this point, we have to be content
return matches 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 # Main command-handler function
@ -253,13 +316,11 @@ def cmdhandler(caller, raw_string, unloggedin=False):
if len(matches) > 1: if len(matches) > 1:
# We have a multiple-match # We have a multiple-match
syscmd = cmdset.get(CMD_MULTIMATCH) syscmd = cmdset.get(CMD_MULTIMATCH)
matchstring = ", ".join([match[0].cmdname sysarg = "There where multiple matches."
for match in matches])
if syscmd: if syscmd:
sysarg = matchstring syscmd.matches = matches
else: else:
sysarg = "There were multiple matches:\n %s" sysarg = format_multimatches(caller, matches)
sysarg = sysarg % matchstring
raise ExecSystemCommand(syscmd, sysarg) raise ExecSystemCommand(syscmd, sysarg)
# At this point, we have a unique command match. # At this point, we have a unique command match.

View file

@ -19,6 +19,7 @@ SPECIAL_CHARS = ["/", "\\", "'", '"', ":", ";", "\-", '#', '=', '!']
# Pre-compiling the regular expression is more effective # Pre-compiling the regular expression is more effective
REGEX = re.compile(r"""["%s"]""" % ("".join(SPECIAL_CHARS))) REGEX = re.compile(r"""["%s"]""" % ("".join(SPECIAL_CHARS)))
class CommandCandidate(object): class CommandCandidate(object):
""" """
This is a convenient container for one possible This is a convenient container for one possible
@ -32,7 +33,9 @@ class CommandCandidate(object):
self.priority = priority self.priority = priority
self.obj_key = obj_key self.obj_key = obj_key
def __str__(self): def __str__(self):
return "<cmdname:'%s',args:'%s'>" % (self.cmdname, self.args) string = "cmdcandidate <name:'%s',args:'%s', "
string += "prio:%s, obj_key:'%s'>"
return string % (self.cmdname, self.args, self.priority, self.obj_key)
# #
# The command parser # The command parser
@ -79,26 +82,28 @@ def cmdparser(raw_string):
longer written name means being more specific, a longer command longer written name means being more specific, a longer command
name takes precedence over a short one. name takes precedence over a short one.
There is one optional form: There are two optional forms:
<objname>'s [<char>]cmdname[ cmdname2 cmdname3 ...][<char>] [the rest] <objname>-[<char>]cmdname[ cmdname2 cmdname3 ...][<char>] [the rest]
<num>-[<char>]cmdname[ cmdname2 cmdname3 ...][<char>] [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 <objname>- 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 <num>- 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 <objname> 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): def produce_candidates(nr_candidates, wordlist):
"Helper function" "Helper function"
candidates = [] candidates = []
cmdwords_list = [] cmdwords_list = []
#print "wordlist:",wordlist
for n_words in range(nr_candidates): for n_words in range(nr_candidates):
cmdwords_list.append(wordlist.pop(0)) cmdwords_list.append(wordlist.pop(0))
cmdwords = " ".join([word.strip().lower() cmdwords = " ".join([word.strip().lower()
@ -116,20 +121,21 @@ def cmdparser(raw_string):
return candidates return candidates
raw_string = raw_string.strip() raw_string = raw_string.strip()
#TODO: check for non-standard characters.
candidates = [] candidates = []
regex_result = REGEX.search(raw_string) regex_result = REGEX.search(raw_string)
if not regex_result == None: if not regex_result == None:
# there are characters from SPECIAL_CHARS in the string. # there are characters from SPECIAL_CHARS in the string.
# since they cannot be part of a longer command, these # since they cannot be part of a longer command, these
# will cut short the command, no matter how long we # will cut short the command, no matter how long we
# allow commands to be. # allow commands to be.
end_index = regex_result.start() end_index = regex_result.start()
end_char = raw_string[end_index] end_char = raw_string[end_index]
if end_index == 0: 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. # a special char, we let that be the command name.
cmdwords = end_char cmdwords = end_char
if len(raw_string) > 1: if len(raw_string) > 1:
@ -140,22 +146,18 @@ def cmdparser(raw_string):
return candidates return candidates
else: else:
# the special char occurred somewhere inside the string # the special char occurred somewhere inside the string
if end_char == "'" and \ if end_char == "-" and len(raw_string) > end_index+1:
len(raw_string) > end_index+1 and \ # the command is on the forms "<num>-command"
raw_string[end_index+1:end_index+3] == "s ": # or "<word>-command"
# The command is of the form "<word>'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).
obj_key = raw_string[:end_index] obj_key = raw_string[:end_index]
alt_string = raw_string[end_index+2:] alt_string = raw_string[end_index+1:]
alt_candidates = cmdparser(alt_string) for candidate in cmdparser(alt_string):
for candidate in alt_candidates:
candidate.obj_key = obj_key candidate.obj_key = obj_key
candidates.extend(alt_candidates) candidate.priority =- 1
# now we let the parser continue as normal, in case candidates.append(candidate)
# the 's -business was not meant to be an obj ref at all.
# 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 # We only run the command finder up until the end char
nr_candidates = len(raw_string[:end_index].split(None)) nr_candidates = len(raw_string[:end_index].split(None))
if nr_candidates <= COMMAND_MAXLEN: if nr_candidates <= COMMAND_MAXLEN:

View file

@ -130,6 +130,7 @@ class ChannelHandler(object):
chan_cmdset = cmdset.CmdSet() chan_cmdset = cmdset.CmdSet()
chan_cmdset.key = '_channelset' chan_cmdset.key = '_channelset'
chan_cmdset.priority = 10 chan_cmdset.priority = 10
chan_cmdset.duplicates = True
for cmd in [cmd for cmd in self.cached_channel_cmds for cmd in [cmd for cmd in self.cached_channel_cmds
if has_perm(source_object, cmd, 'chan_send')]: if has_perm(source_object, cmd, 'chan_send')]:
chan_cmdset.add(cmd) chan_cmdset.add(cmd)

View file

@ -58,6 +58,7 @@ class ExitHandler(object):
exit_cmdset = cmdset.CmdSet(None) exit_cmdset = cmdset.CmdSet(None)
exit_cmdset.key = '_exitset' exit_cmdset.key = '_exitset'
exit_cmdset.priority = 9 exit_cmdset.priority = 9
exit_cmdset.duplicates = True
try: try:
location = srcobj.location location = srcobj.location
except Exception: except Exception:

View file

@ -52,6 +52,8 @@ class Object(TypeClass):
create_scripts = True create_scripts = True
if create_cmdset: if create_cmdset:
dbobj.cmdset = CmdSetHandler(dbobj) dbobj.cmdset = CmdSetHandler(dbobj)
if dbobj.player:
dbobj.cmdset.outside_access = False
if create_scripts: if create_scripts:
dbobj.scripts = ScriptHandler(dbobj) dbobj.scripts = ScriptHandler(dbobj)
@ -321,14 +323,13 @@ class Character(Object):
""" """
from settings import CMDSET_DEFAULT from settings import CMDSET_DEFAULT
self.cmdset.add_default(CMDSET_DEFAULT, permanent=True) 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): def at_after_move(self, source_location):
"Default is to look around after a move." "Default is to look around after a move."
self.execute_cmd('look') self.execute_cmd('look')
# #
# Base Room object # Base Room object
# #

View file

@ -81,7 +81,7 @@ def create_objects():
typeclass=character_typeclass, typeclass=character_typeclass,
user=god_user) user=god_user)
god_character.id = 1 god_character.id = 1
god_character.attr('desc', 'You are Player #1.') god_character.db.desc = 'This is User #1.'
god_character.save() god_character.save()
# Limbo is the initial starting room. # Limbo is the initial starting room.
@ -93,7 +93,7 @@ def create_objects():
string += " From here you are ready to begin development." string += " From here you are ready to begin development."
string += " If you should need help or would like to participate" string += " If you should need help or would like to participate"
string += " in community discussions, visit http://evennia.com." string += " in community discussions, visit http://evennia.com."
limbo_obj.attr('desc', string) limbo_obj.db.desc = string
limbo_obj.save() limbo_obj.save()
# Now that Limbo exists, set the user up in Limbo. # Now that Limbo exists, set the user up in Limbo.