mirror of
https://github.com/evennia/evennia.git
synced 2026-03-20 23:06:31 +01:00
Added a new 'contrib' folder for optional code snippets not suitable for the server core. Added contrib/menusystem for implementing a multi-choice menu system. Added contrib/lineeditor - a powerful line editor with commands mimicking VI. Also added an example NPC class using the menu system to allow for a conversation. As part of creating these contributions, lots of bugs were found and fixed. A new and more powerful cmdparser was intruduced as a result - this one is much easier to understand than the old one, while being more efficient and versatile. All testsuites were updated. Also: Resolves issue 165.
This commit is contained in:
parent
2c47d6a66b
commit
b9c1921a0b
16 changed files with 1426 additions and 354 deletions
|
|
@ -54,6 +54,7 @@ from django.conf import settings
|
|||
from src.comms.channelhandler import CHANNELHANDLER
|
||||
from src.commands.cmdsethandler import import_cmdset
|
||||
from src.utils import logger, utils
|
||||
from src.commands.cmdparser import at_multimatch_cmd
|
||||
|
||||
#This switches the command parser to a user-defined one.
|
||||
# You have to restart the server for this to take effect.
|
||||
|
|
@ -106,7 +107,12 @@ def get_and_merge_cmdsets(caller):
|
|||
# also in the caller's inventory and the location itself
|
||||
local_objlist = location.contents_get(exclude=caller.dbobj) + caller.contents + [location]
|
||||
local_objects_cmdsets = [obj.cmdset.current for obj in local_objlist
|
||||
if obj.locks.check(caller, 'call', no_superuser_bypass=True)]
|
||||
if (obj.cmdset.current and obj.locks.check(caller, 'call', no_superuser_bypass=True))]
|
||||
for cset in local_objects_cmdsets:
|
||||
#This is necessary for object sets, or we won't be able to separate
|
||||
#the command sets from each other in a busy room.
|
||||
cset.old_duplicates = cset.duplicates
|
||||
cset.duplicates = True
|
||||
|
||||
# Player object's commandsets
|
||||
try:
|
||||
|
|
@ -128,127 +134,12 @@ def get_and_merge_cmdsets(caller):
|
|||
cmdset = merging_cmdset + cmdset
|
||||
else:
|
||||
cmdset = None
|
||||
|
||||
for cset in (cset for cset in local_objects_cmdsets if cset):
|
||||
cset.duplicates = cset.old_duplicates
|
||||
|
||||
return cmdset
|
||||
|
||||
def match_command(cmd_candidates, cmdset, logged_caller=None):
|
||||
"""
|
||||
Try to match the command against one of the
|
||||
cmd_candidates.
|
||||
|
||||
logged_caller - a logged-in object, if any.
|
||||
|
||||
"""
|
||||
|
||||
# Searching possible command matches in the given cmdset
|
||||
matches = []
|
||||
prev_found_cmds = [] # to avoid aliases clashing with themselves
|
||||
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:
|
||||
return 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).
|
||||
|
||||
top_ranked = []
|
||||
top_priority = None
|
||||
for match in matches:
|
||||
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:
|
||||
return matches
|
||||
|
||||
# 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
|
||||
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 = top_ranked
|
||||
except Exception:
|
||||
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
|
||||
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
|
||||
|
||||
|
|
@ -284,9 +175,9 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
|
|||
sysarg = ""
|
||||
raise ExecSystemCommand(syscmd, sysarg)
|
||||
|
||||
# Parse the input string into command candidates
|
||||
cmd_candidates = COMMAND_PARSER(raw_string)
|
||||
|
||||
# Parse the input string and match to available cmdset.
|
||||
matches = COMMAND_PARSER(raw_string, cmdset)
|
||||
|
||||
#string ="Command candidates"
|
||||
#for cand in cmd_candidates:
|
||||
# string += "\n %s || %s" % (cand.cmdname, cand.args)
|
||||
|
|
@ -294,10 +185,10 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
|
|||
|
||||
# Try to produce a unique match between the merged
|
||||
# cmdset and the candidates.
|
||||
if unloggedin:
|
||||
matches = match_command(cmd_candidates, cmdset)
|
||||
else:
|
||||
matches = match_command(cmd_candidates, cmdset, caller)
|
||||
# if unloggedin:
|
||||
# matches = match_command(cmd_candidates, cmdset)
|
||||
# else:
|
||||
# matches = match_command(cmd_candidates, cmdset, caller)
|
||||
|
||||
#print "matches: ", matches
|
||||
|
||||
|
|
@ -318,11 +209,12 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
|
|||
if syscmd:
|
||||
syscmd.matches = matches
|
||||
else:
|
||||
sysarg = format_multimatches(caller, matches)
|
||||
sysarg = at_multimatch_cmd(caller, matches)
|
||||
raise ExecSystemCommand(syscmd, sysarg)
|
||||
|
||||
# At this point, we have a unique command match.
|
||||
cmd_candidate, cmd = matches[0]
|
||||
match = matches[0]
|
||||
cmdname, args, cmd = match[0], match[1], match[2]
|
||||
|
||||
# Check so we have permission to use this command.
|
||||
if not cmd.access(caller):
|
||||
|
|
@ -341,16 +233,15 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
|
|||
if syscmd:
|
||||
# replace system command with custom version
|
||||
cmd = syscmd
|
||||
sysarg = "%s:%s" % (cmd_candidate.cmdname,
|
||||
cmd_candidate.args)
|
||||
sysarg = "%s:%s" % (cmdname, args)
|
||||
raise ExecSystemCommand(cmd, sysarg)
|
||||
|
||||
# A normal command.
|
||||
|
||||
# Assign useful variables to the instance
|
||||
cmd.caller = caller
|
||||
cmd.cmdstring = cmd_candidate.cmdname
|
||||
cmd.args = cmd_candidate.args
|
||||
cmd.cmdstring = cmdname
|
||||
cmd.args = args
|
||||
cmd.cmdset = cmdset
|
||||
|
||||
if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'):
|
||||
|
|
@ -384,10 +275,10 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
|
|||
syscmd.args = sysarg
|
||||
syscmd.cmdset = cmdset
|
||||
|
||||
if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'):
|
||||
if hasattr(syscmd, 'obj') and hasattr(syscmd.obj, 'scripts'):
|
||||
# cmd.obj is automatically made available.
|
||||
# we make sure to validate its scripts.
|
||||
cmd.obj.scripts.validate()
|
||||
syscmd.obj.scripts.validate()
|
||||
|
||||
if testing:
|
||||
# only return the command instance
|
||||
|
|
|
|||
|
|
@ -4,177 +4,90 @@ settings.ALTERNATE_PARSER to a Python path to a module containing the
|
|||
replacing cmdparser function. The replacement parser must
|
||||
return a CommandCandidates object.
|
||||
"""
|
||||
import re
|
||||
from django.conf import settings
|
||||
|
||||
# This defines how many space-separated words may at most be in a command.
|
||||
COMMAND_MAXLEN = settings.COMMAND_MAXLEN
|
||||
|
||||
# These chars (and space) end a command name and may
|
||||
# thus never be part of a command name. Exception is
|
||||
# if the char is the very first character - the char
|
||||
# is then treated as the name of the command.
|
||||
SPECIAL_CHARS = ["/", "\\", "'", '"', ":", ";", "\-", '#', '=', '!']
|
||||
|
||||
# Pre-compiling the regular expression is more effective
|
||||
REGEX = re.compile(r"""["%s"]""" % ("".join(SPECIAL_CHARS)))
|
||||
|
||||
|
||||
class CommandCandidate(object):
|
||||
def cmdparser(raw_string, cmdset, match_index=None):
|
||||
"""
|
||||
This is a convenient container for one possible
|
||||
combination of command names that may appear if we allow
|
||||
many-word commands.
|
||||
"""
|
||||
def __init__(self, cmdname, args=0, priority=0, obj_key=None):
|
||||
"initiate"
|
||||
self.cmdname = cmdname
|
||||
self.args = args
|
||||
self.priority = priority
|
||||
self.obj_key = obj_key
|
||||
def __str__(self):
|
||||
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
|
||||
#
|
||||
def cmdparser(raw_string):
|
||||
"""
|
||||
This function parses the raw string into three parts: command
|
||||
name(s), keywords(if any) and arguments(if any). It returns a
|
||||
CommandCandidates object. It should be general enough for most
|
||||
game implementations, but you can also overwrite it should you
|
||||
wish to implement some completely different way of handling and
|
||||
ranking commands. Arguments and keywords are parsed/dealt with by
|
||||
each individual command's parse() command.
|
||||
This function is called by the cmdhandler once it has
|
||||
gathered all valid cmdsets for the calling player. raw_string
|
||||
is the unparsed text entered by the caller.
|
||||
|
||||
The cmdparser understand the following command combinations (where
|
||||
[] marks optional parts and <char> is one of the SPECIAL_CHARs
|
||||
defined globally.):
|
||||
[] marks optional parts.
|
||||
|
||||
[<char>]cmdname[ cmdname2 cmdname3 ...][<char>] [the rest]
|
||||
[cmdname[ cmdname2 cmdname3 ...] [the rest]
|
||||
|
||||
A command may contain spaces, but never any of of the <char>s. A
|
||||
command can maximum have CMD_MAXLEN words, or the number of words
|
||||
up to the first <char>, whichever is smallest. An exception is if
|
||||
<char> is the very first character in the string - the <char> is
|
||||
then assumed to be the actual command name (a common use for this
|
||||
is for e.g ':' to be a shortcut for 'emote').
|
||||
All words not part of the command name is considered a part of the
|
||||
command's argument. Note that <char>s ending a command are never
|
||||
removed but are included as the first character in the
|
||||
argument. This makes it easier for individual commands to identify
|
||||
things like switches. Example: '@create/drop ball' finds the
|
||||
command name to trivially be '@create' since '/' ends it. As the
|
||||
command's arguments are sent '/drop ball'. In this MUX-inspired
|
||||
example, '/' denotes a keyword (or switch) and it is now easy for
|
||||
the receiving command to parse /drop as a keyword just by looking
|
||||
at the first character.
|
||||
A command may consist of any number of space-separated words of any
|
||||
length, and contain any character.
|
||||
|
||||
The parser makes use of the cmdset to find command candidates. The
|
||||
parser return a list of matches. Each match is a tuple with its
|
||||
first three elements being the parsed cmdname (lower case),
|
||||
the remaining arguments, and the matched cmdobject from the cmdset.
|
||||
"""
|
||||
|
||||
Allowing multiple command names means we have to take care of all
|
||||
possible meanings and the result will be a CommandCandidates
|
||||
object with up to COMMAND_MAXLEN names stored in it. So if
|
||||
COMMAND_MAXLEN was, say, 4, we would have to search all commands
|
||||
matching one of 'hit', 'hit orc', 'hit orc with' and 'hit orc with
|
||||
sword' - each which are potentially valid commands. Assuming a
|
||||
longer written name means being more specific, a longer command
|
||||
name takes precedence over a short one.
|
||||
def create_match(cmdname, string, cmdobj):
|
||||
"""
|
||||
Evaluates the quality of a match by counting how many chars of cmdname
|
||||
matches string (counting from beginning of string). We also calculate
|
||||
a ratio from 0-1 describing how much cmdname matches string.
|
||||
We return a tuple (cmdname, count, ratio, args, cmdobj).
|
||||
|
||||
There are two optional forms:
|
||||
<objname>-[<char>]cmdname[ cmdname2 cmdname3 ...][<char>] [the rest]
|
||||
<num>-[<char>]cmdname[ cmdname2 cmdname3 ...][<char>] [the rest]
|
||||
"""
|
||||
cmdlen, strlen = len(cmdname), len(string)
|
||||
mratio = 1 - (strlen - cmdlen) / (1.0 * strlen)
|
||||
args = string[cmdlen:]
|
||||
return (cmdname, args, cmdobj, cmdlen, mratio)
|
||||
|
||||
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
|
||||
if not raw_string:
|
||||
return None
|
||||
|
||||
"""
|
||||
matches = []
|
||||
|
||||
def produce_candidates(nr_candidates, wordlist):
|
||||
"Helper function"
|
||||
candidates = []
|
||||
cmdwords_list = []
|
||||
for n_words in range(nr_candidates):
|
||||
cmdwords_list.append(wordlist.pop(0))
|
||||
cmdwords = " ".join([word.strip().lower()
|
||||
for word in cmdwords_list])
|
||||
args = ""
|
||||
for word in wordlist:
|
||||
if not args or (word and (REGEX.search(word[0]))):
|
||||
#print "nospace: %s '%s'" % (args, word)
|
||||
args += word
|
||||
else:
|
||||
#print "space: %s '%s'" % (args, word)
|
||||
args += " %s" % word
|
||||
#print "'%s' | '%s'" % (cmdwords, args)
|
||||
candidates.append(CommandCandidate(cmdwords, args, priority=n_words))
|
||||
return candidates
|
||||
# match everything that begins with a matching cmdname.
|
||||
l_raw_string = raw_string.lower()
|
||||
for cmd in cmdset:
|
||||
matches.extend([create_match(cmdname, raw_string, cmd)
|
||||
for cmdname in [cmd.key] + cmd.aliases
|
||||
if cmdname and l_raw_string.startswith(cmdname.lower())])
|
||||
|
||||
if not matches:
|
||||
# no matches found.
|
||||
if '-' in raw_string:
|
||||
# This could be due to the user trying to identify the
|
||||
# command with a #num-<command> style syntax.
|
||||
mindex, new_raw_string = raw_string.split("-", 1)
|
||||
if mindex.isdigit():
|
||||
mindex = int(mindex) - 1
|
||||
# feed result back to parser iteratively
|
||||
return cmdparser(new_raw_string, cmdset, match_index=mindex)
|
||||
|
||||
raw_string = raw_string.strip()
|
||||
candidates = []
|
||||
|
||||
regex_result = REGEX.search(raw_string)
|
||||
if len(matches) > 1:
|
||||
# see if it helps to analyze the match with preserved case.
|
||||
matches = [match for match in matches if raw_string.startswith(match[0])]
|
||||
|
||||
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.
|
||||
if len(matches) > 1:
|
||||
# we still have multiple matches. Sort them by count quality.
|
||||
matches = sorted(matches, key=lambda m: m[3])
|
||||
# only pick the matches with highest count quality
|
||||
quality = [mat[3] for mat in matches]
|
||||
matches = matches[-quality.count(quality[-1]):]
|
||||
|
||||
end_index = regex_result.start()
|
||||
end_char = raw_string[end_index]
|
||||
if len(matches) > 1:
|
||||
# still multiple matches. Fall back to ratio-based quality.
|
||||
matches = sorted(matches, key=lambda m: m[4])
|
||||
# only pick the highest rated ratio match
|
||||
quality = [mat[4] for mat in matches]
|
||||
matches = matches[-quality.count(quality[-1]):]
|
||||
|
||||
if end_index == 0:
|
||||
# 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:
|
||||
args = raw_string[1:]
|
||||
else:
|
||||
args = ""
|
||||
candidates.append(CommandCandidate(cmdwords, args))
|
||||
return candidates
|
||||
else:
|
||||
# the special char occurred somewhere inside the string
|
||||
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+1:]
|
||||
for candidate in cmdparser(alt_string):
|
||||
candidate.obj_key = obj_key
|
||||
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:
|
||||
wordlist = raw_string[:end_index].split(" ")
|
||||
wordlist.extend(raw_string[end_index:].split(" "))
|
||||
#print "%i, wordlist: %s" % (nr_candidates, wordlist)
|
||||
candidates.extend(produce_candidates(nr_candidates, wordlist))
|
||||
return candidates
|
||||
if len(matches) > 1 and match_index != None and 0 <= match_index < len(matches):
|
||||
# We couldn't separate match by quality, but we have an index argument to
|
||||
# tell us which match to use.
|
||||
matches = [matches[match_index]]
|
||||
|
||||
# if there were no special characters, or that character
|
||||
# was not found within the allowed number of words, we run normally
|
||||
nr_candidates = min(COMMAND_MAXLEN,
|
||||
len(raw_string.split(None)))
|
||||
wordlist = raw_string.split(" ")
|
||||
candidates.extend(produce_candidates(nr_candidates, wordlist))
|
||||
return candidates
|
||||
# no matter what we have at this point, we have to return it.
|
||||
return matches
|
||||
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Search parsers and support methods
|
||||
|
|
@ -299,3 +212,36 @@ def at_multimatch_input(ostring):
|
|||
return (None, ostring)
|
||||
except IndexError:
|
||||
return (None, ostring)
|
||||
|
||||
|
||||
def at_multimatch_cmd(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)
|
||||
cmdname, arg, cmd, dum, dum = 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.key
|
||||
id2 = " (%s-%s)" % (num + 1, cmdname)
|
||||
else:
|
||||
id1 = "%s-" % (num + 1)
|
||||
id2 = ""
|
||||
string += "\n %s%s%s%s%s" % (id1, cmdname, id2, is_channel, is_exit)
|
||||
return string
|
||||
|
|
|
|||
|
|
@ -30,7 +30,8 @@ class CmdSetMeta(type):
|
|||
"""
|
||||
# by default we key the cmdset the same as the
|
||||
# name of its class.
|
||||
mcs.key = mcs.__name__
|
||||
if not hasattr(mcs, 'key') or not mcs.key:
|
||||
mcs.key = mcs.__name__
|
||||
mcs.path = "%s.%s" % (mcs.__module__, mcs.__name__)
|
||||
|
||||
if not type(mcs.key_mergetypes) == dict:
|
||||
|
|
@ -178,7 +179,7 @@ class CmdSet(object):
|
|||
key_mergetypes = {}
|
||||
no_exits = False
|
||||
no_objs = False
|
||||
no_channels = False
|
||||
no_channels = False
|
||||
|
||||
def __init__(self, cmdsetobj=None, key=None):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False):
|
|||
cmdsetclass = CACHED_CMDSETS.get(wanted_cache_key, None)
|
||||
errstring = ""
|
||||
if not cmdsetclass:
|
||||
#print "cmdset %s not in cache. Reloading." % wanted_cache_key
|
||||
#print "cmdset '%s' not in cache. Reloading %s on %s." % (wanted_cache_key, python_path, cmdsetobj)
|
||||
# Not in cache. Reload from disk.
|
||||
modulepath, classname = python_path.rsplit('.', 1)
|
||||
module = __import__(modulepath, fromlist=[True])
|
||||
|
|
@ -120,8 +120,8 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False):
|
|||
print errstring
|
||||
logger.log_trace()
|
||||
if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
|
||||
object.__getattribute__(emit_to_obj, "msg")(errstring)
|
||||
raise # have to raise, or we will not see any errors in some situations!
|
||||
object.__getattribute__(emit_to_obj, "msg")(errstring)
|
||||
#raise # have to raise, or we will not see any errors in some situations!
|
||||
|
||||
# classes
|
||||
|
||||
|
|
@ -201,17 +201,19 @@ class CmdSetHandler(object):
|
|||
"""
|
||||
if init_mode:
|
||||
# reimport all permanent cmdsets
|
||||
self.permanent_paths = self.obj.cmdset_storage
|
||||
if self.permanent_paths:
|
||||
storage = self.obj.cmdset_storage
|
||||
#print "cmdset_storage:", self.obj.cmdset_storage
|
||||
if storage:
|
||||
self.cmdset_stack = []
|
||||
for pos, path in enumerate(self.permanent_paths):
|
||||
for pos, path in enumerate(storage):
|
||||
if pos == 0 and not path:
|
||||
self.cmdset_stack = [CmdSet(cmdsetobj=self.obj, key="Empty")]
|
||||
else:
|
||||
cmdset = self.import_cmdset(path)
|
||||
elif path:
|
||||
cmdset = self.import_cmdset(path)
|
||||
if cmdset:
|
||||
cmdset.permanent = True
|
||||
self.cmdset_stack.append(cmdset)
|
||||
|
||||
|
||||
# merge the stack into a new merged cmdset
|
||||
new_current = None
|
||||
self.mergetype_stack = []
|
||||
|
|
@ -226,6 +228,7 @@ class CmdSetHandler(object):
|
|||
|
||||
def import_cmdset(self, cmdset_path, emit_to_obj=None):
|
||||
"""
|
||||
Method wrapper for import_cmdset.
|
||||
load a cmdset from a module.
|
||||
cmdset_path - the python path to an cmdset object.
|
||||
emit_to_obj - object to send error messages to
|
||||
|
|
@ -243,8 +246,7 @@ class CmdSetHandler(object):
|
|||
cmdset - can be a cmdset object or the python path to
|
||||
such an object.
|
||||
emit_to_obj - an object to receive error messages.
|
||||
permanent - create a script to automatically add the cmdset
|
||||
every time the server starts/the object logins.
|
||||
permanent - this cmdset will remain across a server reboot
|
||||
|
||||
Note: An interesting feature of this method is if you were to
|
||||
send it an *already instantiated cmdset* (i.e. not a class),
|
||||
|
|
@ -260,16 +262,17 @@ class CmdSetHandler(object):
|
|||
cmdset = cmdset(self.obj)
|
||||
elif isinstance(cmdset, basestring):
|
||||
# this is (maybe) a python path. Try to import from cache.
|
||||
cmdset = self.import_cmdset(cmdset)#, emit_to_obj)
|
||||
cmdset = self.import_cmdset(cmdset)
|
||||
if cmdset:
|
||||
self.cmdset_stack.append(cmdset)
|
||||
if permanent:
|
||||
# store the path permanently
|
||||
self.permanent_paths.append(cmdset.path)
|
||||
self.obj.cmdset_storage = self.permanent_paths
|
||||
cmdset.permanent = True
|
||||
storage = self.obj.cmdset_storage
|
||||
storage.append(cmdset.path)
|
||||
self.obj.cmdset_storage = storage
|
||||
else:
|
||||
# store an empty entry and don't save (this makes it easy to delete).
|
||||
self.permanent_paths.append("")
|
||||
cmdset.permanent = False
|
||||
self.cmdset_stack.append(cmdset)
|
||||
self.update()
|
||||
|
||||
def add_default(self, cmdset, emit_to_obj=None, permanent=True):
|
||||
|
|
@ -298,16 +301,15 @@ class CmdSetHandler(object):
|
|||
self.mergetype_stack = [cmdset.mergetype]
|
||||
|
||||
if permanent:
|
||||
if self.permanent_paths:
|
||||
self.permanent_paths[0] = cmdset.path
|
||||
cmdset.permanent = True
|
||||
storage = self.obj.cmdset_storage
|
||||
if storage:
|
||||
storage[0] = cmdset.path
|
||||
else:
|
||||
self.permanent_paths = [cmdset.path]
|
||||
self.obj.cmdset_storage = self.permanent_paths
|
||||
storage = [cmdset.path]
|
||||
self.obj.cmdset_storage = storage
|
||||
else:
|
||||
if self.permanent_paths:
|
||||
self.permanent_paths[0] = ""
|
||||
else:
|
||||
self.permanent_paths = [""]
|
||||
cmdset.permanent = False
|
||||
self.update()
|
||||
|
||||
def delete(self, cmdset=None):
|
||||
|
|
@ -328,34 +330,53 @@ class CmdSetHandler(object):
|
|||
return
|
||||
|
||||
if not cmdset:
|
||||
# remove the last one in the stack (except the default position)
|
||||
self.cmdset_stack.pop()
|
||||
self.permanent_paths.pop()
|
||||
# remove the last one in the stack
|
||||
cmdset = self.cmdset_stack.pop()
|
||||
if cmdset.permanent:
|
||||
storage = self.obj.cmdset_storage
|
||||
storage.pop()
|
||||
self.obj.cmdset_storage = storage
|
||||
else:
|
||||
# try it as a callable
|
||||
if callable(cmdset) and hasattr(cmdset, 'path'):
|
||||
indices = [i+1 for i, cset in enumerate(self.cmdset_stack[1:]) if cset.path == cmdset.path]
|
||||
delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset.path]
|
||||
else:
|
||||
# try it as a path or key
|
||||
indices = [i+1 for i, cset in enumerate(self.cmdset_stack[1:]) if cset.path == cmdset or cset.key == cmdset]
|
||||
|
||||
for i in indices:
|
||||
del self.cmdset_stack[i]
|
||||
del self.permanent_paths[i]
|
||||
self.obj.cmdset_storage = self.permanent_paths
|
||||
|
||||
delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset or cset.key == cmdset]
|
||||
storage = []
|
||||
|
||||
if any(cset.permanent for cset in delcmdsets):
|
||||
# only hit database if there's need to
|
||||
storage = self.obj.cmdset_storage
|
||||
for cset in delcmdsets:
|
||||
if cset.permanent:
|
||||
try:
|
||||
storage.remove(cset.path)
|
||||
except ValueError:
|
||||
pass
|
||||
for cset in delcmdsets:
|
||||
# clean the in-memory stack
|
||||
try:
|
||||
self.cmdset_stack.remove(cset)
|
||||
except ValueError:
|
||||
pass
|
||||
# re-sync the cmdsethandler.
|
||||
self.update()
|
||||
|
||||
def delete_default(self):
|
||||
"This explicitly deletes the default cmdset. It's the only command that can."
|
||||
if self.cmdset_stack:
|
||||
cmdset = self.cmdet_stack[0]
|
||||
if cmdset.permanent:
|
||||
storage = self.obj.cmdset_storage
|
||||
if storage:
|
||||
storage[0] = ""
|
||||
else:
|
||||
storage = [""]
|
||||
self.cmdset_storage = storage
|
||||
self.cmdset_stack[0] = CmdSet(cmdsetobj=self.obj, key="Empty")
|
||||
self.permanent_paths[0] = ""
|
||||
else:
|
||||
else:
|
||||
self.cmdset_stack = [CmdSet(cmdsetobj=self.obj, key="Empty")]
|
||||
self.permanent_paths = [""]
|
||||
self.obj.cmdset_storage = self.permanent_paths
|
||||
self.update()
|
||||
|
||||
def all(self):
|
||||
|
|
@ -371,8 +392,10 @@ class CmdSetHandler(object):
|
|||
"""
|
||||
self.cmdset_stack = [self.cmdset_stack[0]]
|
||||
self.mergetype_stack = [self.cmdset_stack[0].mergetype]
|
||||
self.permanent_paths = [self.permanent_paths[0]]
|
||||
self.obj.cmdset_storage = self.permanent_paths
|
||||
storage = self.obj.cmdset_storage
|
||||
if storage:
|
||||
storage = storage[0]
|
||||
self.obj.cmdset_storage = storage
|
||||
self.update()
|
||||
|
||||
def all(self):
|
||||
|
|
|
|||
|
|
@ -21,8 +21,11 @@ class CommandMeta(type):
|
|||
"""
|
||||
mcs.key = mcs.key.lower()
|
||||
if mcs.aliases and not is_iter(mcs.aliases):
|
||||
mcs.aliases = mcs.aliases.split(',')
|
||||
mcs.aliases = [str(alias).strip().lower() for alias in mcs.aliases]
|
||||
try:
|
||||
mcs.aliases = mcs.aliases.split(',')
|
||||
except Exception:
|
||||
mcs.aliases = []
|
||||
mcs.aliases = [str(alias).strip() for alias in mcs.aliases]
|
||||
|
||||
# pre-process locks as defined in class definition
|
||||
temp = []
|
||||
|
|
|
|||
|
|
@ -390,7 +390,9 @@ class CmdCreate(ObjManipCommand):
|
|||
# (i.e. starts with game or src) we let it be, otherwise we
|
||||
# add a base path as defined in settings
|
||||
if typeclass and not (typeclass.startswith('src.') or
|
||||
typeclass.startswith('game.')):
|
||||
typeclass.startswith('game.') or
|
||||
typeclass.startswith('contrib')):
|
||||
|
||||
typeclass = "%s.%s" % (settings.BASE_TYPECLASS_PATH,
|
||||
typeclass)
|
||||
|
||||
|
|
@ -1477,13 +1479,13 @@ class CmdExamine(ObjManipCommand):
|
|||
#self.caller.msg(db_attr)
|
||||
string += headers["persistent"]
|
||||
for attr, value in db_attr:
|
||||
if crop:
|
||||
if crop and isinstance(value, basestring):
|
||||
value = utils.crop(value)
|
||||
string += "\n %s = %s" % (attr, value)
|
||||
if ndb_attr and ndb_attr[0]:
|
||||
string += headers["nonpersistent"]
|
||||
for attr, value in ndb_attr:
|
||||
if crop:
|
||||
if crop and isinstance(value, basestring):
|
||||
value = utils.crop(value)
|
||||
string += "\n %s = %s" % (attr, value)
|
||||
return string
|
||||
|
|
|
|||
|
|
@ -485,7 +485,7 @@ class CmdPose(MuxCommand):
|
|||
"""
|
||||
args = self.args
|
||||
if args and not args[0] in ["'", ",", ":"]:
|
||||
args = " %s" % args
|
||||
args = " %s" % args.strip()
|
||||
self.args = args
|
||||
|
||||
def func(self):
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ from src.utils import create, ansi
|
|||
from src.server import session, sessionhandler
|
||||
from src.locks.lockhandler import LockHandler
|
||||
from src.server.models import ServerConfig
|
||||
from src.comms.models import Channel, Msg, PlayerChannelConnection
|
||||
from src.comms.models import Channel, Msg, PlayerChannelConnection, ExternalChannelConnection
|
||||
from django.contrib.auth.models import User
|
||||
from src.players.models import PlayerDB
|
||||
from src.objects.models import ObjectDB
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Command testing
|
||||
|
|
@ -32,6 +35,17 @@ from src.comms.models import Channel, Msg, PlayerChannelConnection
|
|||
# print all feedback from test commands (can become very verbose!)
|
||||
VERBOSE = False
|
||||
|
||||
|
||||
def cleanup():
|
||||
User.objects.all().delete()
|
||||
PlayerDB.objects.all().delete()
|
||||
ObjectDB.objects.all().delete()
|
||||
Channel.objects.all().delete()
|
||||
Msg.objects.all().delete()
|
||||
PlayerChannelConnection.objects.all().delete()
|
||||
ExternalChannelConnection.objects.all().delete()
|
||||
ServerConfig.objects.all().delete()
|
||||
|
||||
class FakeSession(session.Session):
|
||||
"""
|
||||
A fake session that
|
||||
|
|
@ -76,15 +90,16 @@ class CommandTest(TestCase):
|
|||
Inherit new tests from this.
|
||||
"""
|
||||
|
||||
NOMANGLE = False # mangle command input for extra testing
|
||||
NOMANGLE = True # mangle command input for extra testing
|
||||
|
||||
def setUp(self):
|
||||
"sets up the testing environment"
|
||||
ServerConfig.objects.conf("default_home", 2)
|
||||
|
||||
self.addCleanup(cleanup)
|
||||
|
||||
self.room1 = create.create_object(settings.BASE_ROOM_TYPECLASS, key="room1")
|
||||
self.room2 = create.create_object(settings.BASE_ROOM_TYPECLASS, key="room2")
|
||||
|
||||
self.room2 = create.create_object(settings.BASE_ROOM_TYPECLASS, key="room2")
|
||||
# create a faux player/character for testing.
|
||||
self.char1 = create.create_player("TestChar", "testplayer@test.com", "testpassword", location=self.room1)
|
||||
self.char1.player.user.is_superuser = True
|
||||
|
|
@ -111,6 +126,17 @@ class CommandTest(TestCase):
|
|||
self.exit1 = create.create_object(settings.BASE_EXIT_TYPECLASS, key="exit1", location=self.room1)
|
||||
self.exit2 = create.create_object(settings.BASE_EXIT_TYPECLASS, key="exit2", location=self.room2)
|
||||
|
||||
def tearDown(self):
|
||||
"Cleans up testing environment after test has run."
|
||||
User.objects.all().delete()
|
||||
PlayerDB.objects.all().delete()
|
||||
ObjectDB.objects.all().delete()
|
||||
Channel.objects.all().delete()
|
||||
Msg.objects.all().delete()
|
||||
PlayerChannelConnection.objects.all().delete()
|
||||
ExternalChannelConnection.objects.all().delete()
|
||||
ServerConfig.objects.all().delete()
|
||||
|
||||
def get_cmd(self, cmd_class, argument_string=""):
|
||||
"""
|
||||
Obtain a cmd instance from a class and an input string
|
||||
|
|
@ -401,15 +427,16 @@ class TestChannelCreate(CommandTest):
|
|||
self.execute_cmd("@ccreate testchannel1;testchan1;testchan1b = This is a test channel")
|
||||
self.execute_cmd("testchan1 Hello", "[testchannel1] TestChar: Hello")
|
||||
class TestAddCom(CommandTest):
|
||||
def test_call(self):
|
||||
def test_call(self):
|
||||
self.execute_cmd("@cdestroy testchannel1", "Channel 'testchannel1'")
|
||||
self.execute_cmd("@ccreate testchannel1;testchan1;testchan1b = This is a test channel")
|
||||
self.execute_cmd("addcom chan1 = testchannel1")
|
||||
self.execute_cmd("addcom chan2 = testchan1")
|
||||
self.execute_cmd("delcom testchannel1")
|
||||
self.execute_cmd("addcom testchannel1" "You now listen to the channel channel.")
|
||||
|
||||
class TestDelCom(CommandTest):
|
||||
def test_call(self):
|
||||
self.execute_cmd("@cdestroy testchannel1", "Channel 'testchannel1'")
|
||||
self.execute_cmd("@ccreate testchannel1;testchan1;testchan1b = This is a test channel")
|
||||
self.execute_cmd("addcom chan1 = testchan1")
|
||||
self.execute_cmd("addcom chan2 = testchan1b")
|
||||
|
|
@ -430,7 +457,9 @@ class TestChannels(CommandTest):
|
|||
self.execute_cmd("@cdestroy testchannel1", "Channel 'testchannel1'")
|
||||
class TestCBoot(CommandTest):
|
||||
def test_call(self):
|
||||
self.execute_cmd("@cdestroy testchannel1", "Channel 'testchannel1'")
|
||||
self.execute_cmd("@ccreate testchannel1;testchan1;testchan1b = This is a test channel")
|
||||
self.execute_cmd("addcom testchan = testchannel1")
|
||||
self.execute_cmd("@cboot testchannel1 = TestChar", "TestChar boots TestChar from channel.")
|
||||
class TestCemit(CommandTest):
|
||||
def test_call(self):
|
||||
|
|
|
|||
|
|
@ -137,12 +137,8 @@ DATABASE_PORT = ''
|
|||
|
||||
# An alternate command parser module to use
|
||||
COMMAND_PARSER = "src.commands.cmdparser.cmdparser"
|
||||
# How many space-separated words a command name may have
|
||||
# and still be identified as one single command
|
||||
# (e.g. 'push button' instead of 'pushbutton')
|
||||
COMMAND_MAXLEN = 3
|
||||
# The handler that outputs errors when searching
|
||||
# objects using object.search().
|
||||
# objects using object.search().
|
||||
SEARCH_AT_RESULT = "src.commands.cmdparser.at_search_result"
|
||||
# The parser used in order to separate multiple
|
||||
# object matches (so you can separate between same-named
|
||||
|
|
|
|||
|
|
@ -52,12 +52,12 @@ def crop(text, width=78, suffix="[...]"):
|
|||
continues. Cropping will be done so that the suffix will also fit
|
||||
within the given width.
|
||||
"""
|
||||
ltext = len(str(text))
|
||||
ltext = len(to_str(text))
|
||||
if ltext <= width:
|
||||
return text
|
||||
else:
|
||||
lsuffix = len(suffix)
|
||||
return text[:width-lsuffix] + suffix
|
||||
return "%s%s" % (text[:width-lsuffix], suffix)
|
||||
|
||||
def dedent(text):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue