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:
Griatch 2011-05-12 21:51:11 +00:00
parent 2c47d6a66b
commit b9c1921a0b
16 changed files with 1426 additions and 354 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

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