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

@ -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.

View file

@ -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 "<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
@ -79,26 +82,28 @@ def cmdparser(raw_string):
longer written name means being more specific, a longer command
name takes precedence over a short one.
There is one optional form:
<objname>'s [<char>]cmdname[ cmdname2 cmdname3 ...][<char>] [the rest]
There are two optional forms:
<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):
"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 "<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).
if end_char == "-" and len(raw_string) > end_index+1:
# the command is on the forms "<num>-command"
# or "<word>-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:

View file

@ -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):