Merge branch 'master' of https://github.com/Henddher/evennia into bug_1684
This commit is contained in:
commit
01109de875
128 changed files with 22108 additions and 3929 deletions
|
|
@ -586,11 +586,9 @@ class CmdSetHandler(object):
|
|||
"""
|
||||
if callable(cmdset) and hasattr(cmdset, 'path'):
|
||||
# try it as a callable
|
||||
print "Try callable", cmdset
|
||||
if must_be_default:
|
||||
return self.cmdset_stack and (self.cmdset_stack[0].path == cmdset.path)
|
||||
else:
|
||||
print [cset.path for cset in self.cmdset_stack], cmdset.path
|
||||
return any([cset for cset in self.cmdset_stack
|
||||
if cset.path == cmdset.path])
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ class Command(with_metaclass(CommandMeta, object)):
|
|||
Args:
|
||||
srcobj (Object): Object trying to gain permission
|
||||
access_type (str, optional): The lock type to check.
|
||||
default (bool, optional): The fallbacl result if no lock
|
||||
default (bool, optional): The fallback result if no lock
|
||||
of matching `access_type` is found on this Command.
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
|||
key = self.lhs
|
||||
desc = self.rhs
|
||||
|
||||
charmax = _MAX_NR_CHARACTERS if _MULTISESSION_MODE > 1 else 1
|
||||
charmax = _MAX_NR_CHARACTERS
|
||||
|
||||
if not account.is_superuser and \
|
||||
(account.db._playable_characters and
|
||||
|
|
@ -168,7 +168,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
|||
if desc:
|
||||
new_character.db.desc = desc
|
||||
elif not new_character.db.desc:
|
||||
new_character.db.desc = "This is an Account."
|
||||
new_character.db.desc = "This is a character."
|
||||
self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character."
|
||||
% (new_character.key, new_character.key))
|
||||
|
||||
|
|
@ -455,7 +455,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
Usage:
|
||||
@option[/save] [name = value]
|
||||
|
||||
Switch:
|
||||
Switches:
|
||||
save - Save the current option settings for future logins.
|
||||
clear - Clear the saved options.
|
||||
|
||||
|
|
@ -467,6 +467,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@option"
|
||||
aliases = "@options"
|
||||
switch_options = ("save", "clear")
|
||||
locks = "cmd:all()"
|
||||
|
||||
# this is used by the parent
|
||||
|
|
@ -626,10 +627,16 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
oldpass = self.lhslist[0] # Both of these are
|
||||
newpass = self.rhslist[0] # already stripped by parse()
|
||||
|
||||
# Validate password
|
||||
validated, error = account.validate_password(newpass)
|
||||
|
||||
if not account.check_password(oldpass):
|
||||
self.msg("The specified old password isn't correct.")
|
||||
elif len(newpass) < 3:
|
||||
self.msg("Passwords must be at least three characters long.")
|
||||
elif not validated:
|
||||
errors = [e for suberror in error.messages for e in error.messages]
|
||||
string = "\n".join(errors)
|
||||
self.msg(string)
|
||||
else:
|
||||
account.set_password(newpass)
|
||||
account.save()
|
||||
|
|
@ -650,6 +657,7 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
|
|||
game. Use the /all switch to disconnect from all sessions.
|
||||
"""
|
||||
key = "@quit"
|
||||
switch_options = ("all",)
|
||||
locks = "cmd:all()"
|
||||
|
||||
# this is used by the parent
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
|
||||
key = "@boot"
|
||||
switch_options = ("quiet", "sid")
|
||||
locks = "cmd:perm(boot) or perm(Admin)"
|
||||
help_category = "Admin"
|
||||
|
||||
|
|
@ -265,6 +266,7 @@ class CmdDelAccount(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
|
||||
key = "@delaccount"
|
||||
switch_options = ("delobj",)
|
||||
locks = "cmd:perm(delaccount) or perm(Developer)"
|
||||
help_category = "Admin"
|
||||
|
||||
|
|
@ -329,9 +331,9 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
|
|||
@pemit [<obj>, <obj>, ... =] <message>
|
||||
|
||||
Switches:
|
||||
room : limit emits to rooms only (default)
|
||||
accounts : limit emits to accounts only
|
||||
contents : send to the contents of matched objects too
|
||||
room - limit emits to rooms only (default)
|
||||
accounts - limit emits to accounts only
|
||||
contents - send to the contents of matched objects too
|
||||
|
||||
Emits a message to the selected objects or to
|
||||
your immediate surroundings. If the object is a room,
|
||||
|
|
@ -341,6 +343,7 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@emit"
|
||||
aliases = ["@pemit", "@remit"]
|
||||
switch_options = ("room", "accounts", "contents")
|
||||
locks = "cmd:perm(emit) or perm(Builder)"
|
||||
help_category = "Admin"
|
||||
|
||||
|
|
@ -425,12 +428,23 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
|
|||
account = caller.search_account(self.lhs)
|
||||
if not account:
|
||||
return
|
||||
account.set_password(self.rhs)
|
||||
|
||||
newpass = self.rhs
|
||||
|
||||
# Validate password
|
||||
validated, error = account.validate_password(newpass)
|
||||
if not validated:
|
||||
errors = [e for suberror in error.messages for e in error.messages]
|
||||
string = "\n".join(errors)
|
||||
caller.msg(string)
|
||||
return
|
||||
|
||||
account.set_password(newpass)
|
||||
account.save()
|
||||
self.msg("%s - new password set to '%s'." % (account.name, self.rhs))
|
||||
self.msg("%s - new password set to '%s'." % (account.name, newpass))
|
||||
if account.character != caller:
|
||||
account.msg("%s has changed your password to '%s'." % (caller.name,
|
||||
self.rhs))
|
||||
newpass))
|
||||
|
||||
|
||||
class CmdPerm(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -442,14 +456,15 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
|||
@perm[/switch] *<account> [= <permission>[,<permission>,...]]
|
||||
|
||||
Switches:
|
||||
del : delete the given permission from <object> or <account>.
|
||||
account : set permission on an account (same as adding * to name)
|
||||
del - delete the given permission from <object> or <account>.
|
||||
account - set permission on an account (same as adding * to name)
|
||||
|
||||
This command sets/clears individual permission strings on an object
|
||||
or account. If no permission is given, list all permissions on <object>.
|
||||
"""
|
||||
key = "@perm"
|
||||
aliases = "@setperm"
|
||||
switch_options = ("del", "account")
|
||||
locks = "cmd:perm(perm) or perm(Developer)"
|
||||
help_category = "Admin"
|
||||
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@batchcommands"
|
||||
aliases = ["@batchcommand", "@batchcmd"]
|
||||
switch_options = ("interactive",)
|
||||
locks = "cmd:perm(batchcommands) or perm(Developer)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -347,6 +348,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@batchcode"
|
||||
aliases = ["@batchcodes"]
|
||||
switch_options = ("interactive", "debug")
|
||||
locks = "cmd:superuser()"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ from evennia.objects.models import ObjectDB
|
|||
from evennia.locks.lockhandler import LockException
|
||||
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
||||
from evennia.utils import create, utils, search
|
||||
from evennia.utils.utils import inherits_from, class_from_module
|
||||
from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.spawner import spawn
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
||||
from evennia.utils.ansi import raw
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
|
@ -26,12 +27,8 @@ __all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy",
|
|||
"CmdLock", "CmdExamine", "CmdFind", "CmdTeleport",
|
||||
"CmdScript", "CmdTag", "CmdSpawn")
|
||||
|
||||
try:
|
||||
# used by @set
|
||||
from ast import literal_eval as _LITERAL_EVAL
|
||||
except ImportError:
|
||||
# literal_eval is not available before Python 2.6
|
||||
_LITERAL_EVAL = None
|
||||
# used by @set
|
||||
from ast import literal_eval as _LITERAL_EVAL
|
||||
|
||||
# used by @find
|
||||
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
|
||||
|
|
@ -106,9 +103,15 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
Usage:
|
||||
@alias <obj> [= [alias[,alias,alias,...]]]
|
||||
@alias <obj> =
|
||||
@alias/category <obj> = [alias[,alias,...]:<category>
|
||||
|
||||
Switches:
|
||||
category - requires ending input with :category, to store the
|
||||
given aliases with the given category.
|
||||
|
||||
Assigns aliases to an object so it can be referenced by more
|
||||
than one name. Assign empty to remove all aliases from object.
|
||||
than one name. Assign empty to remove all aliases from object. If
|
||||
assigning a category, all aliases given will be using this category.
|
||||
|
||||
Observe that this is not the same thing as personal aliases
|
||||
created with the 'nick' command! Aliases set with @alias are
|
||||
|
|
@ -118,6 +121,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@alias"
|
||||
aliases = "@setobjalias"
|
||||
switch_options = ("category",)
|
||||
locks = "cmd:perm(setobjalias) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -138,9 +142,12 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
if self.rhs is None:
|
||||
# no =, so we just list aliases on object.
|
||||
aliases = obj.aliases.all()
|
||||
aliases = obj.aliases.all(return_key_and_category=True)
|
||||
if aliases:
|
||||
caller.msg("Aliases for '%s': %s" % (obj.get_display_name(caller), ", ".join(aliases)))
|
||||
caller.msg("Aliases for %s: %s" % (
|
||||
obj.get_display_name(caller),
|
||||
", ".join("'%s'%s" % (alias, "" if category is None else "[category:'%s']" % category)
|
||||
for (alias, category) in aliases)))
|
||||
else:
|
||||
caller.msg("No aliases exist for '%s'." % obj.get_display_name(caller))
|
||||
return
|
||||
|
|
@ -159,17 +166,27 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("No aliases to clear.")
|
||||
return
|
||||
|
||||
category = None
|
||||
if "category" in self.switches:
|
||||
if ":" in self.rhs:
|
||||
rhs, category = self.rhs.rsplit(':', 1)
|
||||
category = category.strip()
|
||||
else:
|
||||
caller.msg("If specifying the /category switch, the category must be given "
|
||||
"as :category at the end.")
|
||||
else:
|
||||
rhs = self.rhs
|
||||
|
||||
# merge the old and new aliases (if any)
|
||||
old_aliases = obj.aliases.all()
|
||||
new_aliases = [alias.strip().lower() for alias in self.rhs.split(',')
|
||||
if alias.strip()]
|
||||
old_aliases = obj.aliases.get(category=category, return_list=True)
|
||||
new_aliases = [alias.strip().lower() for alias in rhs.split(',') if alias.strip()]
|
||||
|
||||
# make the aliases only appear once
|
||||
old_aliases.extend(new_aliases)
|
||||
aliases = list(set(old_aliases))
|
||||
|
||||
# save back to object.
|
||||
obj.aliases.add(aliases)
|
||||
obj.aliases.add(aliases, category=category)
|
||||
|
||||
# we need to trigger this here, since this will force
|
||||
# (default) Exits to rebuild their Exit commands with the new
|
||||
|
|
@ -177,7 +194,8 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
|||
obj.at_cmdset_get(force_init=True)
|
||||
|
||||
# report all aliases on the object
|
||||
caller.msg("Alias(es) for '%s' set to %s." % (obj.get_display_name(caller), str(obj.aliases)))
|
||||
caller.msg("Alias(es) for '%s' set to '%s'%s." % (obj.get_display_name(caller),
|
||||
str(obj.aliases), " (category: '%s')" % category if category else ""))
|
||||
|
||||
|
||||
class CmdCopy(ObjManipCommand):
|
||||
|
|
@ -198,6 +216,7 @@ class CmdCopy(ObjManipCommand):
|
|||
"""
|
||||
|
||||
key = "@copy"
|
||||
switch_options = ("reset",)
|
||||
locks = "cmd:perm(copy) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -279,6 +298,7 @@ class CmdCpAttr(ObjManipCommand):
|
|||
If you don't supply a source object, yourself is used.
|
||||
"""
|
||||
key = "@cpattr"
|
||||
switch_options = ("move",)
|
||||
locks = "cmd:perm(cpattr) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -420,6 +440,7 @@ class CmdMvAttr(ObjManipCommand):
|
|||
object. If you don't supply a source object, yourself is used.
|
||||
"""
|
||||
key = "@mvattr"
|
||||
switch_options = ("copy",)
|
||||
locks = "cmd:perm(mvattr) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -468,6 +489,7 @@ class CmdCreate(ObjManipCommand):
|
|||
"""
|
||||
|
||||
key = "@create"
|
||||
switch_options = ("drop",)
|
||||
locks = "cmd:perm(create) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -553,6 +575,7 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@desc"
|
||||
aliases = "@describe"
|
||||
switch_options = ("edit",)
|
||||
locks = "cmd:perm(desc) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -614,11 +637,11 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
|||
Usage:
|
||||
@destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]
|
||||
|
||||
switches:
|
||||
Switches:
|
||||
override - The @destroy command will usually avoid accidentally
|
||||
destroying account objects. This switch overrides this safety.
|
||||
force - destroy without confirmation.
|
||||
examples:
|
||||
Examples:
|
||||
@destroy house, roof, door, 44-78
|
||||
@destroy 5-10, flower, 45
|
||||
@destroy/force north
|
||||
|
|
@ -631,6 +654,7 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@destroy"
|
||||
aliases = ["@delete", "@del"]
|
||||
switch_options = ("override", "force")
|
||||
locks = "cmd:perm(destroy) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -754,6 +778,7 @@ class CmdDig(ObjManipCommand):
|
|||
would be 'north;no;n'.
|
||||
"""
|
||||
key = "@dig"
|
||||
switch_options = ("teleport",)
|
||||
locks = "cmd:perm(dig) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -863,7 +888,7 @@ class CmdDig(ObjManipCommand):
|
|||
new_back_exit.dbref,
|
||||
alias_string)
|
||||
caller.msg("%s%s%s" % (room_string, exit_to_string, exit_back_string))
|
||||
if new_room and ('teleport' in self.switches or "tel" in self.switches):
|
||||
if new_room and 'teleport' in self.switches:
|
||||
caller.move_to(new_room)
|
||||
|
||||
|
||||
|
|
@ -896,6 +921,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@tunnel"
|
||||
aliases = ["@tun"]
|
||||
switch_options = ("oneway", "tel")
|
||||
locks = "cmd: perm(tunnel) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -1429,17 +1455,16 @@ def _convert_from_string(cmd, strobj):
|
|||
# if nothing matches, return as-is
|
||||
return obj
|
||||
|
||||
if _LITERAL_EVAL:
|
||||
# Use literal_eval to parse python structure exactly.
|
||||
try:
|
||||
return _LITERAL_EVAL(strobj)
|
||||
except (SyntaxError, ValueError):
|
||||
# treat as string
|
||||
strobj = utils.to_str(strobj)
|
||||
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
|
||||
"Make sure this is acceptable." % strobj
|
||||
cmd.caller.msg(string)
|
||||
return strobj
|
||||
# Use literal_eval to parse python structure exactly.
|
||||
try:
|
||||
return _LITERAL_EVAL(strobj)
|
||||
except (SyntaxError, ValueError):
|
||||
# treat as string
|
||||
strobj = utils.to_str(strobj)
|
||||
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
|
||||
"Make sure this is acceptable." % strobj
|
||||
cmd.caller.msg(string)
|
||||
return strobj
|
||||
else:
|
||||
# fall back to old recursive solution (does not support
|
||||
# nested lists/dicts)
|
||||
|
|
@ -1458,6 +1483,13 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
|
||||
Switch:
|
||||
edit: Open the line editor (string values only)
|
||||
script: If we're trying to set an attribute on a script
|
||||
channel: If we're trying to set an attribute on a channel
|
||||
account: If we're trying to set an attribute on an account
|
||||
room: Setting an attribute on a room (global search)
|
||||
exit: Setting an attribute on an exit (global search)
|
||||
char: Setting an attribute on a character (global search)
|
||||
character: Alias for char, as above.
|
||||
|
||||
Sets attributes on objects. The second form clears
|
||||
a previously set attribute while the last form
|
||||
|
|
@ -1558,6 +1590,38 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
# start the editor
|
||||
EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr))
|
||||
|
||||
def search_for_obj(self, objname):
|
||||
"""
|
||||
Searches for an object matching objname. The object may be of different typeclasses.
|
||||
Args:
|
||||
objname: Name of the object we're looking for
|
||||
|
||||
Returns:
|
||||
A typeclassed object, or None if nothing is found.
|
||||
"""
|
||||
from evennia.utils.utils import variable_from_module
|
||||
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||
caller = self.caller
|
||||
if objname.startswith('*') or "account" in self.switches:
|
||||
found_obj = caller.search_account(objname.lstrip('*'))
|
||||
elif "script" in self.switches:
|
||||
found_obj = _AT_SEARCH_RESULT(search.search_script(objname), caller)
|
||||
elif "channel" in self.switches:
|
||||
found_obj = _AT_SEARCH_RESULT(search.search_channel(objname), caller)
|
||||
else:
|
||||
global_search = True
|
||||
if "char" in self.switches or "character" in self.switches:
|
||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
elif "room" in self.switches:
|
||||
typeclass = settings.BASE_ROOM_TYPECLASS
|
||||
elif "exit" in self.switches:
|
||||
typeclass = settings.BASE_EXIT_TYPECLASS
|
||||
else:
|
||||
global_search = False
|
||||
typeclass = None
|
||||
found_obj = caller.search(objname, global_search=global_search, typeclass=typeclass)
|
||||
return found_obj
|
||||
|
||||
def func(self):
|
||||
"""Implement the set attribute - a limited form of @py."""
|
||||
|
||||
|
|
@ -1571,10 +1635,7 @@ class CmdSetAttribute(ObjManipCommand):
|
|||
objname = self.lhs_objattr[0]['name']
|
||||
attrs = self.lhs_objattr[0]['attrs']
|
||||
|
||||
if objname.startswith('*'):
|
||||
obj = caller.search_account(objname.lstrip('*'))
|
||||
else:
|
||||
obj = caller.search(objname)
|
||||
obj = self.search_for_obj(objname)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
|
|
@ -1637,17 +1698,22 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
@typeclass[/switch] <object> [= typeclass.path]
|
||||
@type ''
|
||||
@parent ''
|
||||
@typeclass/list/show [typeclass.path]
|
||||
@swap - this is a shorthand for using /force/reset flags.
|
||||
@update - this is a shorthand for using the /force/reload flag.
|
||||
|
||||
Switch:
|
||||
show - display the current typeclass of object (default)
|
||||
show, examine - display the current typeclass of object (default) or, if
|
||||
given a typeclass path, show the docstring of that typeclass.
|
||||
update - *only* re-run at_object_creation on this object
|
||||
meaning locks or other properties set later may remain.
|
||||
reset - clean out *all* the attributes and properties on the
|
||||
object - basically making this a new clean object.
|
||||
force - change to the typeclass also if the object
|
||||
already has a typeclass of the same name.
|
||||
list - show available typeclasses.
|
||||
|
||||
|
||||
Example:
|
||||
@type button = examples.red_button.RedButton
|
||||
|
||||
|
|
@ -1671,6 +1737,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@typeclass"
|
||||
aliases = ["@type", "@parent", "@swap", "@update"]
|
||||
switch_options = ("show", "examine", "update", "reset", "force", "list")
|
||||
locks = "cmd:perm(typeclass) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -1679,10 +1746,56 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
caller = self.caller
|
||||
|
||||
if 'list' in self.switches:
|
||||
tclasses = get_all_typeclasses()
|
||||
contribs = [key for key in sorted(tclasses)
|
||||
if key.startswith("evennia.contrib")] or ["<None loaded>"]
|
||||
core = [key for key in sorted(tclasses)
|
||||
if key.startswith("evennia") and key not in contribs] or ["<None loaded>"]
|
||||
game = [key for key in sorted(tclasses)
|
||||
if not key.startswith("evennia")] or ["<None loaded>"]
|
||||
string = ("|wCore typeclasses|n\n"
|
||||
" {core}\n"
|
||||
"|wLoaded Contrib typeclasses|n\n"
|
||||
" {contrib}\n"
|
||||
"|wGame-dir typeclasses|n\n"
|
||||
" {game}").format(core="\n ".join(core),
|
||||
contrib="\n ".join(contribs),
|
||||
game="\n ".join(game))
|
||||
EvMore(caller, string, exit_on_lastpage=True)
|
||||
return
|
||||
|
||||
if not self.args:
|
||||
caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring)
|
||||
return
|
||||
|
||||
if "show" in self.switches or "examine" in self.switches:
|
||||
oquery = self.lhs
|
||||
obj = caller.search(oquery, quiet=True)
|
||||
if not obj:
|
||||
# no object found to examine, see if it's a typeclass-path instead
|
||||
tclasses = get_all_typeclasses()
|
||||
matches = [(key, tclass)
|
||||
for key, tclass in tclasses.items() if key.endswith(oquery)]
|
||||
nmatches = len(matches)
|
||||
if nmatches > 1:
|
||||
caller.msg("Multiple typeclasses found matching {}:\n {}".format(
|
||||
oquery, "\n ".join(tup[0] for tup in matches)))
|
||||
elif not matches:
|
||||
caller.msg("No object or typeclass path found to match '{}'".format(oquery))
|
||||
else:
|
||||
# one match found
|
||||
caller.msg("Docstring for typeclass '{}':\n{}".format(
|
||||
oquery, matches[0][1].__doc__))
|
||||
else:
|
||||
# do the search again to get the error handling in case of multi-match
|
||||
obj = caller.search(oquery)
|
||||
if not obj:
|
||||
return
|
||||
caller.msg("{}'s current typeclass is '{}.{}'".format(
|
||||
obj.name, obj.__class__.__module__, obj.__class__.__name__))
|
||||
return
|
||||
|
||||
# get object to swap on
|
||||
obj = caller.search(self.lhs)
|
||||
if not obj:
|
||||
|
|
@ -1695,7 +1808,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
new_typeclass = self.rhs or obj.path
|
||||
|
||||
if "show" in self.switches:
|
||||
if "show" in self.switches or "examine" in self.switches:
|
||||
string = "%s's current typeclass is %s." % (obj.name, obj.__class__)
|
||||
caller.msg(string)
|
||||
return
|
||||
|
|
@ -2114,12 +2227,15 @@ class CmdExamine(ObjManipCommand):
|
|||
else:
|
||||
things.append(content)
|
||||
if exits:
|
||||
string += "\n|wExits|n: %s" % ", ".join(["%s(%s)" % (exit.name, exit.dbref) for exit in exits])
|
||||
string += "\n|wExits|n: %s" % ", ".join(
|
||||
["%s(%s)" % (exit.name, exit.dbref) for exit in exits])
|
||||
if pobjs:
|
||||
string += "\n|wCharacters|n: %s" % ", ".join(["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs])
|
||||
string += "\n|wCharacters|n: %s" % ", ".join(
|
||||
["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs])
|
||||
if things:
|
||||
string += "\n|wContents|n: %s" % ", ".join(["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
|
||||
if cont not in exits and cont not in pobjs])
|
||||
string += "\n|wContents|n: %s" % ", ".join(
|
||||
["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
|
||||
if cont not in exits and cont not in pobjs])
|
||||
separator = "-" * _DEFAULT_WIDTH
|
||||
# output info
|
||||
return '%s\n%s\n%s' % (separator, string.strip(), separator)
|
||||
|
|
@ -2202,12 +2318,15 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
Usage:
|
||||
@find[/switches] <name or dbref or *account> [= dbrefmin[-dbrefmax]]
|
||||
@locate - this is a shorthand for using the /loc switch.
|
||||
|
||||
Switches:
|
||||
room - only look for rooms (location=None)
|
||||
exit - only look for exits (destination!=None)
|
||||
char - only look for characters (BASE_CHARACTER_TYPECLASS)
|
||||
exact- only exact matches are returned.
|
||||
room - only look for rooms (location=None)
|
||||
exit - only look for exits (destination!=None)
|
||||
char - only look for characters (BASE_CHARACTER_TYPECLASS)
|
||||
exact - only exact matches are returned.
|
||||
loc - display object location if exists and match has one result
|
||||
startswith - search for names starting with the string, rather than containing
|
||||
|
||||
Searches the database for an object of a particular name or exact #dbref.
|
||||
Use *accountname to search for an account. The switches allows for
|
||||
|
|
@ -2218,6 +2337,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@find"
|
||||
aliases = "@search, @locate"
|
||||
switch_options = ("room", "exit", "char", "exact", "loc", "startswith")
|
||||
locks = "cmd:perm(find) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -2230,6 +2350,9 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("Usage: @find <string> [= low [-high]]")
|
||||
return
|
||||
|
||||
if "locate" in self.cmdstring: # Use option /loc as a default for @locate command alias
|
||||
switches.append('loc')
|
||||
|
||||
searchstring = self.lhs
|
||||
low, high = 1, ObjectDB.objects.all().order_by("-id")[0].id
|
||||
if self.rhs:
|
||||
|
|
@ -2251,7 +2374,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
restrictions = ""
|
||||
if self.switches:
|
||||
restrictions = ", %s" % (",".join(self.switches))
|
||||
restrictions = ", %s" % (", ".join(self.switches))
|
||||
|
||||
if is_dbref or is_account:
|
||||
|
||||
|
|
@ -2279,6 +2402,8 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
else:
|
||||
result = result[0]
|
||||
string += "\n|g %s - %s|n" % (result.get_display_name(caller), result.path)
|
||||
if "loc" in self.switches and not is_account and result.location:
|
||||
string += " (|wlocation|n: |g{}|n)".format(result.location.get_display_name(caller))
|
||||
else:
|
||||
# Not an account/dbref search but a wider search; build a queryset.
|
||||
# Searchs for key and aliases
|
||||
|
|
@ -2286,10 +2411,14 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high)
|
||||
aliasquery = Q(db_tags__db_key__iexact=searchstring,
|
||||
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
|
||||
else:
|
||||
elif "startswith" in switches:
|
||||
keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high)
|
||||
aliasquery = Q(db_tags__db_key__istartswith=searchstring,
|
||||
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
|
||||
else:
|
||||
keyquery = Q(db_key__icontains=searchstring, id__gte=low, id__lte=high)
|
||||
aliasquery = Q(db_tags__db_key__icontains=searchstring,
|
||||
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
|
||||
|
||||
results = ObjectDB.objects.filter(keyquery | aliasquery).distinct()
|
||||
nresults = results.count()
|
||||
|
|
@ -2314,6 +2443,8 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
else:
|
||||
string = "|wOne Match|n(#%i-#%i%s):" % (low, high, restrictions)
|
||||
string += "\n |g%s - %s|n" % (results[0].get_display_name(caller), results[0].path)
|
||||
if "loc" in self.switches and nresults == 1 and results[0].location:
|
||||
string += " (|wlocation|n: |g{}|n)".format(results[0].location.get_display_name(caller))
|
||||
else:
|
||||
string = "|wMatch|n(#%i-#%i%s):" % (low, high, restrictions)
|
||||
string += "\n |RNo matches found for '%s'|n" % searchstring
|
||||
|
|
@ -2327,7 +2458,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
|||
teleport object to another location
|
||||
|
||||
Usage:
|
||||
@tel/switch [<object> =] <target location>
|
||||
@tel/switch [<object> to||=] <target location>
|
||||
|
||||
Examples:
|
||||
@tel Limbo
|
||||
|
|
@ -2351,6 +2482,8 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@tel"
|
||||
aliases = "@teleport"
|
||||
switch_options = ("quiet", "intoexit", "tonone", "loc")
|
||||
rhs_split = ("=", " to ") # Prefer = delimiter, but allow " to " usage.
|
||||
locks = "cmd:perm(teleport) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -2458,6 +2591,7 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@script"
|
||||
aliases = "@addscript"
|
||||
switch_options = ("start", "stop")
|
||||
locks = "cmd:perm(script) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -2557,6 +2691,7 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@tag"
|
||||
aliases = ["@tags"]
|
||||
options = ("search", "del")
|
||||
locks = "cmd:perm(tag) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
arg_regex = r"(/\w+?(\s|$))|\s|$"
|
||||
|
|
@ -2654,100 +2789,313 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
string = "No tags attached to %s." % obj
|
||||
self.caller.msg(string)
|
||||
|
||||
#
|
||||
# To use the prototypes with the @spawn function set
|
||||
# PROTOTYPE_MODULES = ["commands.prototypes"]
|
||||
# Reload the server and the prototypes should be available.
|
||||
#
|
||||
|
||||
|
||||
class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
spawn objects from prototype
|
||||
|
||||
Usage:
|
||||
@spawn
|
||||
@spawn[/switch] <prototype_name>
|
||||
@spawn[/switch] {prototype dictionary}
|
||||
@spawn[/noloc] <prototype_key>
|
||||
@spawn[/noloc] <prototype_dict>
|
||||
|
||||
Switch:
|
||||
@spawn/search [prototype_keykey][;tag[,tag]]
|
||||
@spawn/list [tag, tag, ...]
|
||||
@spawn/show [<prototype_key>]
|
||||
@spawn/update <prototype_key>
|
||||
|
||||
@spawn/save <prototype_dict>
|
||||
@spawn/edit [<prototype_key>]
|
||||
@olc - equivalent to @spawn/edit
|
||||
|
||||
Switches:
|
||||
noloc - allow location to be None if not specified explicitly. Otherwise,
|
||||
location will default to caller's current location.
|
||||
search - search prototype by name or tags.
|
||||
list - list available prototypes, optionally limit by tags.
|
||||
show, examine - inspect prototype by key. If not given, acts like list.
|
||||
save - save a prototype to the database. It will be listable by /list.
|
||||
delete - remove a prototype from database, if allowed to.
|
||||
update - find existing objects with the same prototype_key and update
|
||||
them with latest version of given prototype. If given with /save,
|
||||
will auto-update all objects with the old version of the prototype
|
||||
without asking first.
|
||||
edit, olc - create/manipulate prototype in a menu interface.
|
||||
|
||||
Example:
|
||||
@spawn GOBLIN
|
||||
@spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}
|
||||
@spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()
|
||||
|
||||
Dictionary keys:
|
||||
|wprototype |n - name of parent prototype to use. Can be a list for
|
||||
multiple inheritance (inherits left to right)
|
||||
|wprototype_parent |n - name of parent prototype to use. Required if typeclass is
|
||||
not set. Can be a path or a list for multiple inheritance (inherits
|
||||
left to right). If set one of the parents must have a typeclass.
|
||||
|wtypeclass |n - string. Required if prototype_parent is not set.
|
||||
|wkey |n - string, the main object identifier
|
||||
|wtypeclass |n - string, if not set, will use settings.BASE_OBJECT_TYPECLASS
|
||||
|wlocation |n - this should be a valid object or #dbref
|
||||
|whome |n - valid object or #dbref
|
||||
|wdestination|n - only valid for exits (object or dbref)
|
||||
|wpermissions|n - string or list of permission strings
|
||||
|wlocks |n - a lock-string
|
||||
|waliases |n - string or list of strings
|
||||
|waliases |n - string or list of strings.
|
||||
|wndb_|n<name> - value of a nattribute (ndb_ is stripped)
|
||||
|
||||
|wprototype_key|n - name of this prototype. Unique. Used to store/retrieve from db
|
||||
and update existing prototyped objects if desired.
|
||||
|wprototype_desc|n - desc of this prototype. Used in listings
|
||||
|wprototype_locks|n - locks of this prototype. Limits who may use prototype
|
||||
|wprototype_tags|n - tags of this prototype. Used to find prototype
|
||||
|
||||
any other keywords are interpreted as Attributes and their values.
|
||||
|
||||
The available prototypes are defined globally in modules set in
|
||||
settings.PROTOTYPE_MODULES. If @spawn is used without arguments it
|
||||
displays a list of available prototypes.
|
||||
|
||||
"""
|
||||
|
||||
key = "@spawn"
|
||||
aliases = ["olc"]
|
||||
switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc", "update")
|
||||
locks = "cmd:perm(spawn) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
def func(self):
|
||||
"""Implements the spawner"""
|
||||
|
||||
def _show_prototypes(prototypes):
|
||||
"""Helper to show a list of available prototypes"""
|
||||
prots = ", ".join(sorted(prototypes.keys()))
|
||||
return "\nAvailable prototypes (case sensitive): %s" % (
|
||||
"\n" + utils.fill(prots) if prots else "None")
|
||||
def _parse_prototype(inp, expect=dict):
|
||||
err = None
|
||||
try:
|
||||
prototype = _LITERAL_EVAL(inp)
|
||||
except (SyntaxError, ValueError) as err:
|
||||
# treat as string
|
||||
prototype = utils.to_str(inp)
|
||||
finally:
|
||||
if not isinstance(prototype, expect):
|
||||
if err:
|
||||
string = ("{}\n|RCritical Python syntax error in argument. Only primitive "
|
||||
"Python structures are allowed. \nYou also need to use correct "
|
||||
"Python syntax. Remember especially to put quotes around all "
|
||||
"strings inside lists and dicts.|n For more advanced uses, embed "
|
||||
"inline functions in the strings.".format(err))
|
||||
else:
|
||||
string = "Expected {}, got {}.".format(expect, type(prototype))
|
||||
self.caller.msg(string)
|
||||
return None
|
||||
if expect == dict:
|
||||
# an actual prototype. We need to make sure it's safe. Don't allow exec
|
||||
if "exec" in prototype and not self.caller.check_permstring("Developer"):
|
||||
self.caller.msg("Spawn aborted: You are not allowed to "
|
||||
"use the 'exec' prototype key.")
|
||||
return None
|
||||
try:
|
||||
# we homogenize first, to be more lenient
|
||||
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
|
||||
except RuntimeError as err:
|
||||
self.caller.msg(str(err))
|
||||
return
|
||||
return prototype
|
||||
|
||||
prototypes = spawn(return_prototypes=True)
|
||||
if not self.args:
|
||||
string = "Usage: @spawn {key:value, key, value, ... }"
|
||||
self.caller.msg(string + _show_prototypes(prototypes))
|
||||
return
|
||||
try:
|
||||
# make use of _convert_from_string from the SetAttribute command
|
||||
prototype = _convert_from_string(self, self.args)
|
||||
except SyntaxError:
|
||||
# this means literal_eval tried to parse a faulty string
|
||||
string = "|RCritical Python syntax error in argument. "
|
||||
string += "Only primitive Python structures are allowed. "
|
||||
string += "\nYou also need to use correct Python syntax. "
|
||||
string += "Remember especially to put quotes around all "
|
||||
string += "strings inside lists and dicts.|n"
|
||||
self.caller.msg(string)
|
||||
def _search_show_prototype(query, prototypes=None):
|
||||
# prototype detail
|
||||
if not prototypes:
|
||||
prototypes = protlib.search_prototype(key=query)
|
||||
if prototypes:
|
||||
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
|
||||
else:
|
||||
return False
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches:
|
||||
# OLC menu mode
|
||||
prototype = None
|
||||
if self.lhs:
|
||||
key = self.lhs
|
||||
prototype = spawner.search_prototype(key=key, return_meta=True)
|
||||
if len(prototype) > 1:
|
||||
caller.msg("More than one match for {}:\n{}".format(
|
||||
key, "\n".join(proto.get('prototype_key', '') for proto in prototype)))
|
||||
return
|
||||
elif prototype:
|
||||
# one match
|
||||
prototype = prototype[0]
|
||||
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
|
||||
return
|
||||
|
||||
if isinstance(prototype, basestring):
|
||||
# A prototype key
|
||||
keystr = prototype
|
||||
prototype = prototypes.get(prototype, None)
|
||||
if 'search' in self.switches:
|
||||
# query for a key match
|
||||
if not self.args:
|
||||
self.switches.append("list")
|
||||
else:
|
||||
key, tags = self.args.strip(), None
|
||||
if ';' in self.args:
|
||||
key, tags = (part.strip().lower() for part in self.args.split(";", 1))
|
||||
tags = [tag.strip() for tag in tags.split(",")] if tags else None
|
||||
EvMore(caller, unicode(protlib.list_prototypes(caller, key=key, tags=tags)),
|
||||
exit_on_lastpage=True)
|
||||
return
|
||||
|
||||
if 'show' in self.switches or 'examine' in self.switches:
|
||||
# the argument is a key in this case (may be a partial key)
|
||||
if not self.args:
|
||||
self.switches.append('list')
|
||||
else:
|
||||
matchstring = _search_show_prototype(self.args)
|
||||
if matchstring:
|
||||
caller.msg(matchstring)
|
||||
else:
|
||||
caller.msg("No prototype '{}' was found.".format(self.args))
|
||||
return
|
||||
|
||||
if 'list' in self.switches:
|
||||
# for list, all optional arguments are tags
|
||||
# import pudb; pudb.set_trace()
|
||||
|
||||
EvMore(caller, unicode(protlib.list_prototypes(caller,
|
||||
tags=self.lhslist)), exit_on_lastpage=True)
|
||||
return
|
||||
|
||||
if 'save' in self.switches:
|
||||
# store a prototype to the database store
|
||||
if not self.args:
|
||||
caller.msg(
|
||||
"Usage: @spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>")
|
||||
return
|
||||
|
||||
# handle rhs:
|
||||
prototype = _parse_prototype(self.lhs.strip())
|
||||
if not prototype:
|
||||
string = "No prototype named '%s'." % keystr
|
||||
self.caller.msg(string + _show_prototypes(prototypes))
|
||||
return
|
||||
elif isinstance(prototype, dict):
|
||||
# we got the prototype on the command line. We must make sure to not allow
|
||||
# the 'exec' key unless we are developers or higher.
|
||||
if "exec" in prototype and not self.caller.check_permstring("Developer"):
|
||||
self.caller.msg("Spawn aborted: You don't have access to use the 'exec' prototype key.")
|
||||
|
||||
# present prototype to save
|
||||
new_matchstring = _search_show_prototype("", prototypes=[prototype])
|
||||
string = "|yCreating new prototype:|n\n{}".format(new_matchstring)
|
||||
question = "\nDo you want to continue saving? [Y]/N"
|
||||
|
||||
prototype_key = prototype.get("prototype_key")
|
||||
if not prototype_key:
|
||||
caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.")
|
||||
return
|
||||
else:
|
||||
self.caller.msg("The prototype must be a prototype key or a Python dictionary.")
|
||||
|
||||
# check for existing prototype,
|
||||
old_matchstring = _search_show_prototype(prototype_key)
|
||||
|
||||
if old_matchstring:
|
||||
string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring)
|
||||
question = "\n|yDo you want to replace the existing prototype?|n [Y]/N"
|
||||
|
||||
answer = yield(string + question)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rSave cancelled.|n")
|
||||
return
|
||||
|
||||
# all seems ok. Try to save.
|
||||
try:
|
||||
prot = protlib.save_prototype(**prototype)
|
||||
if not prot:
|
||||
caller.msg("|rError saving:|R {}.|n".format(prototype_key))
|
||||
return
|
||||
except protlib.PermissionError as err:
|
||||
caller.msg("|rError saving:|R {}|n".format(err))
|
||||
return
|
||||
caller.msg("|gSaved prototype:|n {}".format(prototype_key))
|
||||
|
||||
# check if we want to update existing objects
|
||||
existing_objects = protlib.search_objects_with_prototype(prototype_key)
|
||||
if existing_objects:
|
||||
if 'update' not in self.switches:
|
||||
n_existing = len(existing_objects)
|
||||
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
||||
string = ("There are {} objects already created with an older version "
|
||||
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
||||
n_existing, prototype_key, slow))
|
||||
answer = yield(string)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rNo update was done of existing objects. "
|
||||
"Use @spawn/update <key> to apply later as needed.|n")
|
||||
return
|
||||
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
||||
caller.msg("{} objects were updated.".format(n_updated))
|
||||
return
|
||||
|
||||
if not self.args:
|
||||
ncount = len(protlib.search_prototype())
|
||||
caller.msg("Usage: @spawn <prototype-key> or {{key: value, ...}}"
|
||||
"\n ({} existing prototypes. Use /list to inspect)".format(ncount))
|
||||
return
|
||||
|
||||
if 'delete' in self.switches:
|
||||
# remove db-based prototype
|
||||
matchstring = _search_show_prototype(self.args)
|
||||
if matchstring:
|
||||
string = "|rDeleting prototype:|n\n{}".format(matchstring)
|
||||
question = "\nDo you want to continue deleting? [Y]/N"
|
||||
answer = yield(string + question)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rDeletion cancelled.|n")
|
||||
return
|
||||
try:
|
||||
success = protlib.delete_db_prototype(caller, self.args)
|
||||
except protlib.PermissionError as err:
|
||||
caller.msg("|rError deleting:|R {}|n".format(err))
|
||||
caller.msg("Deletion {}.".format(
|
||||
'successful' if success else 'failed (does the prototype exist?)'))
|
||||
return
|
||||
else:
|
||||
caller.msg("Could not find prototype '{}'".format(key))
|
||||
|
||||
if 'update' in self.switches:
|
||||
# update existing prototypes
|
||||
key = self.args.strip().lower()
|
||||
existing_objects = protlib.search_objects_with_prototype(key)
|
||||
if existing_objects:
|
||||
n_existing = len(existing_objects)
|
||||
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
||||
string = ("There are {} objects already created with an older version "
|
||||
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
||||
n_existing, key, slow))
|
||||
answer = yield(string)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rUpdate cancelled.")
|
||||
return
|
||||
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
||||
caller.msg("{} objects were updated.".format(n_updated))
|
||||
|
||||
# A direct creation of an object from a given prototype
|
||||
|
||||
prototype = _parse_prototype(
|
||||
self.args, expect=dict if self.args.strip().startswith("{") else basestring)
|
||||
if not prototype:
|
||||
# this will only let through dicts or strings
|
||||
return
|
||||
|
||||
key = '<unnamed>'
|
||||
if isinstance(prototype, basestring):
|
||||
# A prototype key we are looking to apply
|
||||
key = prototype
|
||||
prototypes = protlib.search_prototype(prototype)
|
||||
nprots = len(prototypes)
|
||||
if not prototypes:
|
||||
caller.msg("No prototype named '%s'." % prototype)
|
||||
return
|
||||
elif nprots > 1:
|
||||
caller.msg("Found {} prototypes matching '{}':\n {}".format(
|
||||
nprots, prototype, ", ".join(prot.get('prototype_key', '')
|
||||
for proto in prototypes)))
|
||||
return
|
||||
# we have a prototype, check access
|
||||
prototype = prototypes[0]
|
||||
if not caller.locks.check_lockstring(caller, prototype.get('prototype_locks', ''), access_type='spawn'):
|
||||
caller.msg("You don't have access to use this prototype.")
|
||||
return
|
||||
|
||||
if "noloc" not in self.switches and "location" not in prototype:
|
||||
prototype["location"] = self.caller.location
|
||||
|
||||
for obj in spawn(prototype):
|
||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||
# proceed to spawning
|
||||
try:
|
||||
for obj in spawner.spawn(prototype):
|
||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||
except RuntimeError as err:
|
||||
caller.msg(err)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ command method rather than caller.msg().
|
|||
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.commands.default import help, comms, admin, system
|
||||
from evennia.commands.default import building, account
|
||||
from evennia.commands.default import building, account, general
|
||||
|
||||
|
||||
class AccountCmdSet(CmdSet):
|
||||
|
|
@ -39,6 +39,9 @@ class AccountCmdSet(CmdSet):
|
|||
self.add(account.CmdColorTest())
|
||||
self.add(account.CmdQuell())
|
||||
|
||||
# nicks
|
||||
self.add(general.CmdNick())
|
||||
|
||||
# testing
|
||||
self.add(building.CmdExamine())
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ make sure to homogenize self.caller to always be the account object
|
|||
for easy handling.
|
||||
|
||||
"""
|
||||
import hashlib
|
||||
import time
|
||||
from past.builtins import cmp
|
||||
from django.conf import settings
|
||||
from evennia.comms.models import ChannelDB, Msg
|
||||
|
|
@ -377,7 +379,7 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
|
|||
Usage:
|
||||
@cboot[/quiet] <channel> = <account> [:reason]
|
||||
|
||||
Switches:
|
||||
Switch:
|
||||
quiet - don't notify the channel
|
||||
|
||||
Kicks an account or object from a channel you control.
|
||||
|
|
@ -385,6 +387,7 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
|
||||
key = "@cboot"
|
||||
switch_options = ("quiet",)
|
||||
locks = "cmd: not pperm(channel_banned)"
|
||||
help_category = "Comms"
|
||||
|
||||
|
|
@ -453,6 +456,7 @@ class CmdCemit(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@cemit"
|
||||
aliases = ["@cmsg"]
|
||||
switch_options = ("sendername", "quiet")
|
||||
locks = "cmd: not pperm(channel_banned) and pperm(Player)"
|
||||
help_category = "Comms"
|
||||
|
||||
|
|
@ -683,6 +687,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "page"
|
||||
aliases = ['tell']
|
||||
switch_options = ("last", "list")
|
||||
locks = "cmd:not pperm(page_banned)"
|
||||
help_category = "Comms"
|
||||
|
||||
|
|
@ -850,6 +855,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
|
||||
key = "@irc2chan"
|
||||
switch_options = ("delete", "remove", "disconnect", "list", "ssl")
|
||||
locks = "cmd:serversetting(IRC_ENABLED) and pperm(Developer)"
|
||||
help_category = "Comms"
|
||||
|
||||
|
|
@ -914,8 +920,9 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
|||
self.msg("Account '%s' already exists and is not a bot." % botname)
|
||||
return
|
||||
else:
|
||||
password = hashlib.md5(str(time.time())).hexdigest()[:11]
|
||||
try:
|
||||
bot = create.create_account(botname, None, None, typeclass=botclass)
|
||||
bot = create.create_account(botname, None, password, typeclass=botclass)
|
||||
except Exception as err:
|
||||
self.msg("|rError, could not create the bot:|n '%s'." % err)
|
||||
return
|
||||
|
|
@ -1016,6 +1023,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
|
||||
key = "@rss2chan"
|
||||
switch_options = ("disconnect", "remove", "list")
|
||||
locks = "cmd:serversetting(RSS_ENABLED) and pperm(Developer)"
|
||||
help_category = "Comms"
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
|
|||
target = caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
self.msg(caller.at_look(target))
|
||||
self.msg((caller.at_look(target), {'type': 'look'}), options=None)
|
||||
|
||||
|
||||
class CmdNick(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -88,8 +88,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
Switches:
|
||||
inputline - replace on the inputline (default)
|
||||
object - replace on object-lookup
|
||||
account - replace on account-lookup
|
||||
|
||||
account - replace on account-lookup
|
||||
list - show all defined aliases (also "nicks" works)
|
||||
delete - remove nick by index in /list
|
||||
clearall - clear all nicks
|
||||
|
|
@ -118,7 +117,8 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
"""
|
||||
key = "nick"
|
||||
aliases = ["nickname", "nicks", "alias"]
|
||||
switch_options = ("inputline", "object", "account", "list", "delete", "clearall")
|
||||
aliases = ["nickname", "nicks"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def parse(self):
|
||||
|
|
@ -143,7 +143,6 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
return re.sub(r"(\$[0-9]+|\*|\?|\[.+?\])", r"|Y\1|n", string)
|
||||
|
||||
caller = self.caller
|
||||
account = self.caller.account or caller
|
||||
switches = self.switches
|
||||
nicktypes = [switch for switch in switches if switch in ("object", "account", "inputline")]
|
||||
specified_nicktype = bool(nicktypes)
|
||||
|
|
@ -151,7 +150,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
nicklist = (utils.make_iter(caller.nicks.get(category="inputline", return_obj=True) or []) +
|
||||
utils.make_iter(caller.nicks.get(category="object", return_obj=True) or []) +
|
||||
utils.make_iter(account.nicks.get(category="account", return_obj=True) or []))
|
||||
utils.make_iter(caller.nicks.get(category="account", return_obj=True) or []))
|
||||
|
||||
if 'list' in switches or self.cmdstring in ("nicks", "@nicks"):
|
||||
|
||||
|
|
@ -174,29 +173,77 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
if 'delete' in switches or 'del' in switches:
|
||||
if not self.args or not self.lhs:
|
||||
caller.msg("usage nick/delete #num ('nicks' for list)")
|
||||
caller.msg("usage nick/delete <nick> or <#num> ('nicks' for list)")
|
||||
return
|
||||
# see if a number was given
|
||||
arg = self.args.lstrip("#")
|
||||
oldnicks = []
|
||||
if arg.isdigit():
|
||||
# we are given a index in nicklist
|
||||
delindex = int(arg)
|
||||
if 0 < delindex <= len(nicklist):
|
||||
oldnick = nicklist[delindex - 1]
|
||||
_, _, old_nickstring, old_replstring = oldnick.value
|
||||
oldnicks.append(nicklist[delindex - 1])
|
||||
else:
|
||||
caller.msg("Not a valid nick index. See 'nicks' for a list.")
|
||||
return
|
||||
nicktype = oldnick.category
|
||||
nicktypestr = "%s-nick" % nicktype.capitalize()
|
||||
else:
|
||||
if not specified_nicktype:
|
||||
nicktypes = ("object", "account", "inputline")
|
||||
for nicktype in nicktypes:
|
||||
oldnicks.append(caller.nicks.get(arg, category=nicktype, return_obj=True))
|
||||
|
||||
if nicktype == "account":
|
||||
account.nicks.remove(old_nickstring, category=nicktype)
|
||||
else:
|
||||
oldnicks = [oldnick for oldnick in oldnicks if oldnick]
|
||||
if oldnicks:
|
||||
for oldnick in oldnicks:
|
||||
nicktype = oldnick.category
|
||||
nicktypestr = "%s-nick" % nicktype.capitalize()
|
||||
_, _, old_nickstring, old_replstring = oldnick.value
|
||||
caller.nicks.remove(old_nickstring, category=nicktype)
|
||||
caller.msg("%s removed: '|w%s|n' -> |w%s|n." % (
|
||||
nicktypestr, old_nickstring, old_replstring))
|
||||
return
|
||||
caller.msg("%s removed: '|w%s|n' -> |w%s|n." % (
|
||||
nicktypestr, old_nickstring, old_replstring))
|
||||
else:
|
||||
caller.msg("No matching nicks to remove.")
|
||||
return
|
||||
|
||||
if not self.rhs and self.lhs:
|
||||
# check what a nick is set to
|
||||
strings = []
|
||||
if not specified_nicktype:
|
||||
nicktypes = ("object", "account", "inputline")
|
||||
for nicktype in nicktypes:
|
||||
nicks = utils.make_iter(caller.nicks.get(category=nicktype, return_obj=True))
|
||||
for nick in nicks:
|
||||
_, _, nick, repl = nick.value
|
||||
if nick.startswith(self.lhs):
|
||||
strings.append("{}-nick: '{}' -> '{}'".format(
|
||||
nicktype.capitalize(), nick, repl))
|
||||
if strings:
|
||||
caller.msg("\n".join(strings))
|
||||
else:
|
||||
caller.msg("No nicks found matching '{}'".format(self.lhs))
|
||||
return
|
||||
|
||||
if not self.rhs and self.lhs:
|
||||
# check what a nick is set to
|
||||
strings = []
|
||||
if not specified_nicktype:
|
||||
nicktypes = ("object", "account", "inputline")
|
||||
for nicktype in nicktypes:
|
||||
if nicktype == "account":
|
||||
obj = account
|
||||
else:
|
||||
obj = caller
|
||||
nicks = utils.make_iter(obj.nicks.get(category=nicktype, return_obj=True))
|
||||
for nick in nicks:
|
||||
_, _, nick, repl = nick.value
|
||||
if nick.startswith(self.lhs):
|
||||
strings.append("{}-nick: '{}' -> '{}'".format(
|
||||
nicktype.capitalize(), nick, repl))
|
||||
if strings:
|
||||
caller.msg("\n".join(strings))
|
||||
else:
|
||||
caller.msg("No nicks found matching '{}'".format(self.lhs))
|
||||
return
|
||||
|
||||
if not self.rhs and self.lhs:
|
||||
# check what a nick is set to
|
||||
|
|
@ -237,16 +284,11 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
errstring = ""
|
||||
string = ""
|
||||
for nicktype in nicktypes:
|
||||
if nicktype == "account":
|
||||
obj = account
|
||||
else:
|
||||
obj = caller
|
||||
|
||||
nicktypestr = "%s-nick" % nicktype.capitalize()
|
||||
old_nickstring = None
|
||||
old_replstring = None
|
||||
|
||||
oldnick = obj.nicks.get(key=nickstring, category=nicktype, return_obj=True)
|
||||
oldnick = caller.nicks.get(key=nickstring, category=nicktype, return_obj=True)
|
||||
if oldnick:
|
||||
_, _, old_nickstring, old_replstring = oldnick.value
|
||||
if replstring:
|
||||
|
|
@ -261,7 +303,7 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
|||
else:
|
||||
string += "\n%s '|w%s|n' mapped to '|w%s|n'." % (nicktypestr, nickstring, replstring)
|
||||
try:
|
||||
obj.nicks.add(nickstring, replstring, category=nicktype)
|
||||
caller.nicks.add(nickstring, replstring, category=nicktype)
|
||||
except NickTemplateInvalid:
|
||||
caller.msg("You must use the same $-markers both in the nick and in the replacement.")
|
||||
return
|
||||
|
|
@ -337,13 +379,17 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
|
|||
caller.msg("You can't get that.")
|
||||
return
|
||||
|
||||
# calling at_before_get hook method
|
||||
if not obj.at_before_get(caller):
|
||||
return
|
||||
|
||||
obj.move_to(caller, quiet=True)
|
||||
caller.msg("You pick up %s." % obj.name)
|
||||
caller.location.msg_contents("%s picks up %s." %
|
||||
(caller.name,
|
||||
obj.name),
|
||||
exclude=caller)
|
||||
# calling hook method
|
||||
# calling at_get hook method
|
||||
obj.at_get(caller)
|
||||
|
||||
|
||||
|
|
@ -378,6 +424,10 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
|
|||
if not obj:
|
||||
return
|
||||
|
||||
# Call the object script's at_before_drop() method.
|
||||
if not obj.at_before_drop(caller):
|
||||
return
|
||||
|
||||
obj.move_to(caller.location, quiet=True)
|
||||
caller.msg("You drop %s." % (obj.name,))
|
||||
caller.location.msg_contents("%s drops %s." %
|
||||
|
|
@ -392,12 +442,13 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
|||
give away something to someone
|
||||
|
||||
Usage:
|
||||
give <inventory obj> = <target>
|
||||
give <inventory obj> <to||=> <target>
|
||||
|
||||
Gives an items from your inventory to another character,
|
||||
placing it in their inventory.
|
||||
"""
|
||||
key = "give"
|
||||
rhs_split = ("=", " to ") # Prefer = delimiter, but allow " to " usage.
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
|
|
@ -420,6 +471,11 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
|||
if not to_give.location == caller:
|
||||
caller.msg("You are not holding %s." % to_give.key)
|
||||
return
|
||||
|
||||
# calling at_before_give hook method
|
||||
if not to_give.at_before_give(caller, target):
|
||||
return
|
||||
|
||||
# give object
|
||||
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
||||
to_give.move_to(target, quiet=True)
|
||||
|
|
|
|||
|
|
@ -317,6 +317,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
"""
|
||||
key = "@sethelp"
|
||||
switch_options = ("edit", "replace", "append", "extend", "delete")
|
||||
locks = "cmd:perm(Helper)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
|
|||
|
|
@ -79,6 +79,13 @@ class MuxCommand(Command):
|
|||
it here). The rest of the command is stored in self.args, which can
|
||||
start with the switch indicator /.
|
||||
|
||||
Optional variables to aid in parsing, if set:
|
||||
self.switch_options - (tuple of valid /switches expected by this
|
||||
command (without the /))
|
||||
self.rhs_split - Alternate string delimiter or tuple of strings
|
||||
to separate left/right hand sides. tuple form
|
||||
gives priority split to first string delimiter.
|
||||
|
||||
This parser breaks self.args into its constituents and stores them in the
|
||||
following variables:
|
||||
self.switches = [list of /switches (without the /)]
|
||||
|
|
@ -97,9 +104,18 @@ class MuxCommand(Command):
|
|||
"""
|
||||
raw = self.args
|
||||
args = raw.strip()
|
||||
# Without explicitly setting these attributes, they assume default values:
|
||||
if not hasattr(self, "switch_options"):
|
||||
self.switch_options = None
|
||||
if not hasattr(self, "rhs_split"):
|
||||
self.rhs_split = "="
|
||||
if not hasattr(self, "account_caller"):
|
||||
self.account_caller = False
|
||||
|
||||
# split out switches
|
||||
switches = []
|
||||
switches, delimiters = [], self.rhs_split
|
||||
if self.switch_options:
|
||||
self.switch_options = [opt.lower() for opt in self.switch_options]
|
||||
if args and len(args) > 1 and raw[0] == "/":
|
||||
# we have a switch, or a set of switches. These end with a space.
|
||||
switches = args[1:].split(None, 1)
|
||||
|
|
@ -109,16 +125,50 @@ class MuxCommand(Command):
|
|||
else:
|
||||
args = ""
|
||||
switches = switches[0].split('/')
|
||||
# If user-provides switches, parse them with parser switch options.
|
||||
if switches and self.switch_options:
|
||||
valid_switches, unused_switches, extra_switches = [], [], []
|
||||
for element in switches:
|
||||
option_check = [opt for opt in self.switch_options if opt == element]
|
||||
if not option_check:
|
||||
option_check = [opt for opt in self.switch_options if opt.startswith(element)]
|
||||
match_count = len(option_check)
|
||||
if match_count > 1:
|
||||
extra_switches.extend(option_check) # Either the option provided is ambiguous,
|
||||
elif match_count == 1:
|
||||
valid_switches.extend(option_check) # or it is a valid option abbreviation,
|
||||
elif match_count == 0:
|
||||
unused_switches.append(element) # or an extraneous option to be ignored.
|
||||
if extra_switches: # User provided switches
|
||||
self.msg('|g%s|n: |wAmbiguous switch supplied: Did you mean /|C%s|w?' %
|
||||
(self.cmdstring, ' |nor /|C'.join(extra_switches)))
|
||||
if unused_switches:
|
||||
plural = '' if len(unused_switches) == 1 else 'es'
|
||||
self.msg('|g%s|n: |wExtra switch%s "/|C%s|w" ignored.' %
|
||||
(self.cmdstring, plural, '|n, /|C'.join(unused_switches)))
|
||||
switches = valid_switches # Only include valid_switches in command function call
|
||||
arglist = [arg.strip() for arg in args.split()]
|
||||
|
||||
# check for arg1, arg2, ... = argA, argB, ... constructs
|
||||
lhs, rhs = args, None
|
||||
lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
|
||||
if args and '=' in args:
|
||||
lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
|
||||
lhslist = [arg.strip() for arg in lhs.split(',')]
|
||||
rhslist = [arg.strip() for arg in rhs.split(',')]
|
||||
|
||||
lhs, rhs = args.strip(), None
|
||||
if lhs:
|
||||
if delimiters and hasattr(delimiters, '__iter__'): # If delimiter is iterable,
|
||||
best_split = delimiters[0] # (default to first delimiter)
|
||||
for this_split in delimiters: # try each delimiter
|
||||
if this_split in lhs: # to find first successful split
|
||||
best_split = this_split # to be the best split.
|
||||
break
|
||||
else:
|
||||
best_split = delimiters
|
||||
# Parse to separate left into left/right sides using best_split delimiter string
|
||||
if best_split in lhs:
|
||||
lhs, rhs = lhs.split(best_split, 1)
|
||||
# Trim user-injected whitespace
|
||||
rhs = rhs.strip() if rhs is not None else None
|
||||
lhs = lhs.strip()
|
||||
# Further split left/right sides by comma delimiter
|
||||
lhslist = [arg.strip() for arg in lhs.split(',')] if lhs is not None else ""
|
||||
rhslist = [arg.strip() for arg in rhs.split(',')] if rhs is not None else ""
|
||||
# save to object properties:
|
||||
self.raw = raw
|
||||
self.switches = switches
|
||||
|
|
@ -133,7 +183,7 @@ class MuxCommand(Command):
|
|||
# sure that self.caller is always the account if possible. We also create
|
||||
# a special property "character" for the puppeted object, if any. This
|
||||
# is convenient for commands defined on the Account only.
|
||||
if hasattr(self, "account_caller") and self.account_caller:
|
||||
if self.account_caller:
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
||||
# caller is an Object/Character
|
||||
self.character = self.caller
|
||||
|
|
@ -169,6 +219,8 @@ class MuxCommand(Command):
|
|||
string += "\nraw argument (self.raw): |w%s|n \n" % self.raw
|
||||
string += "cmd args (self.args): |w%s|n\n" % self.args
|
||||
string += "cmd switches (self.switches): |w%s|n\n" % self.switches
|
||||
string += "cmd options (self.switch_options): |w%s|n\n" % self.switch_options
|
||||
string += "cmd parse left/right using (self.rhs_split): |w%s|n\n" % self.rhs_split
|
||||
string += "space-separated arg list (self.arglist): |w%s|n\n" % self.arglist
|
||||
string += "lhs, left-hand side of '=' (self.lhs): |w%s|n\n" % self.lhs
|
||||
string += "lhs, comma separated (self.lhslist): |w%s|n\n" % self.lhslist
|
||||
|
|
@ -193,18 +245,4 @@ class MuxAccountCommand(MuxCommand):
|
|||
character is actually attached to this Account and Session.
|
||||
"""
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
We run the parent parser as usual, then fix the result
|
||||
"""
|
||||
super(MuxAccountCommand, self).parse()
|
||||
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
||||
# caller is an Object/Character
|
||||
self.character = self.caller
|
||||
self.caller = self.caller.account
|
||||
elif utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount"):
|
||||
# caller was already an Account
|
||||
self.character = self.caller.get_puppet(self.session)
|
||||
else:
|
||||
self.character = None
|
||||
account_caller = True # Using MuxAccountCommand explicitly defaults the caller to an account
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class CmdReload(COMMAND_DEFAULT_CLASS):
|
|||
if self.args:
|
||||
reason = "(Reason: %s) " % self.args.rstrip(".")
|
||||
SESSIONS.announce_all(" Server restart initiated %s..." % reason)
|
||||
SESSIONS.server.shutdown(mode='reload')
|
||||
SESSIONS.portal_restart_server()
|
||||
|
||||
|
||||
class CmdReset(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -91,7 +91,7 @@ class CmdReset(COMMAND_DEFAULT_CLASS):
|
|||
Reload the system.
|
||||
"""
|
||||
SESSIONS.announce_all(" Server resetting/restarting ...")
|
||||
SESSIONS.server.shutdown(mode='reset')
|
||||
SESSIONS.portal_reset_server()
|
||||
|
||||
|
||||
class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -119,7 +119,6 @@ class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
|||
announcement += "%s\n" % self.args
|
||||
logger.log_info('Server shutdown by %s.' % self.caller.name)
|
||||
SESSIONS.announce_all(announcement)
|
||||
SESSIONS.server.shutdown(mode='shutdown')
|
||||
SESSIONS.portal_shutdown()
|
||||
|
||||
|
||||
|
|
@ -246,6 +245,7 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@py"
|
||||
aliases = ["!"]
|
||||
switch_options = ("time", "edit")
|
||||
locks = "cmd:perm(py) or perm(Developer)"
|
||||
help_category = "System"
|
||||
|
||||
|
|
@ -329,6 +329,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@scripts"
|
||||
aliases = ["@globalscript", "@listscripts"]
|
||||
switch_options = ("start", "stop", "kill", "validate")
|
||||
locks = "cmd:perm(listscripts) or perm(Admin)"
|
||||
help_category = "System"
|
||||
|
||||
|
|
@ -522,6 +523,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@service"
|
||||
aliases = ["@services"]
|
||||
switch_options = ("list", "start", "stop", "delete")
|
||||
locks = "cmd:perm(service) or perm(Developer)"
|
||||
help_category = "System"
|
||||
|
||||
|
|
@ -673,7 +675,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
|||
Usage:
|
||||
@server[/mem]
|
||||
|
||||
Switch:
|
||||
Switches:
|
||||
mem - return only a string of the current memory usage
|
||||
flushmem - flush the idmapper cache
|
||||
|
||||
|
|
@ -704,6 +706,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
|||
"""
|
||||
key = "@server"
|
||||
aliases = ["@serverload", "@serverprocess"]
|
||||
switch_options = ("mem", "flushmem")
|
||||
locks = "cmd:perm(list) or perm(Developer)"
|
||||
help_category = "System"
|
||||
|
||||
|
|
|
|||
|
|
@ -22,11 +22,13 @@ from mock import Mock, mock
|
|||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.commands.command import Command, InterruptCommand
|
||||
from evennia.utils import ansi, utils, gametime
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
from evennia import search_object
|
||||
from evennia import DefaultObject, DefaultCharacter
|
||||
from evennia.prototypes import prototypes as protlib
|
||||
|
||||
|
||||
# set up signal here since we are not starting the server
|
||||
|
|
@ -44,7 +46,7 @@ class CommandTest(EvenniaTest):
|
|||
Tests a command
|
||||
"""
|
||||
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None,
|
||||
receiver=None, cmdstring=None, obj=None):
|
||||
receiver=None, cmdstring=None, obj=None, inputs=None):
|
||||
"""
|
||||
Test a command by assigning all the needed
|
||||
properties to cmdobj and running
|
||||
|
|
@ -73,42 +75,58 @@ class CommandTest(EvenniaTest):
|
|||
cmdobj.obj = obj or (caller if caller else self.char1)
|
||||
# test
|
||||
old_msg = receiver.msg
|
||||
returned_msg = ""
|
||||
inputs = inputs or []
|
||||
|
||||
try:
|
||||
receiver.msg = Mock()
|
||||
if cmdobj.at_pre_cmd():
|
||||
return
|
||||
cmdobj.parse()
|
||||
ret = cmdobj.func()
|
||||
|
||||
# handle func's with yield in them (generators)
|
||||
if isinstance(ret, types.GeneratorType):
|
||||
ret.next()
|
||||
while True:
|
||||
try:
|
||||
inp = inputs.pop() if inputs else None
|
||||
if inp:
|
||||
try:
|
||||
ret.send(inp)
|
||||
except TypeError:
|
||||
ret.next()
|
||||
ret = ret.send(inp)
|
||||
else:
|
||||
ret.next()
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
cmdobj.at_post_cmd()
|
||||
except StopIteration:
|
||||
pass
|
||||
except InterruptCommand:
|
||||
pass
|
||||
finally:
|
||||
# clean out evtable sugar. We only operate on text-type
|
||||
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs, force_string=True))
|
||||
for name, args, kwargs in receiver.msg.mock_calls]
|
||||
# Get the first element of a tuple if msg received a tuple instead of a string
|
||||
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
|
||||
if msg is not None:
|
||||
# set our separator for returned messages based on parsing ansi or not
|
||||
msg_sep = "|" if noansi else "||"
|
||||
# Have to strip ansi for each returned message for the regex to handle it correctly
|
||||
returned_msg = msg_sep.join(_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi))
|
||||
for mess in stored_msg).strip()
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
||||
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
||||
sep3 = "\n" + "=" * 78
|
||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||
raise AssertionError(retval)
|
||||
else:
|
||||
returned_msg = "\n".join(str(msg) for msg in stored_msg)
|
||||
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
||||
receiver.msg = old_msg
|
||||
|
||||
# clean out evtable sugar. We only operate on text-type
|
||||
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs, force_string=True))
|
||||
for name, args, kwargs in receiver.msg.mock_calls]
|
||||
# Get the first element of a tuple if msg received a tuple instead of a string
|
||||
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
|
||||
if msg is not None:
|
||||
# set our separator for returned messages based on parsing ansi or not
|
||||
msg_sep = "|" if noansi else "||"
|
||||
# Have to strip ansi for each returned message for the regex to handle it correctly
|
||||
returned_msg = msg_sep.join(_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi))
|
||||
for mess in stored_msg).strip()
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
||||
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
||||
sep3 = "\n" + "=" * 78
|
||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||
raise AssertionError(retval)
|
||||
else:
|
||||
returned_msg = "\n".join(str(msg) for msg in stored_msg)
|
||||
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
||||
receiver.msg = old_msg
|
||||
|
||||
return returned_msg
|
||||
|
||||
|
|
@ -131,18 +149,48 @@ class TestGeneral(CommandTest):
|
|||
self.call(general.CmdPose(), "looks around", "Char looks around")
|
||||
|
||||
def test_nick(self):
|
||||
self.call(general.CmdNick(), "testalias = testaliasedstring1", "Inputline-nick 'testalias' mapped to 'testaliasedstring1'.")
|
||||
self.call(general.CmdNick(), "/account testalias = testaliasedstring2", "Account-nick 'testalias' mapped to 'testaliasedstring2'.")
|
||||
self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Object-nick 'testalias' mapped to 'testaliasedstring3'.")
|
||||
self.call(general.CmdNick(), "testalias = testaliasedstring1",
|
||||
"Inputline-nick 'testalias' mapped to 'testaliasedstring1'.")
|
||||
self.call(general.CmdNick(), "/account testalias = testaliasedstring2",
|
||||
"Account-nick 'testalias' mapped to 'testaliasedstring2'.")
|
||||
self.call(general.CmdNick(), "/object testalias = testaliasedstring3",
|
||||
"Object-nick 'testalias' mapped to 'testaliasedstring3'.")
|
||||
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
|
||||
self.assertEqual(None, self.char1.nicks.get("testalias", category="account"))
|
||||
self.assertEqual(u"testaliasedstring2", self.char1.account.nicks.get("testalias", category="account"))
|
||||
self.assertEqual(u"testaliasedstring2", self.char1.nicks.get("testalias", category="account"))
|
||||
self.assertEqual(None, self.char1.account.nicks.get("testalias", category="account"))
|
||||
self.assertEqual(u"testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
|
||||
|
||||
def test_get_and_drop(self):
|
||||
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
|
||||
self.call(general.CmdDrop(), "Obj", "You drop Obj.")
|
||||
|
||||
def test_give(self):
|
||||
self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.")
|
||||
self.call(general.CmdGive(), "Obj = Char2", "You aren't carrying Obj.")
|
||||
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
|
||||
self.call(general.CmdGive(), "Obj to Char2", "You give")
|
||||
self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2)
|
||||
|
||||
def test_mux_command(self):
|
||||
|
||||
class CmdTest(MuxCommand):
|
||||
key = 'test'
|
||||
switch_options = ('test', 'testswitch', 'testswitch2')
|
||||
|
||||
def func(self):
|
||||
self.msg("Switches matched: {}".format(self.switches))
|
||||
|
||||
self.call(CmdTest(), "/test/testswitch/testswitch2", "Switches matched: ['test', 'testswitch', 'testswitch2']")
|
||||
self.call(CmdTest(), "/test", "Switches matched: ['test']")
|
||||
self.call(CmdTest(), "/test/testswitch", "Switches matched: ['test', 'testswitch']")
|
||||
self.call(CmdTest(), "/testswitch/testswitch2", "Switches matched: ['testswitch', 'testswitch2']")
|
||||
self.call(CmdTest(), "/testswitch", "Switches matched: ['testswitch']")
|
||||
self.call(CmdTest(), "/testswitch2", "Switches matched: ['testswitch2']")
|
||||
self.call(CmdTest(), "/t", "test: Ambiguous switch supplied: "
|
||||
"Did you mean /test or /testswitch or /testswitch2?|Switches matched: []")
|
||||
self.call(CmdTest(), "/tests", "test: Ambiguous switch supplied: "
|
||||
"Did you mean /testswitch or /testswitch2?|Switches matched: []")
|
||||
|
||||
def test_say(self):
|
||||
self.call(general.CmdSay(), "Testing", "You say, \"Testing\"")
|
||||
|
||||
|
|
@ -230,7 +278,8 @@ class TestAccount(CommandTest):
|
|||
self.call(account.CmdColorTest(), "ansi", "ANSI colors:", caller=self.account)
|
||||
|
||||
def test_char_create(self):
|
||||
self.call(account.CmdCharCreate(), "Test1=Test char", "Created new character Test1. Use @ic Test1 to enter the game", caller=self.account)
|
||||
self.call(account.CmdCharCreate(), "Test1=Test char",
|
||||
"Created new character Test1. Use @ic Test1 to enter the game", caller=self.account)
|
||||
|
||||
def test_quell(self):
|
||||
self.call(account.CmdQuell(), "", "Quelling to current puppet's permissions (developer).", caller=self.account)
|
||||
|
|
@ -239,16 +288,19 @@ class TestAccount(CommandTest):
|
|||
class TestBuilding(CommandTest):
|
||||
def test_create(self):
|
||||
name = settings.BASE_OBJECT_TYPECLASS.rsplit('.', 1)[1]
|
||||
self.call(building.CmdCreate(), "/drop TestObj1", "You create a new %s: TestObj1." % name)
|
||||
self.call(building.CmdCreate(), "/d TestObj1", # /d switch is abbreviated form of /drop
|
||||
"You create a new %s: TestObj1." % name)
|
||||
|
||||
def test_examine(self):
|
||||
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
||||
|
||||
def test_set_obj_alias(self):
|
||||
self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj(#4)' set to testobj1b.")
|
||||
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj(#4)")
|
||||
self.call(building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj(#4)' set to 'testobj1b'.")
|
||||
|
||||
def test_copy(self):
|
||||
self.call(building.CmdCopy(), "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b", "Copied Obj to 'TestObj3' (aliases: ['TestObj3b']")
|
||||
self.call(building.CmdCopy(), "Obj = TestObj2;TestObj2b, TestObj3;TestObj3b",
|
||||
"Copied Obj to 'TestObj3' (aliases: ['TestObj3b']")
|
||||
|
||||
def test_attribute_commands(self):
|
||||
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
|
||||
|
|
@ -291,19 +343,36 @@ class TestBuilding(CommandTest):
|
|||
|
||||
def test_typeclass(self):
|
||||
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
|
||||
"Obj changed typeclass from evennia.objects.objects.DefaultObject to evennia.objects.objects.DefaultExit.")
|
||||
"Obj changed typeclass from evennia.objects.objects.DefaultObject "
|
||||
"to evennia.objects.objects.DefaultExit.")
|
||||
|
||||
def test_lock(self):
|
||||
self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.")
|
||||
|
||||
def test_find(self):
|
||||
self.call(building.CmdFind(), "Room2", "One Match")
|
||||
self.call(building.CmdFind(), "oom2", "One Match")
|
||||
expect = "One Match(#1-#7, loc):\n " +\
|
||||
"Char2(#7) - evennia.objects.objects.DefaultCharacter (location: Room(#1))"
|
||||
self.call(building.CmdFind(), "Char2", expect, cmdstring="locate")
|
||||
self.call(building.CmdFind(), "/ex Char2", # /ex is an ambiguous switch
|
||||
"locate: Ambiguous switch supplied: Did you mean /exit or /exact?|" + expect,
|
||||
cmdstring="locate")
|
||||
self.call(building.CmdFind(), "Char2", expect, cmdstring="@locate")
|
||||
self.call(building.CmdFind(), "/l Char2", expect, cmdstring="find") # /l switch is abbreviated form of /loc
|
||||
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find")
|
||||
self.call(building.CmdFind(), "/startswith Room2", "One Match")
|
||||
|
||||
def test_script(self):
|
||||
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
||||
|
||||
def test_teleport(self):
|
||||
self.call(building.CmdTeleport(), "Room2", "Room2(#2)\n|Teleported to Room2.")
|
||||
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.")
|
||||
self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone
|
||||
"Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a None-location.")
|
||||
self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc
|
||||
"Destination has no location.")
|
||||
self.call(building.CmdTeleport(), "/q me to Room2", # /q switch is abbreviated form of /quiet
|
||||
"Char is already at Room2.")
|
||||
|
||||
def test_spawn(self):
|
||||
def getObject(commandTest, objKeyStr):
|
||||
|
|
@ -311,6 +380,7 @@ class TestBuilding(CommandTest):
|
|||
# check that it exists in the process.
|
||||
query = search_object(objKeyStr)
|
||||
commandTest.assertIsNotNone(query)
|
||||
commandTest.assertTrue(bool(query))
|
||||
obj = query[0]
|
||||
commandTest.assertIsNotNone(obj)
|
||||
return obj
|
||||
|
|
@ -319,17 +389,20 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdSpawn(), " ", "Usage: @spawn")
|
||||
|
||||
# Tests "@spawn <prototype_dictionary>" without specifying location.
|
||||
self.call(building.CmdSpawn(), \
|
||||
"{'key':'goblin', 'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin")
|
||||
goblin = getObject(self, "goblin")
|
||||
|
||||
# Tests that the spawned object's type is a DefaultCharacter.
|
||||
self.assertIsInstance(goblin, DefaultCharacter)
|
||||
self.call(building.CmdSpawn(),
|
||||
"/save {'prototype_key': 'testprot', 'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
"Saved prototype: testprot", inputs=['y'])
|
||||
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
||||
self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char")
|
||||
# Tests that the spawned object's location is the same as the caharacter's location, since
|
||||
# we did not specify it.
|
||||
self.assertEqual(goblin.location, self.char1.location)
|
||||
goblin.delete()
|
||||
testchar = getObject(self, "Test Char")
|
||||
self.assertEqual(testchar.location, self.char1.location)
|
||||
testchar.delete()
|
||||
|
||||
# Test "@spawn <prototype_dictionary>" with a location other than the character's.
|
||||
spawnLoc = self.room2
|
||||
|
|
@ -338,39 +411,54 @@ class TestBuilding(CommandTest):
|
|||
# char1's default location in the future...
|
||||
spawnLoc = self.room1
|
||||
|
||||
self.call(building.CmdSpawn(), \
|
||||
"{'prototype':'GOBLIN', 'key':'goblin', 'location':'%s'}" \
|
||||
% spawnLoc.dbref, "Spawned goblin")
|
||||
self.call(building.CmdSpawn(),
|
||||
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
|
||||
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin")
|
||||
goblin = getObject(self, "goblin")
|
||||
# Tests that the spawned object's type is a DefaultCharacter.
|
||||
self.assertIsInstance(goblin, DefaultCharacter)
|
||||
self.assertEqual(goblin.location, spawnLoc)
|
||||
|
||||
goblin.delete()
|
||||
|
||||
# create prototype
|
||||
protlib.create_prototype(**{'key': 'Ball',
|
||||
'typeclass': 'evennia.objects.objects.DefaultCharacter',
|
||||
'prototype_key': 'testball'})
|
||||
|
||||
# Tests "@spawn <prototype_name>"
|
||||
self.call(building.CmdSpawn(), "'BALL'", "Spawned Ball")
|
||||
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
||||
|
||||
ball = getObject(self, "Ball")
|
||||
self.assertEqual(ball.location, self.char1.location)
|
||||
self.assertIsInstance(ball, DefaultObject)
|
||||
ball.delete()
|
||||
|
||||
# Tests "@spawn/noloc ..." without specifying a location.
|
||||
# Tests "@spawn/n ..." without specifying a location.
|
||||
# Location should be "None".
|
||||
self.call(building.CmdSpawn(), "/noloc 'BALL'", "Spawned Ball")
|
||||
self.call(building.CmdSpawn(), "/n 'BALL'", "Spawned Ball") # /n switch is abbreviated form of /noloc
|
||||
ball = getObject(self, "Ball")
|
||||
self.assertIsNone(ball.location)
|
||||
ball.delete()
|
||||
|
||||
self.call(building.CmdSpawn(),
|
||||
"/noloc {'prototype_parent':'TESTBALL', 'prototype_key': 'testball', 'location':'%s'}"
|
||||
% spawnLoc.dbref, "Error: Prototype testball tries to parent itself.")
|
||||
|
||||
# Tests "@spawn/noloc ...", but DO specify a location.
|
||||
# Location should be the specified location.
|
||||
self.call(building.CmdSpawn(), \
|
||||
"/noloc {'prototype':'BALL', 'location':'%s'}" \
|
||||
self.call(building.CmdSpawn(),
|
||||
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}"
|
||||
% spawnLoc.dbref, "Spawned Ball")
|
||||
ball = getObject(self, "Ball")
|
||||
self.assertEqual(ball.location, spawnLoc)
|
||||
ball.delete()
|
||||
|
||||
# test calling spawn with an invalid prototype.
|
||||
self.call(building.CmdSpawn(), \
|
||||
"'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
||||
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
||||
|
||||
# Test listing commands
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
||||
# @span/edit
|
||||
self.call(
|
||||
|
|
@ -388,45 +476,57 @@ class TestComms(CommandTest):
|
|||
|
||||
def setUp(self):
|
||||
super(CommandTest, self).setUp()
|
||||
self.call(comms.CmdChannelCreate(), "testchan;test=Test Channel", "Created channel testchan and connected to it.", receiver=self.account)
|
||||
self.call(comms.CmdChannelCreate(), "testchan;test=Test Channel",
|
||||
"Created channel testchan and connected to it.", receiver=self.account)
|
||||
|
||||
def test_toggle_com(self):
|
||||
self.call(comms.CmdAddCom(), "tc = testchan", "You are already connected to channel testchan. You can now", receiver=self.account)
|
||||
self.call(comms.CmdAddCom(), "tc = testchan",
|
||||
"You are already connected to channel testchan. You can now", receiver=self.account)
|
||||
self.call(comms.CmdDelCom(), "tc", "Your alias 'tc' for channel testchan was cleared.", receiver=self.account)
|
||||
|
||||
def test_channels(self):
|
||||
self.call(comms.CmdChannels(), "", "Available channels (use comlist,addcom and delcom to manage", receiver=self.account)
|
||||
self.call(comms.CmdChannels(), "",
|
||||
"Available channels (use comlist,addcom and delcom to manage", receiver=self.account)
|
||||
|
||||
def test_all_com(self):
|
||||
self.call(comms.CmdAllCom(), "", "Available channels (use comlist,addcom and delcom to manage", receiver=self.account)
|
||||
self.call(comms.CmdAllCom(), "",
|
||||
"Available channels (use comlist,addcom and delcom to manage", receiver=self.account)
|
||||
|
||||
def test_clock(self):
|
||||
self.call(comms.CmdClock(), "testchan=send:all()", "Lock(s) applied. Current locks on testchan:", receiver=self.account)
|
||||
self.call(comms.CmdClock(),
|
||||
"testchan=send:all()", "Lock(s) applied. Current locks on testchan:", receiver=self.account)
|
||||
|
||||
def test_cdesc(self):
|
||||
self.call(comms.CmdCdesc(), "testchan = Test Channel", "Description of channel 'testchan' set to 'Test Channel'.", receiver=self.account)
|
||||
self.call(comms.CmdCdesc(), "testchan = Test Channel",
|
||||
"Description of channel 'testchan' set to 'Test Channel'.", receiver=self.account)
|
||||
|
||||
def test_cemit(self):
|
||||
self.call(comms.CmdCemit(), "testchan = Test Message", "[testchan] Test Message|Sent to channel testchan: Test Message", receiver=self.account)
|
||||
self.call(comms.CmdCemit(), "testchan = Test Message",
|
||||
"[testchan] Test Message|Sent to channel testchan: Test Message", receiver=self.account)
|
||||
|
||||
def test_cwho(self):
|
||||
self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestAccount", receiver=self.account)
|
||||
|
||||
def test_page(self):
|
||||
self.call(comms.CmdPage(), "TestAccount2 = Test", "TestAccount2 is offline. They will see your message if they list their pages later.|You paged TestAccount2 with: 'Test'.", receiver=self.account)
|
||||
self.call(comms.CmdPage(), "TestAccount2 = Test",
|
||||
"TestAccount2 is offline. They will see your message if they list their pages later."
|
||||
"|You paged TestAccount2 with: 'Test'.", receiver=self.account)
|
||||
|
||||
def test_cboot(self):
|
||||
# No one else connected to boot
|
||||
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <account> [:reason]", receiver=self.account)
|
||||
|
||||
def test_cdestroy(self):
|
||||
self.call(comms.CmdCdestroy(), "testchan", "[testchan] TestAccount: testchan is being destroyed. Make sure to change your aliases.|Channel 'testchan' was destroyed.", receiver=self.account)
|
||||
self.call(comms.CmdCdestroy(), "testchan",
|
||||
"[testchan] TestAccount: testchan is being destroyed. Make sure to change your aliases."
|
||||
"|Channel 'testchan' was destroyed.", receiver=self.account)
|
||||
|
||||
|
||||
class TestBatchProcess(CommandTest):
|
||||
def test_batch_commands(self):
|
||||
# cannot test batchcode here, it must run inside the server process
|
||||
self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds", "Running Batch-command processor - Automatic mode for example_batch_cmds")
|
||||
self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds",
|
||||
"Running Batch-command processor - Automatic mode for example_batch_cmds")
|
||||
# we make sure to delete the button again here to stop the running reactor
|
||||
confirm = building.CmdDestroy.confirm
|
||||
building.CmdDestroy.confirm = False
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ Commands that are available from the connect screen.
|
|||
import re
|
||||
import time
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from random import getrandbits
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from evennia.accounts.models import AccountDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.server.throttle import Throttle
|
||||
from evennia.comms.models import ChannelDB
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
|
||||
|
|
@ -26,57 +26,10 @@ __all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
|||
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||
|
||||
# Helper function to throttle failed connection attempts.
|
||||
# This can easily be used to limit account creation too,
|
||||
# (just supply a different storage dictionary), but this
|
||||
# would also block dummyrunner, so it's not added as default.
|
||||
|
||||
_LATEST_FAILED_LOGINS = defaultdict(list)
|
||||
|
||||
|
||||
def _throttle(session, maxlim=None, timeout=None, storage=_LATEST_FAILED_LOGINS):
|
||||
"""
|
||||
This will check the session's address against the
|
||||
_LATEST_LOGINS dictionary to check they haven't
|
||||
spammed too many fails recently.
|
||||
|
||||
Args:
|
||||
session (Session): Session failing
|
||||
maxlim (int): max number of attempts to allow
|
||||
timeout (int): number of timeout seconds after
|
||||
max number of tries has been reached.
|
||||
|
||||
Returns:
|
||||
throttles (bool): True if throttling is active,
|
||||
False otherwise.
|
||||
|
||||
Notes:
|
||||
If maxlim and/or timeout are set, the function will
|
||||
just do the comparison, not append a new datapoint.
|
||||
|
||||
"""
|
||||
address = session.address
|
||||
if isinstance(address, tuple):
|
||||
address = address[0]
|
||||
now = time.time()
|
||||
if maxlim and timeout:
|
||||
# checking mode
|
||||
latest_fails = storage[address]
|
||||
if latest_fails and len(latest_fails) >= maxlim:
|
||||
# too many fails recently
|
||||
if now - latest_fails[-1] < timeout:
|
||||
# too soon - timeout in play
|
||||
return True
|
||||
else:
|
||||
# timeout has passed. Reset faillist
|
||||
storage[address] = []
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
else:
|
||||
# store the time of the latest fail
|
||||
storage[address].append(time.time())
|
||||
return False
|
||||
# Create throttles for too many connections, account-creations and login attempts
|
||||
CONNECTION_THROTTLE = Throttle(limit=5, timeout=1 * 60)
|
||||
CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60)
|
||||
LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60)
|
||||
|
||||
|
||||
def create_guest_account(session):
|
||||
|
|
@ -149,8 +102,11 @@ def create_normal_account(session, name, password):
|
|||
account (Account): the account which was created from the name and password.
|
||||
"""
|
||||
# check for too many login errors too quick.
|
||||
if _throttle(session, maxlim=5, timeout=5 * 60):
|
||||
# timeout is 5 minutes.
|
||||
address = session.address
|
||||
if isinstance(address, tuple):
|
||||
address = address[0]
|
||||
|
||||
if LOGIN_THROTTLE.check(address):
|
||||
session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n")
|
||||
return None
|
||||
|
||||
|
|
@ -161,7 +117,7 @@ def create_normal_account(session, name, password):
|
|||
# No accountname or password match
|
||||
session.msg("Incorrect login information given.")
|
||||
# this just updates the throttle
|
||||
_throttle(session)
|
||||
LOGIN_THROTTLE.update(address)
|
||||
# calls account hook for a failed login if possible.
|
||||
account = AccountDB.objects.get_account_from_name(name)
|
||||
if account:
|
||||
|
|
@ -171,7 +127,6 @@ def create_normal_account(session, name, password):
|
|||
# Check IP and/or name bans
|
||||
bans = ServerConfig.objects.conf("server_bans")
|
||||
if bans and (any(tup[0] == account.name.lower() for tup in bans) or
|
||||
|
||||
any(tup[2].match(session.address) for tup in bans if tup[2])):
|
||||
# this is a banned IP or name!
|
||||
string = "|rYou have been banned and cannot continue from here." \
|
||||
|
|
@ -211,7 +166,10 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
|||
session = self.caller
|
||||
|
||||
# check for too many login errors too quick.
|
||||
if _throttle(session, maxlim=5, timeout=5 * 60, storage=_LATEST_FAILED_LOGINS):
|
||||
address = session.address
|
||||
if isinstance(address, tuple):
|
||||
address = address[0]
|
||||
if CONNECTION_THROTTLE.check(address):
|
||||
# timeout is 5 minutes.
|
||||
session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n")
|
||||
return
|
||||
|
|
@ -234,6 +192,7 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
|||
session.msg("\n\r Usage (without <>): connect <name> <password>")
|
||||
return
|
||||
|
||||
CONNECTION_THROTTLE.update(address)
|
||||
name, password = parts
|
||||
account = create_normal_account(session, name, password)
|
||||
if account:
|
||||
|
|
@ -263,6 +222,15 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
|||
session = self.caller
|
||||
args = self.args.strip()
|
||||
|
||||
# Rate-limit account creation.
|
||||
address = session.address
|
||||
|
||||
if isinstance(address, tuple):
|
||||
address = address[0]
|
||||
if CREATION_THROTTLE.check(address):
|
||||
session.msg("|RYou are creating too many accounts. Try again in a few minutes.|n")
|
||||
return
|
||||
|
||||
# extract double quoted parts
|
||||
parts = [part.strip() for part in re.split(r"\"", args) if part.strip()]
|
||||
if len(parts) == 1:
|
||||
|
|
@ -294,10 +262,14 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
|||
string = "\n\r That name is reserved. Please choose another Accountname."
|
||||
session.msg(string)
|
||||
return
|
||||
if not re.findall(r"^[\w. @+\-']+$", password) or not (3 < len(password)):
|
||||
string = "\n\r Password should be longer than 3 characers. Letters, spaces, digits and @/./+/-/_/' only." \
|
||||
"\nFor best security, make it longer than 8 characters. You can also use a phrase of" \
|
||||
"\nmany words if you enclose the password in double quotes."
|
||||
|
||||
# Validate password
|
||||
Account = utils.class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||
# Have to create a dummy Account object to check username similarity
|
||||
valid, error = Account.validate_password(password, account=Account(username=accountname))
|
||||
if error:
|
||||
errors = [e for suberror in error.messages for e in error.messages]
|
||||
string = "\n".join(errors)
|
||||
session.msg(string)
|
||||
return
|
||||
|
||||
|
|
@ -322,6 +294,10 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
|||
if MULTISESSION_MODE < 2:
|
||||
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
|
||||
_create_character(session, new_account, typeclass, default_home, permissions)
|
||||
|
||||
# Update the throttle to indicate a new account was created from this IP
|
||||
CREATION_THROTTLE.update(address)
|
||||
|
||||
# tell the caller everything went well.
|
||||
string = "A new account '%s' was created. Welcome!"
|
||||
if " " in accountname:
|
||||
|
|
@ -577,7 +553,7 @@ def _create_character(session, new_account, typeclass, home, permissions):
|
|||
|
||||
# If no description is set, set a default description
|
||||
if not new_character.db.desc:
|
||||
new_character.db.desc = "This is an Account."
|
||||
new_character.db.desc = "This is a character."
|
||||
# We need to set this to have @ic auto-connect to this character
|
||||
new_account.db._last_puppet = new_character
|
||||
except Exception as e:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue