commit
8af527c6c0
80 changed files with 4779 additions and 2300 deletions
|
|
@ -1 +1 @@
|
||||||
0.7.0
|
0.8.0-dev
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from evennia.objects.models import ObjectDB
|
||||||
from evennia.comms.models import ChannelDB
|
from evennia.comms.models import ChannelDB
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils.utils import (lazy_property,
|
from evennia.utils.utils import (lazy_property, to_str,
|
||||||
make_iter, to_unicode, is_iter,
|
make_iter, to_unicode, is_iter,
|
||||||
variable_from_module)
|
variable_from_module)
|
||||||
from evennia.typeclasses.attributes import NickHandler
|
from evennia.typeclasses.attributes import NickHandler
|
||||||
|
|
@ -421,10 +421,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
|
|
||||||
kwargs["options"] = options
|
kwargs["options"] = options
|
||||||
|
|
||||||
|
if text is not None:
|
||||||
|
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||||
|
# sanitize text before sending across the wire
|
||||||
|
try:
|
||||||
|
text = to_str(text, force_string=True)
|
||||||
|
except Exception:
|
||||||
|
text = repr(text)
|
||||||
|
kwargs['text'] = text
|
||||||
|
|
||||||
# session relay
|
# session relay
|
||||||
sessions = make_iter(session) if session else self.sessions.all()
|
sessions = make_iter(session) if session else self.sessions.all()
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
session.data_out(text=text, **kwargs)
|
session.data_out(**kwargs)
|
||||||
|
|
||||||
def execute_cmd(self, raw_string, session=None, **kwargs):
|
def execute_cmd(self, raw_string, session=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -457,7 +466,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
callertype="account", session=session, **kwargs)
|
callertype="account", session=session, **kwargs)
|
||||||
|
|
||||||
def search(self, searchdata, return_puppet=False, search_object=False,
|
def search(self, searchdata, return_puppet=False, search_object=False,
|
||||||
typeclass=None, nofound_string=None, multimatch_string=None, **kwargs):
|
typeclass=None, nofound_string=None, multimatch_string=None, use_nicks=True, **kwargs):
|
||||||
"""
|
"""
|
||||||
This is similar to `DefaultObject.search` but defaults to searching
|
This is similar to `DefaultObject.search` but defaults to searching
|
||||||
for Accounts only.
|
for Accounts only.
|
||||||
|
|
@ -481,6 +490,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
multimatch_string (str, optional): A one-time error
|
multimatch_string (str, optional): A one-time error
|
||||||
message to echo if `searchdata` leads to multiple matches.
|
message to echo if `searchdata` leads to multiple matches.
|
||||||
If not given, will fall back to the default handler.
|
If not given, will fall back to the default handler.
|
||||||
|
use_nicks (bool, optional): Use account-level nick replacement.
|
||||||
|
|
||||||
Return:
|
Return:
|
||||||
match (Account, Object or None): A single Account or Object match.
|
match (Account, Object or None): A single Account or Object match.
|
||||||
|
|
@ -496,8 +506,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
if searchdata.lower() in ("me", "*me", "self", "*self",):
|
if searchdata.lower() in ("me", "*me", "self", "*self",):
|
||||||
return self
|
return self
|
||||||
if search_object:
|
if search_object:
|
||||||
matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass)
|
matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass, use_nicks=use_nicks)
|
||||||
else:
|
else:
|
||||||
|
searchdata = self.nicks.nickreplace(searchdata, categories=("account", ), include_account=False)
|
||||||
|
|
||||||
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
|
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
|
||||||
matches = _AT_SEARCH_RESULT(matches, self, query=searchdata,
|
matches = _AT_SEARCH_RESULT(matches, self, query=searchdata,
|
||||||
nofound_string=nofound_string,
|
nofound_string=nofound_string,
|
||||||
|
|
@ -616,7 +628,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
self.basetype_setup()
|
self.basetype_setup()
|
||||||
self.at_account_creation()
|
self.at_account_creation()
|
||||||
|
|
||||||
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
|
permissions = [settings.PERMISSION_ACCOUNT_DEFAULT]
|
||||||
if hasattr(self, "_createdict"):
|
if hasattr(self, "_createdict"):
|
||||||
# this will only be set if the utils.create_account
|
# this will only be set if the utils.create_account
|
||||||
# function was used to create the object.
|
# function was used to create the object.
|
||||||
|
|
|
||||||
|
|
@ -297,7 +297,7 @@ class Command(with_metaclass(CommandMeta, object)):
|
||||||
Args:
|
Args:
|
||||||
srcobj (Object): Object trying to gain permission
|
srcobj (Object): Object trying to gain permission
|
||||||
access_type (str, optional): The lock type to check.
|
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.
|
of matching `access_type` is found on this Command.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -455,7 +455,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
||||||
Usage:
|
Usage:
|
||||||
@option[/save] [name = value]
|
@option[/save] [name = value]
|
||||||
|
|
||||||
Switch:
|
Switches:
|
||||||
save - Save the current option settings for future logins.
|
save - Save the current option settings for future logins.
|
||||||
clear - Clear the saved options.
|
clear - Clear the saved options.
|
||||||
|
|
||||||
|
|
@ -467,6 +467,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@option"
|
key = "@option"
|
||||||
aliases = "@options"
|
aliases = "@options"
|
||||||
|
switch_options = ("save", "clear")
|
||||||
locks = "cmd:all()"
|
locks = "cmd:all()"
|
||||||
|
|
||||||
# this is used by the parent
|
# this is used by the parent
|
||||||
|
|
@ -549,6 +550,9 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
||||||
try:
|
try:
|
||||||
old_val = flags.get(new_name, False)
|
old_val = flags.get(new_name, False)
|
||||||
new_val = validator(new_val)
|
new_val = validator(new_val)
|
||||||
|
if old_val == new_val:
|
||||||
|
self.msg("Option |w%s|n was kept as '|w%s|n'." % (new_name, old_val))
|
||||||
|
else:
|
||||||
flags[new_name] = new_val
|
flags[new_name] = new_val
|
||||||
self.msg("Option |w%s|n was changed from '|w%s|n' to '|w%s|n'." % (new_name, old_val, new_val))
|
self.msg("Option |w%s|n was changed from '|w%s|n' to '|w%s|n'." % (new_name, old_val, new_val))
|
||||||
return {new_name: new_val}
|
return {new_name: new_val}
|
||||||
|
|
@ -572,7 +576,8 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
||||||
"TERM": utils.to_str,
|
"TERM": utils.to_str,
|
||||||
"UTF-8": validate_bool,
|
"UTF-8": validate_bool,
|
||||||
"XTERM256": validate_bool,
|
"XTERM256": validate_bool,
|
||||||
"INPUTDEBUG": validate_bool}
|
"INPUTDEBUG": validate_bool,
|
||||||
|
"FORCEDENDLINE": validate_bool}
|
||||||
|
|
||||||
name = self.lhs.upper()
|
name = self.lhs.upper()
|
||||||
val = self.rhs.strip()
|
val = self.rhs.strip()
|
||||||
|
|
@ -646,6 +651,7 @@ class CmdQuit(COMMAND_DEFAULT_CLASS):
|
||||||
game. Use the /all switch to disconnect from all sessions.
|
game. Use the /all switch to disconnect from all sessions.
|
||||||
"""
|
"""
|
||||||
key = "@quit"
|
key = "@quit"
|
||||||
|
switch_options = ("all",)
|
||||||
locks = "cmd:all()"
|
locks = "cmd:all()"
|
||||||
|
|
||||||
# this is used by the parent
|
# this is used by the parent
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@boot"
|
key = "@boot"
|
||||||
|
switch_options = ("quiet", "sid")
|
||||||
locks = "cmd:perm(boot) or perm(Admin)"
|
locks = "cmd:perm(boot) or perm(Admin)"
|
||||||
help_category = "Admin"
|
help_category = "Admin"
|
||||||
|
|
||||||
|
|
@ -265,6 +266,7 @@ class CmdDelAccount(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@delaccount"
|
key = "@delaccount"
|
||||||
|
switch_options = ("delobj",)
|
||||||
locks = "cmd:perm(delaccount) or perm(Developer)"
|
locks = "cmd:perm(delaccount) or perm(Developer)"
|
||||||
help_category = "Admin"
|
help_category = "Admin"
|
||||||
|
|
||||||
|
|
@ -329,9 +331,9 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
|
||||||
@pemit [<obj>, <obj>, ... =] <message>
|
@pemit [<obj>, <obj>, ... =] <message>
|
||||||
|
|
||||||
Switches:
|
Switches:
|
||||||
room : limit emits to rooms only (default)
|
room - limit emits to rooms only (default)
|
||||||
accounts : limit emits to accounts only
|
accounts - limit emits to accounts only
|
||||||
contents : send to the contents of matched objects too
|
contents - send to the contents of matched objects too
|
||||||
|
|
||||||
Emits a message to the selected objects or to
|
Emits a message to the selected objects or to
|
||||||
your immediate surroundings. If the object is a room,
|
your immediate surroundings. If the object is a room,
|
||||||
|
|
@ -341,6 +343,7 @@ class CmdEmit(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@emit"
|
key = "@emit"
|
||||||
aliases = ["@pemit", "@remit"]
|
aliases = ["@pemit", "@remit"]
|
||||||
|
switch_options = ("room", "accounts", "contents")
|
||||||
locks = "cmd:perm(emit) or perm(Builder)"
|
locks = "cmd:perm(emit) or perm(Builder)"
|
||||||
help_category = "Admin"
|
help_category = "Admin"
|
||||||
|
|
||||||
|
|
@ -442,14 +445,15 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
||||||
@perm[/switch] *<account> [= <permission>[,<permission>,...]]
|
@perm[/switch] *<account> [= <permission>[,<permission>,...]]
|
||||||
|
|
||||||
Switches:
|
Switches:
|
||||||
del : delete the given permission from <object> or <account>.
|
del - delete the given permission from <object> or <account>.
|
||||||
account : set permission on an account (same as adding * to name)
|
account - set permission on an account (same as adding * to name)
|
||||||
|
|
||||||
This command sets/clears individual permission strings on an object
|
This command sets/clears individual permission strings on an object
|
||||||
or account. If no permission is given, list all permissions on <object>.
|
or account. If no permission is given, list all permissions on <object>.
|
||||||
"""
|
"""
|
||||||
key = "@perm"
|
key = "@perm"
|
||||||
aliases = "@setperm"
|
aliases = "@setperm"
|
||||||
|
switch_options = ("del", "account")
|
||||||
locks = "cmd:perm(perm) or perm(Developer)"
|
locks = "cmd:perm(perm) or perm(Developer)"
|
||||||
help_category = "Admin"
|
help_category = "Admin"
|
||||||
|
|
||||||
|
|
@ -544,7 +548,8 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
|
||||||
Usage:
|
Usage:
|
||||||
@wall <message>
|
@wall <message>
|
||||||
|
|
||||||
Announces a message to all connected accounts.
|
Announces a message to all connected sessions
|
||||||
|
including all currently unlogged in.
|
||||||
"""
|
"""
|
||||||
key = "@wall"
|
key = "@wall"
|
||||||
locks = "cmd:perm(wall) or perm(Admin)"
|
locks = "cmd:perm(wall) or perm(Admin)"
|
||||||
|
|
@ -556,5 +561,5 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
|
||||||
self.caller.msg("Usage: @wall <message>")
|
self.caller.msg("Usage: @wall <message>")
|
||||||
return
|
return
|
||||||
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
|
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
|
||||||
self.msg("Announcing to all connected accounts ...")
|
self.msg("Announcing to all connected sessions ...")
|
||||||
SESSIONS.announce_all(message)
|
SESSIONS.announce_all(message)
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,7 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@batchcommands"
|
key = "@batchcommands"
|
||||||
aliases = ["@batchcommand", "@batchcmd"]
|
aliases = ["@batchcommand", "@batchcmd"]
|
||||||
|
switch_options = ("interactive",)
|
||||||
locks = "cmd:perm(batchcommands) or perm(Developer)"
|
locks = "cmd:perm(batchcommands) or perm(Developer)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -347,6 +348,7 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@batchcode"
|
key = "@batchcode"
|
||||||
aliases = ["@batchcodes"]
|
aliases = ["@batchcodes"]
|
||||||
|
switch_options = ("interactive", "debug")
|
||||||
locks = "cmd:superuser()"
|
locks = "cmd:superuser()"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,15 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
||||||
Usage:
|
Usage:
|
||||||
@alias <obj> [= [alias[,alias,alias,...]]]
|
@alias <obj> [= [alias[,alias,alias,...]]]
|
||||||
@alias <obj> =
|
@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
|
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
|
Observe that this is not the same thing as personal aliases
|
||||||
created with the 'nick' command! Aliases set with @alias are
|
created with the 'nick' command! Aliases set with @alias are
|
||||||
|
|
@ -118,6 +124,7 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@alias"
|
key = "@alias"
|
||||||
aliases = "@setobjalias"
|
aliases = "@setobjalias"
|
||||||
|
switch_options = ("category",)
|
||||||
locks = "cmd:perm(setobjalias) or perm(Builder)"
|
locks = "cmd:perm(setobjalias) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -138,9 +145,12 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
||||||
return
|
return
|
||||||
if self.rhs is None:
|
if self.rhs is None:
|
||||||
# no =, so we just list aliases on object.
|
# no =, so we just list aliases on object.
|
||||||
aliases = obj.aliases.all()
|
aliases = obj.aliases.all(return_key_and_category=True)
|
||||||
if aliases:
|
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:
|
else:
|
||||||
caller.msg("No aliases exist for '%s'." % obj.get_display_name(caller))
|
caller.msg("No aliases exist for '%s'." % obj.get_display_name(caller))
|
||||||
return
|
return
|
||||||
|
|
@ -159,17 +169,27 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
||||||
caller.msg("No aliases to clear.")
|
caller.msg("No aliases to clear.")
|
||||||
return
|
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)
|
# merge the old and new aliases (if any)
|
||||||
old_aliases = obj.aliases.all()
|
old_aliases = obj.aliases.get(category=category, return_list=True)
|
||||||
new_aliases = [alias.strip().lower() for alias in self.rhs.split(',')
|
new_aliases = [alias.strip().lower() for alias in rhs.split(',') if alias.strip()]
|
||||||
if alias.strip()]
|
|
||||||
|
|
||||||
# make the aliases only appear once
|
# make the aliases only appear once
|
||||||
old_aliases.extend(new_aliases)
|
old_aliases.extend(new_aliases)
|
||||||
aliases = list(set(old_aliases))
|
aliases = list(set(old_aliases))
|
||||||
|
|
||||||
# save back to object.
|
# save back to object.
|
||||||
obj.aliases.add(aliases)
|
obj.aliases.add(aliases, category=category)
|
||||||
|
|
||||||
# we need to trigger this here, since this will force
|
# we need to trigger this here, since this will force
|
||||||
# (default) Exits to rebuild their Exit commands with the new
|
# (default) Exits to rebuild their Exit commands with the new
|
||||||
|
|
@ -177,7 +197,8 @@ class CmdSetObjAlias(COMMAND_DEFAULT_CLASS):
|
||||||
obj.at_cmdset_get(force_init=True)
|
obj.at_cmdset_get(force_init=True)
|
||||||
|
|
||||||
# report all aliases on the object
|
# 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):
|
class CmdCopy(ObjManipCommand):
|
||||||
|
|
@ -198,6 +219,7 @@ class CmdCopy(ObjManipCommand):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@copy"
|
key = "@copy"
|
||||||
|
switch_options = ("reset",)
|
||||||
locks = "cmd:perm(copy) or perm(Builder)"
|
locks = "cmd:perm(copy) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -279,6 +301,7 @@ class CmdCpAttr(ObjManipCommand):
|
||||||
If you don't supply a source object, yourself is used.
|
If you don't supply a source object, yourself is used.
|
||||||
"""
|
"""
|
||||||
key = "@cpattr"
|
key = "@cpattr"
|
||||||
|
switch_options = ("move",)
|
||||||
locks = "cmd:perm(cpattr) or perm(Builder)"
|
locks = "cmd:perm(cpattr) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -420,6 +443,7 @@ class CmdMvAttr(ObjManipCommand):
|
||||||
object. If you don't supply a source object, yourself is used.
|
object. If you don't supply a source object, yourself is used.
|
||||||
"""
|
"""
|
||||||
key = "@mvattr"
|
key = "@mvattr"
|
||||||
|
switch_options = ("copy",)
|
||||||
locks = "cmd:perm(mvattr) or perm(Builder)"
|
locks = "cmd:perm(mvattr) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -468,6 +492,7 @@ class CmdCreate(ObjManipCommand):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@create"
|
key = "@create"
|
||||||
|
switch_options = ("drop",)
|
||||||
locks = "cmd:perm(create) or perm(Builder)"
|
locks = "cmd:perm(create) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -553,6 +578,7 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@desc"
|
key = "@desc"
|
||||||
aliases = "@describe"
|
aliases = "@describe"
|
||||||
|
switch_options = ("edit",)
|
||||||
locks = "cmd:perm(desc) or perm(Builder)"
|
locks = "cmd:perm(desc) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -568,6 +594,9 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not (obj.access(self.caller, 'control') or obj.access(self.caller, 'edit')):
|
||||||
|
self.caller.msg("You don't have permission to edit the description of %s." % obj.key)
|
||||||
|
|
||||||
self.caller.db.evmenu_target = obj
|
self.caller.db.evmenu_target = obj
|
||||||
# launch the editor
|
# launch the editor
|
||||||
EvEditor(self.caller, loadfunc=_desc_load, savefunc=_desc_save,
|
EvEditor(self.caller, loadfunc=_desc_load, savefunc=_desc_save,
|
||||||
|
|
@ -597,7 +626,7 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
desc = self.args
|
desc = self.args
|
||||||
if obj.access(caller, "edit"):
|
if (obj.access(self.caller, 'control') or obj.access(self.caller, 'edit')):
|
||||||
obj.db.desc = desc
|
obj.db.desc = desc
|
||||||
caller.msg("The description was set on %s." % obj.get_display_name(caller))
|
caller.msg("The description was set on %s." % obj.get_display_name(caller))
|
||||||
else:
|
else:
|
||||||
|
|
@ -611,11 +640,11 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
||||||
Usage:
|
Usage:
|
||||||
@destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]
|
@destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]
|
||||||
|
|
||||||
switches:
|
Switches:
|
||||||
override - The @destroy command will usually avoid accidentally
|
override - The @destroy command will usually avoid accidentally
|
||||||
destroying account objects. This switch overrides this safety.
|
destroying account objects. This switch overrides this safety.
|
||||||
force - destroy without confirmation.
|
force - destroy without confirmation.
|
||||||
examples:
|
Examples:
|
||||||
@destroy house, roof, door, 44-78
|
@destroy house, roof, door, 44-78
|
||||||
@destroy 5-10, flower, 45
|
@destroy 5-10, flower, 45
|
||||||
@destroy/force north
|
@destroy/force north
|
||||||
|
|
@ -628,6 +657,7 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@destroy"
|
key = "@destroy"
|
||||||
aliases = ["@delete", "@del"]
|
aliases = ["@delete", "@del"]
|
||||||
|
switch_options = ("override", "force")
|
||||||
locks = "cmd:perm(destroy) or perm(Builder)"
|
locks = "cmd:perm(destroy) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -751,6 +781,7 @@ class CmdDig(ObjManipCommand):
|
||||||
would be 'north;no;n'.
|
would be 'north;no;n'.
|
||||||
"""
|
"""
|
||||||
key = "@dig"
|
key = "@dig"
|
||||||
|
switch_options = ("teleport",)
|
||||||
locks = "cmd:perm(dig) or perm(Builder)"
|
locks = "cmd:perm(dig) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -860,7 +891,7 @@ class CmdDig(ObjManipCommand):
|
||||||
new_back_exit.dbref,
|
new_back_exit.dbref,
|
||||||
alias_string)
|
alias_string)
|
||||||
caller.msg("%s%s%s" % (room_string, exit_to_string, exit_back_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)
|
caller.move_to(new_room)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -893,6 +924,7 @@ class CmdTunnel(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@tunnel"
|
key = "@tunnel"
|
||||||
aliases = ["@tun"]
|
aliases = ["@tun"]
|
||||||
|
switch_options = ("oneway", "tel")
|
||||||
locks = "cmd: perm(tunnel) or perm(Builder)"
|
locks = "cmd: perm(tunnel) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -1455,6 +1487,13 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
|
|
||||||
Switch:
|
Switch:
|
||||||
edit: Open the line editor (string values only)
|
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
|
Sets attributes on objects. The second form clears
|
||||||
a previously set attribute while the last form
|
a previously set attribute while the last form
|
||||||
|
|
@ -1555,6 +1594,38 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
# start the editor
|
# start the editor
|
||||||
EvEditor(self.caller, load, save, key="%s/%s" % (obj, attr))
|
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):
|
def func(self):
|
||||||
"""Implement the set attribute - a limited form of @py."""
|
"""Implement the set attribute - a limited form of @py."""
|
||||||
|
|
||||||
|
|
@ -1568,10 +1639,7 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
objname = self.lhs_objattr[0]['name']
|
objname = self.lhs_objattr[0]['name']
|
||||||
attrs = self.lhs_objattr[0]['attrs']
|
attrs = self.lhs_objattr[0]['attrs']
|
||||||
|
|
||||||
if objname.startswith('*'):
|
obj = self.search_for_obj(objname)
|
||||||
obj = caller.search_account(objname.lstrip('*'))
|
|
||||||
else:
|
|
||||||
obj = caller.search(objname)
|
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
@ -1581,6 +1649,10 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
result = []
|
result = []
|
||||||
if "edit" in self.switches:
|
if "edit" in self.switches:
|
||||||
# edit in the line editor
|
# edit in the line editor
|
||||||
|
if not (obj.access(self.caller, 'control') or obj.access(self.caller, 'edit')):
|
||||||
|
caller.msg("You don't have permission to edit %s." % obj.key)
|
||||||
|
return
|
||||||
|
|
||||||
if len(attrs) > 1:
|
if len(attrs) > 1:
|
||||||
caller.msg("The Line editor can only be applied "
|
caller.msg("The Line editor can only be applied "
|
||||||
"to one attribute at a time.")
|
"to one attribute at a time.")
|
||||||
|
|
@ -1601,12 +1673,18 @@ class CmdSetAttribute(ObjManipCommand):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# deleting the attribute(s)
|
# deleting the attribute(s)
|
||||||
|
if not (obj.access(self.caller, 'control') or obj.access(self.caller, 'edit')):
|
||||||
|
caller.msg("You don't have permission to edit %s." % obj.key)
|
||||||
|
return
|
||||||
for attr in attrs:
|
for attr in attrs:
|
||||||
if not self.check_attr(obj, attr):
|
if not self.check_attr(obj, attr):
|
||||||
continue
|
continue
|
||||||
result.append(self.rm_attr(obj, attr))
|
result.append(self.rm_attr(obj, attr))
|
||||||
else:
|
else:
|
||||||
# setting attribute(s). Make sure to convert to real Python type before saving.
|
# setting attribute(s). Make sure to convert to real Python type before saving.
|
||||||
|
if not (obj.access(self.caller, 'control') or obj.access(self.caller, 'edit')):
|
||||||
|
caller.msg("You don't have permission to edit %s." % obj.key)
|
||||||
|
return
|
||||||
for attr in attrs:
|
for attr in attrs:
|
||||||
if not self.check_attr(obj, attr):
|
if not self.check_attr(obj, attr):
|
||||||
continue
|
continue
|
||||||
|
|
@ -1807,13 +1885,13 @@ class CmdLock(ObjManipCommand):
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
'get: id(25) or perm(Admin)'
|
'get: id(25) or perm(Admin)'
|
||||||
The 'get' access_type is checked by the get command and will
|
The 'get' lock access_type is checked e.g. by the 'get' command.
|
||||||
an object locked with this string will only be possible to
|
An object locked with this example lock will only be possible to pick up
|
||||||
pick up by Admins or by object with id=25.
|
by Admins or by an object with id=25.
|
||||||
|
|
||||||
You can add several access_types after one another by separating
|
You can add several access_types after one another by separating
|
||||||
them by ';', i.e:
|
them by ';', i.e:
|
||||||
'get:id(25);delete:perm(Builder)'
|
'get:id(25); delete:perm(Builder)'
|
||||||
"""
|
"""
|
||||||
key = "@lock"
|
key = "@lock"
|
||||||
aliases = ["@locks"]
|
aliases = ["@locks"]
|
||||||
|
|
@ -1840,9 +1918,16 @@ class CmdLock(ObjManipCommand):
|
||||||
obj = caller.search(objname)
|
obj = caller.search(objname)
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
if not (obj.access(caller, 'control') or obj.access(caller, "edit")):
|
has_control_access = obj.access(caller, 'control')
|
||||||
|
if access_type == 'control' and not has_control_access:
|
||||||
|
# only allow to change 'control' access if you have 'control' access already
|
||||||
|
caller.msg("You need 'control' access to change this type of lock.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not has_control_access or obj.access(caller, "edit"):
|
||||||
caller.msg("You are not allowed to do that.")
|
caller.msg("You are not allowed to do that.")
|
||||||
return
|
return
|
||||||
|
|
||||||
lockdef = obj.locks.get(access_type)
|
lockdef = obj.locks.get(access_type)
|
||||||
|
|
||||||
if lockdef:
|
if lockdef:
|
||||||
|
|
@ -2182,12 +2267,15 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@find[/switches] <name or dbref or *account> [= dbrefmin[-dbrefmax]]
|
@find[/switches] <name or dbref or *account> [= dbrefmin[-dbrefmax]]
|
||||||
|
@locate - this is a shorthand for using the /loc switch.
|
||||||
|
|
||||||
Switches:
|
Switches:
|
||||||
room - only look for rooms (location=None)
|
room - only look for rooms (location=None)
|
||||||
exit - only look for exits (destination!=None)
|
exit - only look for exits (destination!=None)
|
||||||
char - only look for characters (BASE_CHARACTER_TYPECLASS)
|
char - only look for characters (BASE_CHARACTER_TYPECLASS)
|
||||||
exact- only exact matches are returned.
|
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.
|
Searches the database for an object of a particular name or exact #dbref.
|
||||||
Use *accountname to search for an account. The switches allows for
|
Use *accountname to search for an account. The switches allows for
|
||||||
|
|
@ -2198,6 +2286,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@find"
|
key = "@find"
|
||||||
aliases = "@search, @locate"
|
aliases = "@search, @locate"
|
||||||
|
switch_options = ("room", "exit", "char", "exact", "loc", "startswith")
|
||||||
locks = "cmd:perm(find) or perm(Builder)"
|
locks = "cmd:perm(find) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -2210,6 +2299,9 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
caller.msg("Usage: @find <string> [= low [-high]]")
|
caller.msg("Usage: @find <string> [= low [-high]]")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if "locate" in self.cmdstring: # Use option /loc as a default for @locate command alias
|
||||||
|
switches.append('loc')
|
||||||
|
|
||||||
searchstring = self.lhs
|
searchstring = self.lhs
|
||||||
low, high = 1, ObjectDB.objects.all().order_by("-id")[0].id
|
low, high = 1, ObjectDB.objects.all().order_by("-id")[0].id
|
||||||
if self.rhs:
|
if self.rhs:
|
||||||
|
|
@ -2231,7 +2323,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
restrictions = ""
|
restrictions = ""
|
||||||
if self.switches:
|
if self.switches:
|
||||||
restrictions = ", %s" % (",".join(self.switches))
|
restrictions = ", %s" % (", ".join(self.switches))
|
||||||
|
|
||||||
if is_dbref or is_account:
|
if is_dbref or is_account:
|
||||||
|
|
||||||
|
|
@ -2259,6 +2351,8 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
else:
|
else:
|
||||||
result = result[0]
|
result = result[0]
|
||||||
string += "\n|g %s - %s|n" % (result.get_display_name(caller), result.path)
|
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:
|
else:
|
||||||
# Not an account/dbref search but a wider search; build a queryset.
|
# Not an account/dbref search but a wider search; build a queryset.
|
||||||
# Searchs for key and aliases
|
# Searchs for key and aliases
|
||||||
|
|
@ -2266,10 +2360,14 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high)
|
keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high)
|
||||||
aliasquery = Q(db_tags__db_key__iexact=searchstring,
|
aliasquery = Q(db_tags__db_key__iexact=searchstring,
|
||||||
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
|
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)
|
keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high)
|
||||||
aliasquery = Q(db_tags__db_key__istartswith=searchstring,
|
aliasquery = Q(db_tags__db_key__istartswith=searchstring,
|
||||||
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
|
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()
|
results = ObjectDB.objects.filter(keyquery | aliasquery).distinct()
|
||||||
nresults = results.count()
|
nresults = results.count()
|
||||||
|
|
@ -2294,6 +2392,8 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
else:
|
else:
|
||||||
string = "|wOne Match|n(#%i-#%i%s):" % (low, high, restrictions)
|
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)
|
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:
|
else:
|
||||||
string = "|wMatch|n(#%i-#%i%s):" % (low, high, restrictions)
|
string = "|wMatch|n(#%i-#%i%s):" % (low, high, restrictions)
|
||||||
string += "\n |RNo matches found for '%s'|n" % searchstring
|
string += "\n |RNo matches found for '%s'|n" % searchstring
|
||||||
|
|
@ -2307,11 +2407,11 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
||||||
teleport object to another location
|
teleport object to another location
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@tel/switch [<object> =] <target location>
|
@tel/switch [<object> to||=] <target location>
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@tel Limbo
|
@tel Limbo
|
||||||
@tel/quiet box Limbo
|
@tel/quiet box = Limbo
|
||||||
@tel/tonone box
|
@tel/tonone box
|
||||||
|
|
||||||
Switches:
|
Switches:
|
||||||
|
|
@ -2327,9 +2427,12 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
||||||
loc - teleport object to the target's location instead of its contents
|
loc - teleport object to the target's location instead of its contents
|
||||||
|
|
||||||
Teleports an object somewhere. If no object is given, you yourself
|
Teleports an object somewhere. If no object is given, you yourself
|
||||||
is teleported to the target location. """
|
is teleported to the target location.
|
||||||
|
"""
|
||||||
key = "@tel"
|
key = "@tel"
|
||||||
aliases = "@teleport"
|
aliases = "@teleport"
|
||||||
|
switch_options = ("quiet", "intoexit", "tonone", "loc")
|
||||||
|
rhs_split = ("=", " to ") # Prefer = delimiter, but allow " to " usage.
|
||||||
locks = "cmd:perm(teleport) or perm(Builder)"
|
locks = "cmd:perm(teleport) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -2437,6 +2540,7 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@script"
|
key = "@script"
|
||||||
aliases = "@addscript"
|
aliases = "@addscript"
|
||||||
|
switch_options = ("start", "stop")
|
||||||
locks = "cmd:perm(script) or perm(Builder)"
|
locks = "cmd:perm(script) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -2536,6 +2640,7 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@tag"
|
key = "@tag"
|
||||||
aliases = ["@tags"]
|
aliases = ["@tags"]
|
||||||
|
options = ("search", "del")
|
||||||
locks = "cmd:perm(tag) or perm(Builder)"
|
locks = "cmd:perm(tag) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
arg_regex = r"(/\w+?(\s|$))|\s|$"
|
arg_regex = r"(/\w+?(\s|$))|\s|$"
|
||||||
|
|
@ -2677,6 +2782,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@spawn"
|
key = "@spawn"
|
||||||
|
switch_options = ("noloc", )
|
||||||
locks = "cmd:perm(spawn) or perm(Builder)"
|
locks = "cmd:perm(spawn) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -2686,7 +2792,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
def _show_prototypes(prototypes):
|
def _show_prototypes(prototypes):
|
||||||
"""Helper to show a list of available prototypes"""
|
"""Helper to show a list of available prototypes"""
|
||||||
prots = ", ".join(sorted(prototypes.keys()))
|
prots = ", ".join(sorted(prototypes.keys()))
|
||||||
return "\nAvailable prototypes (case sensistive): %s" % (
|
return "\nAvailable prototypes (case sensitive): %s" % (
|
||||||
"\n" + utils.fill(prots) if prots else "None")
|
"\n" + utils.fill(prots) if prots else "None")
|
||||||
|
|
||||||
prototypes = spawn(return_prototypes=True)
|
prototypes = spawn(return_prototypes=True)
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ command method rather than caller.msg().
|
||||||
|
|
||||||
from evennia.commands.cmdset import CmdSet
|
from evennia.commands.cmdset import CmdSet
|
||||||
from evennia.commands.default import help, comms, admin, system
|
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):
|
class AccountCmdSet(CmdSet):
|
||||||
|
|
@ -39,6 +39,9 @@ class AccountCmdSet(CmdSet):
|
||||||
self.add(account.CmdColorTest())
|
self.add(account.CmdColorTest())
|
||||||
self.add(account.CmdQuell())
|
self.add(account.CmdQuell())
|
||||||
|
|
||||||
|
# nicks
|
||||||
|
self.add(general.CmdNick())
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
self.add(building.CmdExamine())
|
self.add(building.CmdExamine())
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,3 +23,4 @@ class UnloggedinCmdSet(CmdSet):
|
||||||
self.add(unloggedin.CmdUnconnectedHelp())
|
self.add(unloggedin.CmdUnconnectedHelp())
|
||||||
self.add(unloggedin.CmdUnconnectedEncoding())
|
self.add(unloggedin.CmdUnconnectedEncoding())
|
||||||
self.add(unloggedin.CmdUnconnectedScreenreader())
|
self.add(unloggedin.CmdUnconnectedScreenreader())
|
||||||
|
self.add(unloggedin.CmdUnconnectedInfo())
|
||||||
|
|
|
||||||
|
|
@ -377,7 +377,7 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
|
||||||
Usage:
|
Usage:
|
||||||
@cboot[/quiet] <channel> = <account> [:reason]
|
@cboot[/quiet] <channel> = <account> [:reason]
|
||||||
|
|
||||||
Switches:
|
Switch:
|
||||||
quiet - don't notify the channel
|
quiet - don't notify the channel
|
||||||
|
|
||||||
Kicks an account or object from a channel you control.
|
Kicks an account or object from a channel you control.
|
||||||
|
|
@ -385,6 +385,7 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@cboot"
|
key = "@cboot"
|
||||||
|
switch_options = ("quiet",)
|
||||||
locks = "cmd: not pperm(channel_banned)"
|
locks = "cmd: not pperm(channel_banned)"
|
||||||
help_category = "Comms"
|
help_category = "Comms"
|
||||||
|
|
||||||
|
|
@ -453,6 +454,7 @@ class CmdCemit(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@cemit"
|
key = "@cemit"
|
||||||
aliases = ["@cmsg"]
|
aliases = ["@cmsg"]
|
||||||
|
switch_options = ("sendername", "quiet")
|
||||||
locks = "cmd: not pperm(channel_banned) and pperm(Player)"
|
locks = "cmd: not pperm(channel_banned) and pperm(Player)"
|
||||||
help_category = "Comms"
|
help_category = "Comms"
|
||||||
|
|
||||||
|
|
@ -683,6 +685,7 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "page"
|
key = "page"
|
||||||
aliases = ['tell']
|
aliases = ['tell']
|
||||||
|
switch_options = ("last", "list")
|
||||||
locks = "cmd:not pperm(page_banned)"
|
locks = "cmd:not pperm(page_banned)"
|
||||||
help_category = "Comms"
|
help_category = "Comms"
|
||||||
|
|
||||||
|
|
@ -850,6 +853,7 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@irc2chan"
|
key = "@irc2chan"
|
||||||
|
switch_options = ("delete", "remove", "disconnect", "list", "ssl")
|
||||||
locks = "cmd:serversetting(IRC_ENABLED) and pperm(Developer)"
|
locks = "cmd:serversetting(IRC_ENABLED) and pperm(Developer)"
|
||||||
help_category = "Comms"
|
help_category = "Comms"
|
||||||
|
|
||||||
|
|
@ -1016,6 +1020,7 @@ class CmdRSS2Chan(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
key = "@rss2chan"
|
key = "@rss2chan"
|
||||||
|
switch_options = ("disconnect", "remove", "list")
|
||||||
locks = "cmd:serversetting(RSS_ENABLED) and pperm(Developer)"
|
locks = "cmd:serversetting(RSS_ENABLED) and pperm(Developer)"
|
||||||
help_category = "Comms"
|
help_category = "Comms"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
"""
|
"""
|
||||||
General Character commands usually available to all characters
|
General Character commands usually available to all characters
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.utils import utils, evtable
|
from evennia.utils import utils, evtable
|
||||||
from evennia.typeclasses.attributes import NickTemplateInvalid
|
from evennia.typeclasses.attributes import NickTemplateInvalid
|
||||||
|
|
@ -70,42 +71,45 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
|
||||||
target = caller.search(self.args)
|
target = caller.search(self.args)
|
||||||
if not target:
|
if not target:
|
||||||
return
|
return
|
||||||
self.msg(caller.at_look(target))
|
self.msg((caller.at_look(target), {'type':'look'}), options=None)
|
||||||
|
|
||||||
|
|
||||||
class CmdNick(COMMAND_DEFAULT_CLASS):
|
class CmdNick(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
define a personal alias/nick
|
define a personal alias/nick by defining a string to
|
||||||
|
match and replace it with another on the fly
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
nick[/switches] <string> [= [replacement_string]]
|
nick[/switches] <string> [= [replacement_string]]
|
||||||
nick[/switches] <template> = <replacement_template>
|
nick[/switches] <template> = <replacement_template>
|
||||||
nick/delete <string> or number
|
nick/delete <string> or number
|
||||||
nick/test <test string>
|
nicks
|
||||||
|
|
||||||
Switches:
|
Switches:
|
||||||
inputline - replace on the inputline (default)
|
inputline - replace on the inputline (default)
|
||||||
object - replace on object-lookup
|
object - replace on object-lookup
|
||||||
account - replace on account-lookup
|
account - replace on account-lookup
|
||||||
delete - remove nick by name or by index given by /list
|
|
||||||
clearall - clear all nicks
|
|
||||||
list - show all defined aliases (also "nicks" works)
|
list - show all defined aliases (also "nicks" works)
|
||||||
test - test input to see what it matches with
|
delete - remove nick by index in /list
|
||||||
|
clearall - clear all nicks
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
nick hi = say Hello, I'm Sarah!
|
nick hi = say Hello, I'm Sarah!
|
||||||
nick/object tom = the tall man
|
nick/object tom = the tall man
|
||||||
nick build $1 $2 = @create/drop $1;$2 - (template)
|
nick build $1 $2 = @create/drop $1;$2
|
||||||
nick tell $1 $2=@page $1=$2 - (template)
|
nick tell $1 $2=@page $1=$2
|
||||||
|
nick tm?$1=@page tallman=$1
|
||||||
|
nick tm\=$1=@page tallman=$1
|
||||||
|
|
||||||
A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
|
A 'nick' is a personal string replacement. Use $1, $2, ... to catch arguments.
|
||||||
Put the last $-marker without an ending space to catch all remaining text. You
|
Put the last $-marker without an ending space to catch all remaining text. You
|
||||||
can also use unix-glob matching:
|
can also use unix-glob matching for the left-hand side <string>:
|
||||||
|
|
||||||
* - matches everything
|
* - matches everything
|
||||||
? - matches a single character
|
? - matches 0 or 1 single characters
|
||||||
[seq] - matches all chars in sequence
|
[abcd] - matches these chars in any order
|
||||||
[!seq] - matches everything not in sequence
|
[!abcd] - matches everything not among these chars
|
||||||
|
\= - escape literal '=' you want in your <string>
|
||||||
|
|
||||||
Note that no objects are actually renamed or changed by this command - your nicks
|
Note that no objects are actually renamed or changed by this command - your nicks
|
||||||
are only available to you. If you want to permanently add keywords to an object
|
are only available to you. If you want to permanently add keywords to an object
|
||||||
|
|
@ -113,17 +117,40 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
key = "nick"
|
key = "nick"
|
||||||
aliases = ["nickname", "nicks", "alias"]
|
switch_options = ("inputline", "object", "account", "list", "delete", "clearall")
|
||||||
|
aliases = ["nickname", "nicks"]
|
||||||
locks = "cmd:all()"
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def parse(self):
|
||||||
|
"""
|
||||||
|
Support escaping of = with \=
|
||||||
|
"""
|
||||||
|
super(CmdNick, self).parse()
|
||||||
|
args = (self.lhs or "") + (" = %s" % self.rhs if self.rhs else "")
|
||||||
|
parts = re.split(r"(?<!\\)=", args, 1)
|
||||||
|
self.rhs = None
|
||||||
|
if len(parts) < 2:
|
||||||
|
self.lhs = parts[0].strip()
|
||||||
|
else:
|
||||||
|
self.lhs, self.rhs = [part.strip() for part in parts]
|
||||||
|
self.lhs = self.lhs.replace("\=", "=")
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""Create the nickname"""
|
"""Create the nickname"""
|
||||||
|
|
||||||
|
def _cy(string):
|
||||||
|
"add color to the special markers"
|
||||||
|
return re.sub(r"(\$[0-9]+|\*|\?|\[.+?\])", r"|Y\1|n", string)
|
||||||
|
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
switches = self.switches
|
switches = self.switches
|
||||||
nicktypes = [switch for switch in switches if switch in ("object", "account", "inputline")] or ["inputline"]
|
nicktypes = [switch for switch in switches if switch in ("object", "account", "inputline")]
|
||||||
|
specified_nicktype = bool(nicktypes)
|
||||||
|
nicktypes = nicktypes if specified_nicktype else ["inputline"]
|
||||||
|
|
||||||
nicklist = utils.make_iter(caller.nicks.get(return_obj=True) or [])
|
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(caller.nicks.get(category="account", return_obj=True) or []))
|
||||||
|
|
||||||
if 'list' in switches or self.cmdstring in ("nicks", "@nicks"):
|
if 'list' in switches or self.cmdstring in ("nicks", "@nicks"):
|
||||||
|
|
||||||
|
|
@ -133,24 +160,121 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
||||||
table = evtable.EvTable("#", "Type", "Nick match", "Replacement")
|
table = evtable.EvTable("#", "Type", "Nick match", "Replacement")
|
||||||
for inum, nickobj in enumerate(nicklist):
|
for inum, nickobj in enumerate(nicklist):
|
||||||
_, _, nickvalue, replacement = nickobj.value
|
_, _, nickvalue, replacement = nickobj.value
|
||||||
table.add_row(str(inum + 1), nickobj.db_category, nickvalue, replacement)
|
table.add_row(str(inum + 1), nickobj.db_category, _cy(nickvalue), _cy(replacement))
|
||||||
string = "|wDefined Nicks:|n\n%s" % table
|
string = "|wDefined Nicks:|n\n%s" % table
|
||||||
caller.msg(string)
|
caller.msg(string)
|
||||||
return
|
return
|
||||||
|
|
||||||
if 'clearall' in switches:
|
if 'clearall' in switches:
|
||||||
caller.nicks.clear()
|
caller.nicks.clear()
|
||||||
|
caller.account.nicks.clear()
|
||||||
caller.msg("Cleared all nicks.")
|
caller.msg("Cleared all nicks.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if 'delete' in switches or 'del' in switches:
|
||||||
|
if not self.args or not self.lhs:
|
||||||
|
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):
|
||||||
|
oldnicks.append(nicklist[delindex - 1])
|
||||||
|
else:
|
||||||
|
caller.msg("Not a valid nick index. See 'nicks' for a list.")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if not specified_nicktype:
|
||||||
|
nicktypes = ("object", "account", "inputline")
|
||||||
|
for nicktype in nicktypes:
|
||||||
|
oldnicks.append(caller.nicks.get(arg, category=nicktype, return_obj=True))
|
||||||
|
|
||||||
|
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))
|
||||||
|
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
|
||||||
|
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.args or not self.lhs:
|
if not self.args or not self.lhs:
|
||||||
caller.msg("Usage: nick[/switches] nickname = [realname]")
|
caller.msg("Usage: nick[/switches] nickname = [realname]")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# setting new nicks
|
||||||
|
|
||||||
nickstring = self.lhs
|
nickstring = self.lhs
|
||||||
replstring = self.rhs
|
replstring = self.rhs
|
||||||
old_nickstring = None
|
|
||||||
old_replstring = None
|
|
||||||
|
|
||||||
if replstring == nickstring:
|
if replstring == nickstring:
|
||||||
caller.msg("No point in setting nick same as the string to replace...")
|
caller.msg("No point in setting nick same as the string to replace...")
|
||||||
|
|
@ -160,36 +284,24 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
||||||
errstring = ""
|
errstring = ""
|
||||||
string = ""
|
string = ""
|
||||||
for nicktype in nicktypes:
|
for nicktype in nicktypes:
|
||||||
|
nicktypestr = "%s-nick" % nicktype.capitalize()
|
||||||
|
old_nickstring = None
|
||||||
|
old_replstring = None
|
||||||
|
|
||||||
oldnick = caller.nicks.get(key=nickstring, category=nicktype, return_obj=True)
|
oldnick = caller.nicks.get(key=nickstring, category=nicktype, return_obj=True)
|
||||||
if oldnick:
|
if oldnick:
|
||||||
_, _, old_nickstring, old_replstring = oldnick.value
|
_, _, old_nickstring, old_replstring = oldnick.value
|
||||||
else:
|
if replstring:
|
||||||
# no old nick, see if a number was given
|
|
||||||
arg = self.args.lstrip("#")
|
|
||||||
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
|
|
||||||
else:
|
|
||||||
errstring += "Not a valid nick index."
|
|
||||||
else:
|
|
||||||
errstring += "Nick not found."
|
|
||||||
if "delete" in switches or "del" in switches:
|
|
||||||
# clear the nick
|
|
||||||
if old_nickstring and caller.nicks.has(old_nickstring, category=nicktype):
|
|
||||||
caller.nicks.remove(old_nickstring, category=nicktype)
|
|
||||||
string += "\nNick removed: '|w%s|n' -> |w%s|n." % (old_nickstring, old_replstring)
|
|
||||||
else:
|
|
||||||
errstring += "\nNick '|w%s|n' was not deleted." % old_nickstring
|
|
||||||
elif replstring:
|
|
||||||
# creating new nick
|
# creating new nick
|
||||||
errstring = ""
|
errstring = ""
|
||||||
if oldnick:
|
if oldnick:
|
||||||
string += "\nNick '|w%s|n' updated to map to '|w%s|n'." % (old_nickstring, replstring)
|
if replstring == old_replstring:
|
||||||
|
string += "\nIdentical %s already set." % nicktypestr.lower()
|
||||||
else:
|
else:
|
||||||
string += "\nNick '|w%s|n' mapped to '|w%s|n'." % (nickstring, replstring)
|
string += "\n%s '|w%s|n' updated to map to '|w%s|n'." % (
|
||||||
|
nicktypestr, old_nickstring, replstring)
|
||||||
|
else:
|
||||||
|
string += "\n%s '|w%s|n' mapped to '|w%s|n'." % (nicktypestr, nickstring, replstring)
|
||||||
try:
|
try:
|
||||||
caller.nicks.add(nickstring, replstring, category=nicktype)
|
caller.nicks.add(nickstring, replstring, category=nicktype)
|
||||||
except NickTemplateInvalid:
|
except NickTemplateInvalid:
|
||||||
|
|
@ -197,10 +309,10 @@ class CmdNick(COMMAND_DEFAULT_CLASS):
|
||||||
return
|
return
|
||||||
elif old_nickstring and old_replstring:
|
elif old_nickstring and old_replstring:
|
||||||
# just looking at the nick
|
# just looking at the nick
|
||||||
string += "\nNick '|w%s|n' maps to '|w%s|n'." % (old_nickstring, old_replstring)
|
string += "\n%s '|w%s|n' maps to '|w%s|n'." % (nicktypestr, old_nickstring, old_replstring)
|
||||||
errstring = ""
|
errstring = ""
|
||||||
string = errstring if errstring else string
|
string = errstring if errstring else string
|
||||||
caller.msg(string)
|
caller.msg(_cy(string))
|
||||||
|
|
||||||
|
|
||||||
class CmdInventory(COMMAND_DEFAULT_CLASS):
|
class CmdInventory(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -330,12 +442,13 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
||||||
give away something to someone
|
give away something to someone
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
give <inventory obj> = <target>
|
give <inventory obj> <to||=> <target>
|
||||||
|
|
||||||
Gives an items from your inventory to another character,
|
Gives an items from your inventory to another character,
|
||||||
placing it in their inventory.
|
placing it in their inventory.
|
||||||
"""
|
"""
|
||||||
key = "give"
|
key = "give"
|
||||||
|
rhs_split = ("=", " to ") # Prefer = delimiter, but allow " to " usage.
|
||||||
locks = "cmd:all()"
|
locks = "cmd:all()"
|
||||||
arg_regex = r"\s|$"
|
arg_regex = r"\s|$"
|
||||||
|
|
||||||
|
|
@ -439,7 +552,7 @@ class CmdWhisper(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
whisper <character> = <message>
|
whisper <character> = <message>
|
||||||
whisper <char1>, <char2> = <message?
|
whisper <char1>, <char2> = <message>
|
||||||
|
|
||||||
Talk privately to one or more characters in your current location, without
|
Talk privately to one or more characters in your current location, without
|
||||||
others in the room being informed.
|
others in the room being informed.
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
key = "@sethelp"
|
key = "@sethelp"
|
||||||
|
switch_options = ("edit", "replace", "append", "extend", "delete")
|
||||||
locks = "cmd:perm(Helper)"
|
locks = "cmd:perm(Helper)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,13 @@ class MuxCommand(Command):
|
||||||
it here). The rest of the command is stored in self.args, which can
|
it here). The rest of the command is stored in self.args, which can
|
||||||
start with the switch indicator /.
|
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
|
This parser breaks self.args into its constituents and stores them in the
|
||||||
following variables:
|
following variables:
|
||||||
self.switches = [list of /switches (without the /)]
|
self.switches = [list of /switches (without the /)]
|
||||||
|
|
@ -97,9 +104,18 @@ class MuxCommand(Command):
|
||||||
"""
|
"""
|
||||||
raw = self.args
|
raw = self.args
|
||||||
args = raw.strip()
|
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
|
# 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] == "/":
|
if args and len(args) > 1 and raw[0] == "/":
|
||||||
# we have a switch, or a set of switches. These end with a space.
|
# we have a switch, or a set of switches. These end with a space.
|
||||||
switches = args[1:].split(None, 1)
|
switches = args[1:].split(None, 1)
|
||||||
|
|
@ -109,16 +125,50 @@ class MuxCommand(Command):
|
||||||
else:
|
else:
|
||||||
args = ""
|
args = ""
|
||||||
switches = switches[0].split('/')
|
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()]
|
arglist = [arg.strip() for arg in args.split()]
|
||||||
|
|
||||||
# check for arg1, arg2, ... = argA, argB, ... constructs
|
# check for arg1, arg2, ... = argA, argB, ... constructs
|
||||||
lhs, rhs = args, None
|
lhs, rhs = args.strip(), None
|
||||||
lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
|
if lhs:
|
||||||
if args and '=' in args:
|
if delimiters and hasattr(delimiters, '__iter__'): # If delimiter is iterable,
|
||||||
lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
|
best_split = delimiters[0] # (default to first delimiter)
|
||||||
lhslist = [arg.strip() for arg in lhs.split(',')]
|
for this_split in delimiters: # try each delimiter
|
||||||
rhslist = [arg.strip() for arg in rhs.split(',')]
|
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:
|
# save to object properties:
|
||||||
self.raw = raw
|
self.raw = raw
|
||||||
self.switches = switches
|
self.switches = switches
|
||||||
|
|
@ -133,7 +183,7 @@ class MuxCommand(Command):
|
||||||
# sure that self.caller is always the account if possible. We also create
|
# sure that self.caller is always the account if possible. We also create
|
||||||
# a special property "character" for the puppeted object, if any. This
|
# a special property "character" for the puppeted object, if any. This
|
||||||
# is convenient for commands defined on the Account only.
|
# 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"):
|
if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
||||||
# caller is an Object/Character
|
# caller is an Object/Character
|
||||||
self.character = self.caller
|
self.character = self.caller
|
||||||
|
|
@ -169,6 +219,8 @@ class MuxCommand(Command):
|
||||||
string += "\nraw argument (self.raw): |w%s|n \n" % self.raw
|
string += "\nraw argument (self.raw): |w%s|n \n" % self.raw
|
||||||
string += "cmd args (self.args): |w%s|n\n" % self.args
|
string += "cmd args (self.args): |w%s|n\n" % self.args
|
||||||
string += "cmd switches (self.switches): |w%s|n\n" % self.switches
|
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 += "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, left-hand side of '=' (self.lhs): |w%s|n\n" % self.lhs
|
||||||
string += "lhs, comma separated (self.lhslist): |w%s|n\n" % self.lhslist
|
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.
|
character is actually attached to this Account and Session.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def parse(self):
|
account_caller = True # Using MuxAccountCommand explicitly defaults the caller to an account
|
||||||
"""
|
|
||||||
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
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ class CmdReload(COMMAND_DEFAULT_CLASS):
|
||||||
if self.args:
|
if self.args:
|
||||||
reason = "(Reason: %s) " % self.args.rstrip(".")
|
reason = "(Reason: %s) " % self.args.rstrip(".")
|
||||||
SESSIONS.announce_all(" Server restart initiated %s..." % reason)
|
SESSIONS.announce_all(" Server restart initiated %s..." % reason)
|
||||||
SESSIONS.server.shutdown(mode='reload')
|
SESSIONS.portal_restart_server()
|
||||||
|
|
||||||
|
|
||||||
class CmdReset(COMMAND_DEFAULT_CLASS):
|
class CmdReset(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -91,7 +91,7 @@ class CmdReset(COMMAND_DEFAULT_CLASS):
|
||||||
Reload the system.
|
Reload the system.
|
||||||
"""
|
"""
|
||||||
SESSIONS.announce_all(" Server resetting/restarting ...")
|
SESSIONS.announce_all(" Server resetting/restarting ...")
|
||||||
SESSIONS.server.shutdown(mode='reset')
|
SESSIONS.portal_reset_server()
|
||||||
|
|
||||||
|
|
||||||
class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -119,7 +119,6 @@ class CmdShutdown(COMMAND_DEFAULT_CLASS):
|
||||||
announcement += "%s\n" % self.args
|
announcement += "%s\n" % self.args
|
||||||
logger.log_info('Server shutdown by %s.' % self.caller.name)
|
logger.log_info('Server shutdown by %s.' % self.caller.name)
|
||||||
SESSIONS.announce_all(announcement)
|
SESSIONS.announce_all(announcement)
|
||||||
SESSIONS.server.shutdown(mode='shutdown')
|
|
||||||
SESSIONS.portal_shutdown()
|
SESSIONS.portal_shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -246,6 +245,7 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@py"
|
key = "@py"
|
||||||
aliases = ["!"]
|
aliases = ["!"]
|
||||||
|
switch_options = ("time", "edit")
|
||||||
locks = "cmd:perm(py) or perm(Developer)"
|
locks = "cmd:perm(py) or perm(Developer)"
|
||||||
help_category = "System"
|
help_category = "System"
|
||||||
|
|
||||||
|
|
@ -329,6 +329,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@scripts"
|
key = "@scripts"
|
||||||
aliases = ["@globalscript", "@listscripts"]
|
aliases = ["@globalscript", "@listscripts"]
|
||||||
|
switch_options = ("start", "stop", "kill", "validate")
|
||||||
locks = "cmd:perm(listscripts) or perm(Admin)"
|
locks = "cmd:perm(listscripts) or perm(Admin)"
|
||||||
help_category = "System"
|
help_category = "System"
|
||||||
|
|
||||||
|
|
@ -522,6 +523,7 @@ class CmdService(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@service"
|
key = "@service"
|
||||||
aliases = ["@services"]
|
aliases = ["@services"]
|
||||||
|
switch_options = ("list", "start", "stop", "delete")
|
||||||
locks = "cmd:perm(service) or perm(Developer)"
|
locks = "cmd:perm(service) or perm(Developer)"
|
||||||
help_category = "System"
|
help_category = "System"
|
||||||
|
|
||||||
|
|
@ -673,7 +675,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
||||||
Usage:
|
Usage:
|
||||||
@server[/mem]
|
@server[/mem]
|
||||||
|
|
||||||
Switch:
|
Switches:
|
||||||
mem - return only a string of the current memory usage
|
mem - return only a string of the current memory usage
|
||||||
flushmem - flush the idmapper cache
|
flushmem - flush the idmapper cache
|
||||||
|
|
||||||
|
|
@ -704,6 +706,7 @@ class CmdServerLoad(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
key = "@server"
|
key = "@server"
|
||||||
aliases = ["@serverload", "@serverprocess"]
|
aliases = ["@serverload", "@serverprocess"]
|
||||||
|
switch_options = ("mem", "flushmem")
|
||||||
locks = "cmd:perm(list) or perm(Developer)"
|
locks = "cmd:perm(list) or perm(Developer)"
|
||||||
help_category = "System"
|
help_category = "System"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,15 +14,17 @@ main test suite started with
|
||||||
|
|
||||||
import re
|
import re
|
||||||
import types
|
import types
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from mock import Mock
|
from mock import Mock, mock
|
||||||
|
|
||||||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms
|
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.commands.command import Command, InterruptCommand
|
||||||
from evennia.utils import ansi, utils
|
from evennia.utils import ansi, utils, gametime
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
from evennia import search_object
|
from evennia import search_object
|
||||||
from evennia import DefaultObject, DefaultCharacter
|
from evennia import DefaultObject, DefaultCharacter
|
||||||
|
|
@ -37,12 +39,13 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--*|\|(?:\s|$)", re.MULTILINE)
|
||||||
# Command testing
|
# Command testing
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class CommandTest(EvenniaTest):
|
class CommandTest(EvenniaTest):
|
||||||
"""
|
"""
|
||||||
Tests a command
|
Tests a command
|
||||||
"""
|
"""
|
||||||
|
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None,
|
||||||
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):
|
||||||
"""
|
"""
|
||||||
Test a command by assigning all the needed
|
Test a command by assigning all the needed
|
||||||
properties to cmdobj and running
|
properties to cmdobj and running
|
||||||
|
|
@ -71,10 +74,10 @@ class CommandTest(EvenniaTest):
|
||||||
cmdobj.obj = obj or (caller if caller else self.char1)
|
cmdobj.obj = obj or (caller if caller else self.char1)
|
||||||
# test
|
# test
|
||||||
old_msg = receiver.msg
|
old_msg = receiver.msg
|
||||||
returned_msg = ""
|
|
||||||
try:
|
try:
|
||||||
receiver.msg = Mock()
|
receiver.msg = Mock()
|
||||||
cmdobj.at_pre_cmd()
|
if cmdobj.at_pre_cmd():
|
||||||
|
return
|
||||||
cmdobj.parse()
|
cmdobj.parse()
|
||||||
ret = cmdobj.func()
|
ret = cmdobj.func()
|
||||||
if isinstance(ret, types.GeneratorType):
|
if isinstance(ret, types.GeneratorType):
|
||||||
|
|
@ -125,17 +128,48 @@ class TestGeneral(CommandTest):
|
||||||
self.call(general.CmdPose(), "looks around", "Char looks around")
|
self.call(general.CmdPose(), "looks around", "Char looks around")
|
||||||
|
|
||||||
def test_nick(self):
|
def test_nick(self):
|
||||||
self.call(general.CmdNick(), "testalias = testaliasedstring1", "Nick 'testalias' mapped to 'testaliasedstring1'.")
|
self.call(general.CmdNick(), "testalias = testaliasedstring1",
|
||||||
self.call(general.CmdNick(), "/account testalias = testaliasedstring2", "Nick 'testalias' mapped to 'testaliasedstring2'.")
|
"Inputlinenick 'testalias' mapped to 'testaliasedstring1'.")
|
||||||
self.call(general.CmdNick(), "/object testalias = testaliasedstring3", "Nick 'testalias' mapped to 'testaliasedstring3'.")
|
self.call(general.CmdNick(), "/account testalias = testaliasedstring2",
|
||||||
|
"Accountnick 'testalias' mapped to 'testaliasedstring2'.")
|
||||||
|
self.call(general.CmdNick(), "/object testalias = testaliasedstring3",
|
||||||
|
"Objectnick 'testalias' mapped to 'testaliasedstring3'.")
|
||||||
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
|
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
|
||||||
self.assertEqual(u"testaliasedstring2", self.char1.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"))
|
self.assertEqual(u"testaliasedstring3", self.char1.nicks.get("testalias", category="object"))
|
||||||
|
|
||||||
def test_get_and_drop(self):
|
def test_get_and_drop(self):
|
||||||
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
|
self.call(general.CmdGet(), "Obj", "You pick up Obj.")
|
||||||
self.call(general.CmdDrop(), "Obj", "You drop 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):
|
def test_say(self):
|
||||||
self.call(general.CmdSay(), "Testing", "You say, \"Testing\"")
|
self.call(general.CmdSay(), "Testing", "You say, \"Testing\"")
|
||||||
|
|
||||||
|
|
@ -183,7 +217,7 @@ class TestAdmin(CommandTest):
|
||||||
self.call(admin.CmdPerm(), "Char2 = Builder", "Permission 'Builder' given to Char2 (the Object/Character).")
|
self.call(admin.CmdPerm(), "Char2 = Builder", "Permission 'Builder' given to Char2 (the Object/Character).")
|
||||||
|
|
||||||
def test_wall(self):
|
def test_wall(self):
|
||||||
self.call(admin.CmdWall(), "Test", "Announcing to all connected accounts ...")
|
self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...")
|
||||||
|
|
||||||
def test_ban(self):
|
def test_ban(self):
|
||||||
self.call(admin.CmdBan(), "Char", "NameBan char was added.")
|
self.call(admin.CmdBan(), "Char", "NameBan char was added.")
|
||||||
|
|
@ -223,7 +257,8 @@ class TestAccount(CommandTest):
|
||||||
self.call(account.CmdColorTest(), "ansi", "ANSI colors:", caller=self.account)
|
self.call(account.CmdColorTest(), "ansi", "ANSI colors:", caller=self.account)
|
||||||
|
|
||||||
def test_char_create(self):
|
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):
|
def test_quell(self):
|
||||||
self.call(account.CmdQuell(), "", "Quelling to current puppet's permissions (developer).", caller=self.account)
|
self.call(account.CmdQuell(), "", "Quelling to current puppet's permissions (developer).", caller=self.account)
|
||||||
|
|
@ -232,16 +267,19 @@ class TestAccount(CommandTest):
|
||||||
class TestBuilding(CommandTest):
|
class TestBuilding(CommandTest):
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
name = settings.BASE_OBJECT_TYPECLASS.rsplit('.', 1)[1]
|
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):
|
def test_examine(self):
|
||||||
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
self.call(building.CmdExamine(), "Obj", "Name/key: Obj")
|
||||||
|
|
||||||
def test_set_obj_alias(self):
|
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):
|
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):
|
def test_attribute_commands(self):
|
||||||
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
|
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
|
||||||
|
|
@ -284,19 +322,36 @@ class TestBuilding(CommandTest):
|
||||||
|
|
||||||
def test_typeclass(self):
|
def test_typeclass(self):
|
||||||
self.call(building.CmdTypeclass(), "Obj = evennia.objects.objects.DefaultExit",
|
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):
|
def test_lock(self):
|
||||||
self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.")
|
self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.")
|
||||||
|
|
||||||
def test_find(self):
|
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):
|
def test_script(self):
|
||||||
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
||||||
|
|
||||||
def test_teleport(self):
|
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 Nonelocation.")
|
||||||
|
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 test_spawn(self):
|
||||||
def getObject(commandTest, objKeyStr):
|
def getObject(commandTest, objKeyStr):
|
||||||
|
|
@ -312,7 +367,7 @@ class TestBuilding(CommandTest):
|
||||||
self.call(building.CmdSpawn(), " ", "Usage: @spawn")
|
self.call(building.CmdSpawn(), " ", "Usage: @spawn")
|
||||||
|
|
||||||
# Tests "@spawn <prototype_dictionary>" without specifying location.
|
# Tests "@spawn <prototype_dictionary>" without specifying location.
|
||||||
self.call(building.CmdSpawn(), \
|
self.call(building.CmdSpawn(),
|
||||||
"{'key':'goblin', 'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin")
|
"{'key':'goblin', 'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin")
|
||||||
goblin = getObject(self, "goblin")
|
goblin = getObject(self, "goblin")
|
||||||
|
|
||||||
|
|
@ -331,8 +386,8 @@ class TestBuilding(CommandTest):
|
||||||
# char1's default location in the future...
|
# char1's default location in the future...
|
||||||
spawnLoc = self.room1
|
spawnLoc = self.room1
|
||||||
|
|
||||||
self.call(building.CmdSpawn(), \
|
self.call(building.CmdSpawn(),
|
||||||
"{'prototype':'GOBLIN', 'key':'goblin', 'location':'%s'}" \
|
"{'prototype':'GOBLIN', 'key':'goblin', 'location':'%s'}"
|
||||||
% spawnLoc.dbref, "Spawned goblin")
|
% spawnLoc.dbref, "Spawned goblin")
|
||||||
goblin = getObject(self, "goblin")
|
goblin = getObject(self, "goblin")
|
||||||
self.assertEqual(goblin.location, spawnLoc)
|
self.assertEqual(goblin.location, spawnLoc)
|
||||||
|
|
@ -345,70 +400,81 @@ class TestBuilding(CommandTest):
|
||||||
self.assertIsInstance(ball, DefaultObject)
|
self.assertIsInstance(ball, DefaultObject)
|
||||||
ball.delete()
|
ball.delete()
|
||||||
|
|
||||||
# Tests "@spawn/noloc ..." without specifying a location.
|
# Tests "@spawn/n ..." without specifying a location.
|
||||||
# Location should be "None".
|
# 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")
|
ball = getObject(self, "Ball")
|
||||||
self.assertIsNone(ball.location)
|
self.assertIsNone(ball.location)
|
||||||
ball.delete()
|
ball.delete()
|
||||||
|
|
||||||
# Tests "@spawn/noloc ...", but DO specify a location.
|
# Tests "@spawn/noloc ...", but DO specify a location.
|
||||||
# Location should be the specified location.
|
# Location should be the specified location.
|
||||||
self.call(building.CmdSpawn(), \
|
self.call(building.CmdSpawn(),
|
||||||
"/noloc {'prototype':'BALL', 'location':'%s'}" \
|
"/noloc {'prototype':'BALL', 'location':'%s'}"
|
||||||
% spawnLoc.dbref, "Spawned Ball")
|
% spawnLoc.dbref, "Spawned Ball")
|
||||||
ball = getObject(self, "Ball")
|
ball = getObject(self, "Ball")
|
||||||
self.assertEqual(ball.location, spawnLoc)
|
self.assertEqual(ball.location, spawnLoc)
|
||||||
ball.delete()
|
ball.delete()
|
||||||
|
|
||||||
# test calling spawn with an invalid prototype.
|
# test calling spawn with an invalid prototype.
|
||||||
self.call(building.CmdSpawn(), \
|
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
||||||
"'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
|
||||||
|
|
||||||
|
|
||||||
class TestComms(CommandTest):
|
class TestComms(CommandTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(CommandTest, self).setUp()
|
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):
|
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)
|
self.call(comms.CmdDelCom(), "tc", "Your alias 'tc' for channel testchan was cleared.", receiver=self.account)
|
||||||
|
|
||||||
def test_channels(self):
|
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):
|
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):
|
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):
|
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):
|
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):
|
def test_cwho(self):
|
||||||
self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestAccount", receiver=self.account)
|
self.call(comms.CmdCWho(), "testchan", "Channel subscriptions\ntestchan:\n TestAccount", receiver=self.account)
|
||||||
|
|
||||||
def test_page(self):
|
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):
|
def test_cboot(self):
|
||||||
# No one else connected to boot
|
# No one else connected to boot
|
||||||
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <account> [:reason]", receiver=self.account)
|
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <account> [:reason]", receiver=self.account)
|
||||||
|
|
||||||
def test_cdestroy(self):
|
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):
|
class TestBatchProcess(CommandTest):
|
||||||
def test_batch_commands(self):
|
def test_batch_commands(self):
|
||||||
# cannot test batchcode here, it must run inside the server process
|
# cannot test batchcode here, it must run inside the server process
|
||||||
self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds", "Running Batchcommand processor Automatic mode for example_batch_cmds")
|
self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds",
|
||||||
|
"Running Batchcommand processor Automatic mode for example_batch_cmds")
|
||||||
# we make sure to delete the button again here to stop the running reactor
|
# we make sure to delete the button again here to stop the running reactor
|
||||||
confirm = building.CmdDestroy.confirm
|
confirm = building.CmdDestroy.confirm
|
||||||
building.CmdDestroy.confirm = False
|
building.CmdDestroy.confirm = False
|
||||||
|
|
@ -431,3 +497,12 @@ class TestInterruptCommand(CommandTest):
|
||||||
def test_interrupt_command(self):
|
def test_interrupt_command(self):
|
||||||
ret = self.call(CmdInterrupt(), "")
|
ret = self.call(CmdInterrupt(), "")
|
||||||
self.assertEqual(ret, "")
|
self.assertEqual(ret, "")
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnconnectedCommand(CommandTest):
|
||||||
|
def test_info_command(self):
|
||||||
|
expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % (
|
||||||
|
settings.SERVERNAME,
|
||||||
|
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
|
||||||
|
SESSIONS.account_count(), utils.get_evennia_version().replace("-", ""))
|
||||||
|
self.call(unloggedin.CmdUnconnectedInfo(), "", expected)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ Commands that are available from the connect screen.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
import datetime
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from random import getrandbits
|
from random import getrandbits
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -11,8 +12,9 @@ from evennia.accounts.models import AccountDB
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.server.models import ServerConfig
|
from evennia.server.models import ServerConfig
|
||||||
from evennia.comms.models import ChannelDB
|
from evennia.comms.models import ChannelDB
|
||||||
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
|
|
||||||
from evennia.utils import create, logger, utils
|
from evennia.utils import create, logger, utils, gametime
|
||||||
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
@ -516,6 +518,24 @@ class CmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS):
|
||||||
self.session.sessionhandler.session_portal_sync(self.session)
|
self.session.sessionhandler.session_portal_sync(self.session)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS):
|
||||||
|
"""
|
||||||
|
Provides MUDINFO output, so that Evennia games can be added to Mudconnector
|
||||||
|
and Mudstats. Sadly, the MUDINFO specification seems to have dropped off the
|
||||||
|
face of the net, but it is still used by some crawlers. This implementation
|
||||||
|
was created by looking at the MUDINFO implementation in MUX2, TinyMUSH, Rhost,
|
||||||
|
and PennMUSH.
|
||||||
|
"""
|
||||||
|
key = "info"
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
self.caller.msg("## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % (
|
||||||
|
settings.SERVERNAME,
|
||||||
|
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
|
||||||
|
SESSIONS.account_count(), utils.get_evennia_version()))
|
||||||
|
|
||||||
|
|
||||||
def _create_account(session, accountname, password, permissions, typeclass=None, email=None):
|
def _create_account(session, accountname, password, permissions, typeclass=None, email=None):
|
||||||
"""
|
"""
|
||||||
Helper function, creates an account of the specified typeclass.
|
Helper function, creates an account of the specified typeclass.
|
||||||
|
|
|
||||||
|
|
@ -264,14 +264,20 @@ class TestCmdSetMergers(TestCase):
|
||||||
# test cmdhandler functions
|
# test cmdhandler functions
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
from twisted.trial.unittest import TestCase as TwistedTestCase
|
from twisted.trial.unittest import TestCase as TwistedTestCase
|
||||||
|
|
||||||
|
|
||||||
|
def _mockdelay(time, func, *args, **kwargs):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
|
class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
|
||||||
"Test the cmdhandler.get_and_merge_cmdsets function."
|
"Test the cmdhandler.get_and_merge_cmdsets function."
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
self.patch(sys.modules['evennia.server.sessionhandler'], 'delay', _mockdelay)
|
||||||
super(TestGetAndMergeCmdSets, self).setUp()
|
super(TestGetAndMergeCmdSets, self).setUp()
|
||||||
self.cmdset_a = _CmdSetA()
|
self.cmdset_a = _CmdSetA()
|
||||||
self.cmdset_b = _CmdSetB()
|
self.cmdset_b = _CmdSetB()
|
||||||
|
|
@ -325,6 +331,7 @@ class TestGetAndMergeCmdSets(TwistedTestCase, EvenniaTest):
|
||||||
a.no_exits = True
|
a.no_exits = True
|
||||||
a.no_channels = True
|
a.no_channels = True
|
||||||
self.set_cmdsets(self.obj1, a, b, c, d)
|
self.set_cmdsets(self.obj1, a, b, c, d)
|
||||||
|
|
||||||
deferred = cmdhandler.get_and_merge_cmdsets(self.obj1, None, None, self.obj1, "object", "")
|
deferred = cmdhandler.get_and_merge_cmdsets(self.obj1, None, None, self.obj1, "object", "")
|
||||||
|
|
||||||
def _callback(cmdset):
|
def _callback(cmdset):
|
||||||
|
|
|
||||||
|
|
@ -159,7 +159,7 @@ class ChannelHandler(object):
|
||||||
"""
|
"""
|
||||||
The ChannelHandler manages all active in-game channels and
|
The ChannelHandler manages all active in-game channels and
|
||||||
dynamically creates channel commands for users so that they can
|
dynamically creates channel commands for users so that they can
|
||||||
just give the channek's key or alias to write to it. Whenever a
|
just give the channel's key or alias to write to it. Whenever a
|
||||||
new channel is created in the database, the update() method on
|
new channel is created in the database, the update() method on
|
||||||
this handler must be called to sync it with the database (this is
|
this handler must be called to sync it with the database (this is
|
||||||
done automatically if creating the channel with
|
done automatically if creating the channel with
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
|
||||||
the hooks called by this method.
|
the hooks called by this method.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
self.basetype_setup()
|
||||||
self.at_channel_creation()
|
self.at_channel_creation()
|
||||||
self.attributes.add("log_file", "channel_%s.log" % self.key)
|
self.attributes.add("log_file", "channel_%s.log" % self.key)
|
||||||
if hasattr(self, "_createdict"):
|
if hasattr(self, "_createdict"):
|
||||||
|
|
@ -46,11 +47,7 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
|
||||||
if cdict.get("desc"):
|
if cdict.get("desc"):
|
||||||
self.attributes.add("desc", cdict["desc"])
|
self.attributes.add("desc", cdict["desc"])
|
||||||
|
|
||||||
def at_channel_creation(self):
|
def basetype_setup(self):
|
||||||
"""
|
|
||||||
Called once, when the channel is first created.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# delayed import of the channelhandler
|
# delayed import of the channelhandler
|
||||||
global _CHANNEL_HANDLER
|
global _CHANNEL_HANDLER
|
||||||
if not _CHANNEL_HANDLER:
|
if not _CHANNEL_HANDLER:
|
||||||
|
|
@ -58,6 +55,15 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
|
||||||
# register ourselves with the channelhandler.
|
# register ourselves with the channelhandler.
|
||||||
_CHANNEL_HANDLER.add(self)
|
_CHANNEL_HANDLER.add(self)
|
||||||
|
|
||||||
|
self.locks.add("send:all();listen:all();control:perm(Admin)")
|
||||||
|
|
||||||
|
def at_channel_creation(self):
|
||||||
|
"""
|
||||||
|
Called once, when the channel is first created.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
# helper methods, for easy overloading
|
# helper methods, for easy overloading
|
||||||
|
|
||||||
def has_connection(self, subscriber):
|
def has_connection(self, subscriber):
|
||||||
|
|
|
||||||
|
|
@ -355,15 +355,16 @@ class ChannelDBManager(TypedObjectManager):
|
||||||
channel (Channel or None): A channel match.
|
channel (Channel or None): A channel match.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# first check the channel key
|
dbref = self.dbref(channelkey)
|
||||||
channels = self.filter(db_key__iexact=channelkey)
|
if dbref:
|
||||||
if not channels:
|
try:
|
||||||
# also check aliases
|
return self.get(id=dbref)
|
||||||
channels = [channel for channel in self.all()
|
except self.model.DoesNotExist:
|
||||||
if channelkey in channel.aliases.all()]
|
pass
|
||||||
if channels:
|
results = self.filter(Q(db_key__iexact=channelkey) |
|
||||||
return channels[0]
|
Q(db_tags__db_tagtype__iexact="alias",
|
||||||
return None
|
db_tags__db_key__iexact=channelkey)).distinct()
|
||||||
|
return results[0] if results else None
|
||||||
|
|
||||||
def get_subscriptions(self, subscriber):
|
def get_subscriptions(self, subscriber):
|
||||||
"""
|
"""
|
||||||
|
|
@ -393,26 +394,20 @@ class ChannelDBManager(TypedObjectManager):
|
||||||
case sensitive) match.
|
case sensitive) match.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
channels = []
|
dbref = self.dbref(ostring)
|
||||||
if not ostring:
|
if dbref:
|
||||||
return channels
|
|
||||||
try:
|
try:
|
||||||
# try an id match first
|
return self.get(id=dbref)
|
||||||
dbref = int(ostring.strip('#'))
|
except self.model.DoesNotExist:
|
||||||
channels = self.filter(id=dbref)
|
|
||||||
except Exception:
|
|
||||||
# Usually because we couldn't convert to int - not a dbref
|
|
||||||
pass
|
pass
|
||||||
if not channels:
|
|
||||||
# no id match. Search on the key.
|
|
||||||
if exact:
|
if exact:
|
||||||
channels = self.filter(db_key__iexact=ostring)
|
channels = self.filter(Q(db_key__iexact=ostring) |
|
||||||
|
Q(db_tags__db_tagtype__iexact="alias",
|
||||||
|
db_tags__db_key__iexact=ostring)).distinct()
|
||||||
else:
|
else:
|
||||||
channels = self.filter(db_key__icontains=ostring)
|
channels = self.filter(Q(db_key__icontains=ostring) |
|
||||||
if not channels:
|
Q(db_tags__db_tagtype__iexact="alias",
|
||||||
# still no match. Search by alias.
|
db_tags__db_key__icontains=ostring)).distinct()
|
||||||
channels = [channel for channel in self.all()
|
|
||||||
if ostring.lower() in [a.lower for a in channel.aliases.all()]]
|
|
||||||
return channels
|
return channels
|
||||||
# back-compatibility alias
|
# back-compatibility alias
|
||||||
channel_search = search_channel
|
channel_search = search_channel
|
||||||
|
|
|
||||||
|
|
@ -584,9 +584,7 @@ class SubscriptionHandler(object):
|
||||||
for obj in self.all():
|
for obj in self.all():
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
try:
|
try:
|
||||||
if hasattr(obj, 'account'):
|
if hasattr(obj, 'account') and obj.account:
|
||||||
if not obj.account:
|
|
||||||
continue
|
|
||||||
obj = obj.account
|
obj = obj.account
|
||||||
if not obj.is_connected:
|
if not obj.is_connected:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ things you want from here into your game folder and change them there.
|
||||||
## Contrib modules
|
## Contrib modules
|
||||||
|
|
||||||
* Barter system (Griatch 2012) - A safe and effective barter-system
|
* Barter system (Griatch 2012) - A safe and effective barter-system
|
||||||
for any game. Allows safe trading of any godds (including coin)
|
for any game. Allows safe trading of any goods (including coin).
|
||||||
* CharGen (Griatch 2011) - A simple Character creator for OOC mode.
|
* CharGen (Griatch 2011) - A simple Character creator for OOC mode.
|
||||||
Meant as a starting point for a more fleshed-out system.
|
Meant as a starting point for a more fleshed-out system.
|
||||||
* Clothing (FlutterSprite 2017) - A layered clothing system with
|
* Clothing (FlutterSprite 2017) - A layered clothing system with
|
||||||
|
|
@ -33,7 +33,7 @@ things you want from here into your game folder and change them there.
|
||||||
on a character and access it in an emote with a custom marker.
|
on a character and access it in an emote with a custom marker.
|
||||||
* Mail (grungies1138 2016) - An in-game mail system for communication.
|
* Mail (grungies1138 2016) - An in-game mail system for communication.
|
||||||
* Menu login (Griatch 2011) - A login system using menus asking
|
* Menu login (Griatch 2011) - A login system using menus asking
|
||||||
for name/password rather than giving them as one command
|
for name/password rather than giving them as one command.
|
||||||
* Map Builder (CloudKeeper 2016) - Build a game area based on a 2D
|
* Map Builder (CloudKeeper 2016) - Build a game area based on a 2D
|
||||||
"graphical" unicode map. Supports assymmetric exits.
|
"graphical" unicode map. Supports assymmetric exits.
|
||||||
* Menu Login (Vincent-lg 2016) - Alternate login system using EvMenu.
|
* Menu Login (Vincent-lg 2016) - Alternate login system using EvMenu.
|
||||||
|
|
@ -60,7 +60,7 @@ things you want from here into your game folder and change them there.
|
||||||
## Contrib packages
|
## Contrib packages
|
||||||
|
|
||||||
* EGI_Client (gtaylor 2016) - Client for reporting game status
|
* EGI_Client (gtaylor 2016) - Client for reporting game status
|
||||||
to the Evennia game index (games.evennia.com)
|
to the Evennia game index (games.evennia.com).
|
||||||
* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script
|
* In-game Python (Vincent Le Goff 2017) - Allow trusted builders to script
|
||||||
objects and events using Python from in-game.
|
objects and events using Python from in-game.
|
||||||
* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant
|
* Turnbattle (FlutterSprite 2017) - A turn-based combat engine meant
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ class ExtendedRoom(DefaultRoom):
|
||||||
key (str): A detail identifier.
|
key (str): A detail identifier.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
detail (str or None): A detail mathing the given key.
|
detail (str or None): A detail matching the given key.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
A detail is a way to offer more things to look at in a room
|
A detail is a way to offer more things to look at in a room
|
||||||
|
|
@ -213,37 +213,39 @@ class ExtendedRoom(DefaultRoom):
|
||||||
return detail
|
return detail
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def return_appearance(self, looker):
|
def return_appearance(self, looker, **kwargs):
|
||||||
"""
|
"""
|
||||||
This is called when e.g. the look command wants to retrieve
|
This is called when e.g. the look command wants to retrieve
|
||||||
the description of this object.
|
the description of this object.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
looker (Object): The object looking at us.
|
looker (Object): The object looking at us.
|
||||||
|
**kwargs (dict): Arbitrary, optional arguments for users
|
||||||
|
overriding the call (unused by default).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
description (str): Our description.
|
description (str): Our description.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
update = False
|
# ensures that our description is current based on time/season
|
||||||
|
self.update_current_description()
|
||||||
|
# run the normal return_appearance method, now that desc is updated.
|
||||||
|
return super(ExtendedRoom, self).return_appearance(looker, **kwargs)
|
||||||
|
|
||||||
|
def update_current_description(self):
|
||||||
|
"""
|
||||||
|
This will update the description of the room if the time or season
|
||||||
|
has changed since last checked.
|
||||||
|
"""
|
||||||
|
update = False
|
||||||
# get current time and season
|
# get current time and season
|
||||||
curr_season, curr_timeslot = self.get_time_and_season()
|
curr_season, curr_timeslot = self.get_time_and_season()
|
||||||
|
|
||||||
# compare with previously stored slots
|
# compare with previously stored slots
|
||||||
last_season = self.ndb.last_season
|
last_season = self.ndb.last_season
|
||||||
last_timeslot = self.ndb.last_timeslot
|
last_timeslot = self.ndb.last_timeslot
|
||||||
|
|
||||||
if curr_season != last_season:
|
if curr_season != last_season:
|
||||||
# season changed. Load new desc, or a fallback.
|
# season changed. Load new desc, or a fallback.
|
||||||
if curr_season == 'spring':
|
new_raw_desc = self.attributes.get("%s_desc" % curr_season)
|
||||||
new_raw_desc = self.db.spring_desc
|
|
||||||
elif curr_season == 'summer':
|
|
||||||
new_raw_desc = self.db.summer_desc
|
|
||||||
elif curr_season == 'autumn':
|
|
||||||
new_raw_desc = self.db.autumn_desc
|
|
||||||
else:
|
|
||||||
new_raw_desc = self.db.winter_desc
|
|
||||||
if new_raw_desc:
|
if new_raw_desc:
|
||||||
raw_desc = new_raw_desc
|
raw_desc = new_raw_desc
|
||||||
else:
|
else:
|
||||||
|
|
@ -252,19 +254,15 @@ class ExtendedRoom(DefaultRoom):
|
||||||
self.db.raw_desc = raw_desc
|
self.db.raw_desc = raw_desc
|
||||||
self.ndb.last_season = curr_season
|
self.ndb.last_season = curr_season
|
||||||
update = True
|
update = True
|
||||||
|
|
||||||
if curr_timeslot != last_timeslot:
|
if curr_timeslot != last_timeslot:
|
||||||
# timeslot changed. Set update flag.
|
# timeslot changed. Set update flag.
|
||||||
self.ndb.last_timeslot = curr_timeslot
|
self.ndb.last_timeslot = curr_timeslot
|
||||||
update = True
|
update = True
|
||||||
|
|
||||||
if update:
|
if update:
|
||||||
# if anything changed we have to re-parse
|
# if anything changed we have to re-parse
|
||||||
# the raw_desc for time markers
|
# the raw_desc for time markers
|
||||||
# and re-save the description again.
|
# and re-save the description again.
|
||||||
self.db.desc = self.replace_timeslots(self.db.raw_desc, curr_timeslot)
|
self.db.desc = self.replace_timeslots(self.db.raw_desc, curr_timeslot)
|
||||||
# run the normal return_appearance method, now that desc is updated.
|
|
||||||
return super(ExtendedRoom, self).return_appearance(looker)
|
|
||||||
|
|
||||||
|
|
||||||
# Custom Look command supporting Room details. Add this to
|
# Custom Look command supporting Room details. Add this to
|
||||||
|
|
@ -369,6 +367,7 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
aliases = ["describe", "detail"]
|
aliases = ["describe", "detail"]
|
||||||
|
switch_options = () # Inherits from default_cmds.CmdDesc, but unused here
|
||||||
|
|
||||||
def reset_times(self, obj):
|
def reset_times(self, obj):
|
||||||
"""By deleteting the caches we force a re-load."""
|
"""By deleteting the caches we force a re-load."""
|
||||||
|
|
|
||||||
|
|
@ -430,7 +430,7 @@ class EventCharacter(DefaultCharacter):
|
||||||
|
|
||||||
# Browse all the room's other characters
|
# Browse all the room's other characters
|
||||||
for obj in location.contents:
|
for obj in location.contents:
|
||||||
if obj is self or not inherits_from(obj, "objects.objects.DefaultCharacter"):
|
if obj is self or not inherits_from(obj, "evennia.objects.objects.DefaultCharacter"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
allow = obj.callbacks.call("can_say", self, obj, message, parameters=message)
|
allow = obj.callbacks.call("can_say", self, obj, message, parameters=message)
|
||||||
|
|
@ -491,7 +491,7 @@ class EventCharacter(DefaultCharacter):
|
||||||
parameters=message)
|
parameters=message)
|
||||||
|
|
||||||
# Call the other characters' "say" event
|
# Call the other characters' "say" event
|
||||||
presents = [obj for obj in location.contents if obj is not self and inherits_from(obj, "objects.objects.DefaultCharacter")]
|
presents = [obj for obj in location.contents if obj is not self and inherits_from(obj, "evennia.objects.objects.DefaultCharacter")]
|
||||||
for present in presents:
|
for present in presents:
|
||||||
present.callbacks.call("say", self, present, message, parameters=message)
|
present.callbacks.call("say", self, present, message, parameters=message)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ class CmdMail(default_cmds.MuxCommand):
|
||||||
index += 1
|
index += 1
|
||||||
|
|
||||||
table.reformat_column(0, width=6)
|
table.reformat_column(0, width=6)
|
||||||
table.reformat_column(1, width=17)
|
table.reformat_column(1, width=18)
|
||||||
table.reformat_column(2, width=34)
|
table.reformat_column(2, width=34)
|
||||||
table.reformat_column(3, width=13)
|
table.reformat_column(3, width=13)
|
||||||
table.reformat_column(4, width=7)
|
table.reformat_column(4, width=7)
|
||||||
|
|
|
||||||
|
|
@ -708,12 +708,15 @@ class RecogHandler(object):
|
||||||
than `max_length`.
|
than `max_length`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not obj.access(self.obj, "enable_recog", default=True):
|
||||||
|
raise SdescError("This person is unrecognizeable.")
|
||||||
|
|
||||||
# strip emote components from recog
|
# strip emote components from recog
|
||||||
recog = _RE_REF.sub(r"\1",
|
recog = _RE_REF.sub(
|
||||||
_RE_REF_LANG.sub(r"\1",
|
r"\1", _RE_REF_LANG.sub(
|
||||||
_RE_SELF_REF.sub(r"",
|
r"\1", _RE_SELF_REF.sub(
|
||||||
_RE_LANGUAGE.sub(r"",
|
r"", _RE_LANGUAGE.sub(
|
||||||
_RE_OBJ_REF_START.sub(r"", recog)))))
|
r"", _RE_OBJ_REF_START.sub(r"", recog)))))
|
||||||
|
|
||||||
# make an recog clean of ANSI codes
|
# make an recog clean of ANSI codes
|
||||||
cleaned_recog = ansi.strip_ansi(recog)
|
cleaned_recog = ansi.strip_ansi(recog)
|
||||||
|
|
@ -1085,7 +1088,7 @@ class CmdMask(RPCommand):
|
||||||
if self.cmdstring == "mask":
|
if self.cmdstring == "mask":
|
||||||
# wear a mask
|
# wear a mask
|
||||||
if not self.args:
|
if not self.args:
|
||||||
caller.msg("Usage: (un)wearmask sdesc")
|
caller.msg("Usage: (un)mask sdesc")
|
||||||
return
|
return
|
||||||
if caller.db.unmasked_sdesc:
|
if caller.db.unmasked_sdesc:
|
||||||
caller.msg("You are already wearing a mask.")
|
caller.msg("You are already wearing a mask.")
|
||||||
|
|
@ -1108,7 +1111,7 @@ class CmdMask(RPCommand):
|
||||||
del caller.db.unmasked_sdesc
|
del caller.db.unmasked_sdesc
|
||||||
caller.locks.remove("enable_recog")
|
caller.locks.remove("enable_recog")
|
||||||
caller.sdesc.add(old_sdesc)
|
caller.sdesc.add(old_sdesc)
|
||||||
caller.msg("You remove your mask and is again '%s'." % old_sdesc)
|
caller.msg("You remove your mask and are again '%s'." % old_sdesc)
|
||||||
|
|
||||||
|
|
||||||
class RPSystemCmdSet(CmdSet):
|
class RPSystemCmdSet(CmdSet):
|
||||||
|
|
@ -1200,7 +1203,7 @@ class ContribRPObject(DefaultObject):
|
||||||
below.
|
below.
|
||||||
exact (bool): if unset (default) - prefers to match to beginning of
|
exact (bool): if unset (default) - prefers to match to beginning of
|
||||||
string rather than not matching at all. If set, requires
|
string rather than not matching at all. If set, requires
|
||||||
exact mathing of entire string.
|
exact matching of entire string.
|
||||||
candidates (list of objects): this is an optional custom list of objects
|
candidates (list of objects): this is an optional custom list of objects
|
||||||
to search (filter) between. It is ignored if `global_search`
|
to search (filter) between. It is ignored if `global_search`
|
||||||
is given. If not set, this list will automatically be defined
|
is given. If not set, this list will automatically be defined
|
||||||
|
|
|
||||||
|
|
@ -798,7 +798,7 @@ from evennia.contrib import talking_npc
|
||||||
class TestTalkingNPC(CommandTest):
|
class TestTalkingNPC(CommandTest):
|
||||||
def test_talkingnpc(self):
|
def test_talkingnpc(self):
|
||||||
npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1)
|
npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1)
|
||||||
self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)|")
|
self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)")
|
||||||
npc.delete()
|
npc.delete()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -824,9 +824,30 @@ class TestTutorialWorldMob(EvenniaTest):
|
||||||
|
|
||||||
|
|
||||||
from evennia.contrib.tutorial_world import objects as tutobjects
|
from evennia.contrib.tutorial_world import objects as tutobjects
|
||||||
|
from mock.mock import MagicMock
|
||||||
|
from twisted.trial.unittest import TestCase as TwistedTestCase
|
||||||
|
|
||||||
|
from twisted.internet.base import DelayedCall
|
||||||
|
DelayedCall.debug = True
|
||||||
|
|
||||||
|
|
||||||
class TestTutorialWorldObjects(CommandTest):
|
def _mockdelay(tim, func, *args, **kwargs):
|
||||||
|
func(*args, **kwargs)
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
|
||||||
|
def _mockdeferLater(reactor, timedelay, callback, *args, **kwargs):
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
return MagicMock()
|
||||||
|
|
||||||
|
|
||||||
|
class TestTutorialWorldObjects(TwistedTestCase, CommandTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.patch(sys.modules['evennia.contrib.tutorial_world.objects'], 'delay', _mockdelay)
|
||||||
|
self.patch(sys.modules['evennia.scripts.taskhandler'], 'deferLater', _mockdeferLater)
|
||||||
|
super(TestTutorialWorldObjects, self).setUp()
|
||||||
|
|
||||||
def test_tutorialobj(self):
|
def test_tutorialobj(self):
|
||||||
obj1 = create_object(tutobjects.TutorialObject, key="tutobj")
|
obj1 = create_object(tutobjects.TutorialObject, key="tutobj")
|
||||||
obj1.reset()
|
obj1.reset()
|
||||||
|
|
@ -848,10 +869,7 @@ class TestTutorialWorldObjects(CommandTest):
|
||||||
|
|
||||||
def test_lightsource(self):
|
def test_lightsource(self):
|
||||||
light = create_object(tutobjects.LightSource, key="torch", location=self.room1)
|
light = create_object(tutobjects.LightSource, key="torch", location=self.room1)
|
||||||
self.call(tutobjects.CmdLight(), "", "You light torch.", obj=light)
|
self.call(tutobjects.CmdLight(), "", "A torch on the floor flickers and dies.|You light torch.", obj=light)
|
||||||
light._burnout()
|
|
||||||
if hasattr(light, "deferred"):
|
|
||||||
light.deferred.cancel()
|
|
||||||
self.assertFalse(light.pk)
|
self.assertFalse(light.pk)
|
||||||
|
|
||||||
def test_crumblingwall(self):
|
def test_crumblingwall(self):
|
||||||
|
|
@ -869,12 +887,12 @@ class TestTutorialWorldObjects(CommandTest):
|
||||||
"You shift the weedy green root upwards.|Holding aside the root you think you notice something behind it ...", obj=wall)
|
"You shift the weedy green root upwards.|Holding aside the root you think you notice something behind it ...", obj=wall)
|
||||||
self.call(tutobjects.CmdPressButton(), "",
|
self.call(tutobjects.CmdPressButton(), "",
|
||||||
"You move your fingers over the suspicious depression, then gives it a decisive push. First", obj=wall)
|
"You move your fingers over the suspicious depression, then gives it a decisive push. First", obj=wall)
|
||||||
self.assertTrue(wall.db.button_exposed)
|
# we patch out the delay, so these are closed immediately
|
||||||
self.assertTrue(wall.db.exit_open)
|
self.assertFalse(wall.db.button_exposed)
|
||||||
|
self.assertFalse(wall.db.exit_open)
|
||||||
wall.reset()
|
wall.reset()
|
||||||
if hasattr(wall, "deferred"):
|
|
||||||
wall.deferred.cancel()
|
|
||||||
wall.delete()
|
wall.delete()
|
||||||
|
return wall.deferred
|
||||||
|
|
||||||
def test_weapon(self):
|
def test_weapon(self):
|
||||||
weapon = create_object(tutobjects.Weapon, key="sword", location=self.char1)
|
weapon = create_object(tutobjects.Weapon, key="sword", location=self.char1)
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,7 @@ from future.utils import listvalues
|
||||||
import random
|
import random
|
||||||
|
|
||||||
from evennia import DefaultObject, DefaultExit, Command, CmdSet
|
from evennia import DefaultObject, DefaultExit, Command, CmdSet
|
||||||
from evennia import utils
|
from evennia.utils import search, delay
|
||||||
from evennia.utils import search
|
|
||||||
from evennia.utils.spawner import spawn
|
from evennia.utils.spawner import spawn
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
@ -373,7 +372,7 @@ class LightSource(TutorialObject):
|
||||||
# start the burn timer. When it runs out, self._burnout
|
# start the burn timer. When it runs out, self._burnout
|
||||||
# will be called. We store the deferred so it can be
|
# will be called. We store the deferred so it can be
|
||||||
# killed in unittesting.
|
# killed in unittesting.
|
||||||
self.deferred = utils.delay(60 * 3, self._burnout)
|
self.deferred = delay(60 * 3, self._burnout)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -645,7 +644,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
||||||
self.db.exit_open = True
|
self.db.exit_open = True
|
||||||
# start a 45 second timer before closing again. We store the deferred so it can be
|
# start a 45 second timer before closing again. We store the deferred so it can be
|
||||||
# killed in unittesting.
|
# killed in unittesting.
|
||||||
self.deferred = utils.delay(45, self.reset)
|
self.deferred = delay(45, self.reset)
|
||||||
|
|
||||||
def _translate_position(self, root, ipos):
|
def _translate_position(self, root, ipos):
|
||||||
"""Translates the position into words"""
|
"""Translates the position into words"""
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ class CmdTutorialLook(default_cmds.CmdLook):
|
||||||
else:
|
else:
|
||||||
# no detail found, delegate our result to the normal
|
# no detail found, delegate our result to the normal
|
||||||
# error message handler.
|
# error message handler.
|
||||||
_SEARCH_AT_RESULT(None, caller, args, looking_at_obj)
|
_SEARCH_AT_RESULT(looking_at_obj, caller, args)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# we found a match, extract it from the list and carry on
|
# we found a match, extract it from the list and carry on
|
||||||
|
|
|
||||||
|
|
@ -34,27 +34,6 @@ from evennia.settings_default import *
|
||||||
# This is the name of your game. Make it catchy!
|
# This is the name of your game. Make it catchy!
|
||||||
SERVERNAME = {servername}
|
SERVERNAME = {servername}
|
||||||
|
|
||||||
# Server ports. If enabled and marked as "visible", the port
|
|
||||||
# should be visible to the outside world on a production server.
|
|
||||||
# Note that there are many more options available beyond these.
|
|
||||||
|
|
||||||
# Telnet ports. Visible.
|
|
||||||
TELNET_ENABLED = True
|
|
||||||
TELNET_PORTS = [4000]
|
|
||||||
# (proxy, internal). Only proxy should be visible.
|
|
||||||
WEBSERVER_ENABLED = True
|
|
||||||
WEBSERVER_PORTS = [(4001, 4002)]
|
|
||||||
# Telnet+SSL ports, for supporting clients. Visible.
|
|
||||||
SSL_ENABLED = False
|
|
||||||
SSL_PORTS = [4003]
|
|
||||||
# SSH client ports. Requires crypto lib. Visible.
|
|
||||||
SSH_ENABLED = False
|
|
||||||
SSH_PORTS = [4004]
|
|
||||||
# Websocket-client port. Visible.
|
|
||||||
WEBSOCKET_CLIENT_ENABLED = True
|
|
||||||
WEBSOCKET_CLIENT_PORT = 4005
|
|
||||||
# Internal Server-Portal port. Not visible.
|
|
||||||
AMP_PORT = 4006
|
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Settings given in secret_settings.py override those in this file.
|
# Settings given in secret_settings.py override those in this file.
|
||||||
|
|
@ -62,4 +41,4 @@ AMP_PORT = 4006
|
||||||
try:
|
try:
|
||||||
from server.conf.secret_settings import *
|
from server.conf.secret_settings import *
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print "secret_settings.py file not found or failed to import."
|
print("secret_settings.py file not found or failed to import.")
|
||||||
|
|
|
||||||
|
|
@ -89,10 +89,14 @@ DefaultLock: Exits: controls who may traverse the exit to
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
|
|
||||||
|
from ast import literal_eval
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.utils import utils
|
from evennia.utils import utils
|
||||||
|
|
||||||
_PERMISSION_HIERARCHY = [pe.lower() for pe in settings.PERMISSION_HIERARCHY]
|
_PERMISSION_HIERARCHY = [pe.lower() for pe in settings.PERMISSION_HIERARCHY]
|
||||||
|
# also accept different plural forms
|
||||||
|
_PERMISSION_HIERARCHY_PLURAL = [pe + 's' if not pe.endswith('s') else pe
|
||||||
|
for pe in _PERMISSION_HIERARCHY]
|
||||||
|
|
||||||
|
|
||||||
def _to_account(accessing_obj):
|
def _to_account(accessing_obj):
|
||||||
|
|
@ -158,49 +162,77 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# this allows the perm_above lockfunc to make use of this function too
|
# this allows the perm_above lockfunc to make use of this function too
|
||||||
gtmode = kwargs.pop("_greater_than", False)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
permission = args[0].lower()
|
permission = args[0].lower()
|
||||||
perms_object = [p.lower() for p in accessing_obj.permissions.all()]
|
perms_object = accessing_obj.permissions.all()
|
||||||
except (AttributeError, IndexError):
|
except (AttributeError, IndexError):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if utils.inherits_from(accessing_obj, "evennia.objects.objects.DefaultObject") and accessing_obj.account:
|
gtmode = kwargs.pop("_greater_than", False)
|
||||||
account = accessing_obj.account
|
is_quell = False
|
||||||
# we strip eventual plural forms, so Builders == Builder
|
|
||||||
perms_account = [p.lower().rstrip("s") for p in account.permissions.all()]
|
account = (utils.inherits_from(accessing_obj, "evennia.objects.objects.DefaultObject") and
|
||||||
|
accessing_obj.account)
|
||||||
|
# check object perms (note that accessing_obj could be an Account too)
|
||||||
|
perms_account = []
|
||||||
|
if account:
|
||||||
|
perms_account = account.permissions.all()
|
||||||
is_quell = account.attributes.get("_quell")
|
is_quell = account.attributes.get("_quell")
|
||||||
|
|
||||||
|
# Check hirarchy matches; handle both singular/plural forms in hierarchy
|
||||||
|
hpos_target = None
|
||||||
if permission in _PERMISSION_HIERARCHY:
|
if permission in _PERMISSION_HIERARCHY:
|
||||||
# check hierarchy without allowing escalation obj->account
|
|
||||||
hpos_target = _PERMISSION_HIERARCHY.index(permission)
|
hpos_target = _PERMISSION_HIERARCHY.index(permission)
|
||||||
hpos_account = [hpos for hpos, hperm in enumerate(_PERMISSION_HIERARCHY) if hperm in perms_account]
|
if permission.endswith('s') and permission[:-1] in _PERMISSION_HIERARCHY:
|
||||||
|
hpos_target = _PERMISSION_HIERARCHY.index(permission[:-1])
|
||||||
|
if hpos_target is not None:
|
||||||
|
# hieratchy match
|
||||||
|
hpos_account = -1
|
||||||
|
hpos_object = -1
|
||||||
|
|
||||||
|
if account:
|
||||||
|
# we have an account puppeting this object. We must check what perms it has
|
||||||
|
perms_account_single = [p[:-1] if p.endswith('s') else p for p in perms_account]
|
||||||
|
hpos_account = [hpos for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
|
||||||
|
if hperm in perms_account_single]
|
||||||
hpos_account = hpos_account and hpos_account[-1] or -1
|
hpos_account = hpos_account and hpos_account[-1] or -1
|
||||||
if is_quell:
|
|
||||||
hpos_object = [hpos for hpos, hperm in enumerate(_PERMISSION_HIERARCHY) if hperm in perms_object]
|
if not account or is_quell:
|
||||||
|
# only get the object-level perms if there is no account or quelling
|
||||||
|
perms_object_single = [p[:-1] if p.endswith('s') else p for p in perms_object]
|
||||||
|
hpos_object = [hpos for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
|
||||||
|
if hperm in perms_object_single]
|
||||||
hpos_object = hpos_object and hpos_object[-1] or -1
|
hpos_object = hpos_object and hpos_object[-1] or -1
|
||||||
|
|
||||||
|
if account and is_quell:
|
||||||
|
# quell mode: use smallest perm from account and object
|
||||||
if gtmode:
|
if gtmode:
|
||||||
return hpos_target < min(hpos_account, hpos_object)
|
return hpos_target < min(hpos_account, hpos_object)
|
||||||
else:
|
else:
|
||||||
return hpos_target <= min(hpos_account, hpos_object)
|
return hpos_target <= min(hpos_account, hpos_object)
|
||||||
elif gtmode:
|
elif account:
|
||||||
|
# use account perm
|
||||||
|
if gtmode:
|
||||||
return hpos_target < hpos_account
|
return hpos_target < hpos_account
|
||||||
else:
|
else:
|
||||||
return hpos_target <= hpos_account
|
return hpos_target <= hpos_account
|
||||||
elif not is_quell and permission in perms_account:
|
else:
|
||||||
# if we get here, check account perms first, otherwise
|
# use object perm
|
||||||
# continue as normal
|
if gtmode:
|
||||||
|
return hpos_target < hpos_object
|
||||||
|
else:
|
||||||
|
return hpos_target <= hpos_object
|
||||||
|
else:
|
||||||
|
# no hierarchy match - check direct matches
|
||||||
|
if account:
|
||||||
|
# account exists, check it first unless quelled
|
||||||
|
if is_quell and permission in perms_object:
|
||||||
|
return True
|
||||||
|
elif permission in perms_account:
|
||||||
|
return True
|
||||||
|
elif permission in perms_object:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if permission in perms_object:
|
|
||||||
# simplest case - we have direct match
|
|
||||||
return True
|
|
||||||
if permission in _PERMISSION_HIERARCHY:
|
|
||||||
# check if we have a higher hierarchy position
|
|
||||||
hpos_target = _PERMISSION_HIERARCHY.index(permission)
|
|
||||||
return any(1 for hpos, hperm in enumerate(_PERMISSION_HIERARCHY)
|
|
||||||
if hperm in perms_object and hpos_target < hpos)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -229,7 +261,6 @@ def pperm(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
return perm(_to_account(accessing_obj), accessed_obj, *args, **kwargs)
|
return perm(_to_account(accessing_obj), accessed_obj, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def pperm_above(accessing_obj, accessed_obj, *args, **kwargs):
|
def pperm_above(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Only allow Account objects with a permission *higher* in the permission
|
Only allow Account objects with a permission *higher* in the permission
|
||||||
|
|
@ -482,7 +513,7 @@ def tag(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
accessing_obj = accessing_obj.obj
|
accessing_obj = accessing_obj.obj
|
||||||
tagkey = args[0] if args else None
|
tagkey = args[0] if args else None
|
||||||
category = args[1] if len(args) > 1 else None
|
category = args[1] if len(args) > 1 else None
|
||||||
return accessing_obj.tags.get(tagkey, category=category)
|
return bool(accessing_obj.tags.get(tagkey, category=category))
|
||||||
|
|
||||||
|
|
||||||
def objtag(accessing_obj, accessed_obj, *args, **kwargs):
|
def objtag(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
|
|
@ -494,7 +525,7 @@ def objtag(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
Only true if accessed_obj has the specified tag and optional
|
Only true if accessed_obj has the specified tag and optional
|
||||||
category.
|
category.
|
||||||
"""
|
"""
|
||||||
return accessed_obj.tags.get(*args)
|
return bool(accessed_obj.tags.get(*args))
|
||||||
|
|
||||||
|
|
||||||
def inside(accessing_obj, accessed_obj, *args, **kwargs):
|
def inside(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
|
|
@ -592,7 +623,9 @@ def serversetting(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
serversetting(IRC_ENABLED)
|
serversetting(IRC_ENABLED)
|
||||||
serversetting(BASE_SCRIPT_PATH, ['types'])
|
serversetting(BASE_SCRIPT_PATH, ['types'])
|
||||||
|
|
||||||
A given True/False or integers will be converted properly.
|
A given True/False or integers will be converted properly. Note that
|
||||||
|
everything will enter this function as strings, so they have to be
|
||||||
|
unpacked to their real value. We only support basic properties.
|
||||||
"""
|
"""
|
||||||
if not args or not args[0]:
|
if not args or not args[0]:
|
||||||
return False
|
return False
|
||||||
|
|
@ -602,12 +635,12 @@ def serversetting(accessing_obj, accessed_obj, *args, **kwargs):
|
||||||
else:
|
else:
|
||||||
setting, val = args[0], args[1]
|
setting, val = args[0], args[1]
|
||||||
# convert
|
# convert
|
||||||
if val == 'True':
|
try:
|
||||||
val = True
|
val = literal_eval(val)
|
||||||
elif val == 'False':
|
except Exception:
|
||||||
val = False
|
# we swallow errors here, lockfuncs has noone to report to
|
||||||
elif val.isdigit():
|
return False
|
||||||
val = int(val)
|
|
||||||
if setting in settings._wrapped.__dict__:
|
if setting in settings._wrapped.__dict__:
|
||||||
return settings._wrapped.__dict__[setting] == val
|
return settings._wrapped.__dict__[setting] == val
|
||||||
return False
|
return False
|
||||||
|
|
|
||||||
|
|
@ -541,6 +541,46 @@ class LockHandler(object):
|
||||||
return all(self._eval_access_type(accessing_obj, locks, access_type) for access_type in locks)
|
return all(self._eval_access_type(accessing_obj, locks, access_type) for access_type in locks)
|
||||||
|
|
||||||
|
|
||||||
|
# convenience access function
|
||||||
|
|
||||||
|
# dummy to be able to call check_lockstring from the outside
|
||||||
|
|
||||||
|
class _ObjDummy:
|
||||||
|
lock_storage = ''
|
||||||
|
|
||||||
|
_LOCK_HANDLER = LockHandler(_ObjDummy())
|
||||||
|
|
||||||
|
|
||||||
|
def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False,
|
||||||
|
default=False, access_type=None):
|
||||||
|
"""
|
||||||
|
Do a direct check against a lockstring ('atype:func()..'),
|
||||||
|
without any intermediary storage on the accessed object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
accessing_obj (object or None): The object seeking access.
|
||||||
|
Importantly, this can be left unset if the lock functions
|
||||||
|
don't access it, no updating or storage of locks are made
|
||||||
|
against this object in this method.
|
||||||
|
lockstring (str): Lock string to check, on the form
|
||||||
|
`"access_type:lock_definition"` where the `access_type`
|
||||||
|
part can potentially be set to a dummy value to just check
|
||||||
|
a lock condition.
|
||||||
|
no_superuser_bypass (bool, optional): Force superusers to heed lock.
|
||||||
|
default (bool, optional): Fallback result to use if `access_type` is set
|
||||||
|
but no such `access_type` is found in the given `lockstring`.
|
||||||
|
access_type (str, bool): If set, only this access_type will be looked up
|
||||||
|
among the locks defined by `lockstring`.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
access (bool): If check is passed or not.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return _LOCK_HANDLER.check_lockstring(
|
||||||
|
accessing_obj, lockstring, no_superuser_bypass=no_superuser_bypass,
|
||||||
|
default=default, access_type=access_type)
|
||||||
|
|
||||||
|
|
||||||
def _test():
|
def _test():
|
||||||
# testing
|
# testing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,10 +11,11 @@ from evennia.utils.test_resources import EvenniaTest
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# this is a special optimized Django version, only available in current Django devel
|
# this is a special optimized Django version, only available in current Django devel
|
||||||
from django.utils.unittest import TestCase
|
from django.utils.unittest import TestCase, override_settings
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from django.test import TestCase
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from evennia import settings_default
|
||||||
from evennia.locks import lockfuncs
|
from evennia.locks import lockfuncs
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
@ -25,7 +26,8 @@ from evennia.locks import lockfuncs
|
||||||
class TestLockCheck(EvenniaTest):
|
class TestLockCheck(EvenniaTest):
|
||||||
def testrun(self):
|
def testrun(self):
|
||||||
dbref = self.obj2.dbref
|
dbref = self.obj2.dbref
|
||||||
self.obj1.locks.add("owner:dbref(%s);edit:dbref(%s) or perm(Admin);examine:perm(Builder) and id(%s);delete:perm(Admin);get:all()" % (dbref, dbref, dbref))
|
self.obj1.locks.add("owner:dbref(%s);edit:dbref(%s) or perm(Admin);examine:perm(Builder) "
|
||||||
|
"and id(%s);delete:perm(Admin);get:all()" % (dbref, dbref, dbref))
|
||||||
self.obj2.permissions.add('Admin')
|
self.obj2.permissions.add('Admin')
|
||||||
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'owner'))
|
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'owner'))
|
||||||
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'edit'))
|
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'edit'))
|
||||||
|
|
@ -36,20 +38,154 @@ class TestLockCheck(EvenniaTest):
|
||||||
self.assertEquals(False, self.obj1.locks.check(self.obj2, 'get'))
|
self.assertEquals(False, self.obj1.locks.check(self.obj2, 'get'))
|
||||||
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'not_exist', default=True))
|
self.assertEquals(True, self.obj1.locks.check(self.obj2, 'not_exist', default=True))
|
||||||
|
|
||||||
|
|
||||||
class TestLockfuncs(EvenniaTest):
|
class TestLockfuncs(EvenniaTest):
|
||||||
def testrun(self):
|
def setUp(self):
|
||||||
|
super(TestLockfuncs, self).setUp()
|
||||||
|
self.account2.permissions.add('Admin')
|
||||||
|
self.char2.permissions.add('Builder')
|
||||||
|
|
||||||
|
def test_booleans(self):
|
||||||
|
self.assertEquals(True, lockfuncs.true(self.account2, self.obj1))
|
||||||
|
self.assertEquals(True, lockfuncs.all(self.account2, self.obj1))
|
||||||
|
self.assertEquals(False, lockfuncs.false(self.account2, self.obj1))
|
||||||
|
self.assertEquals(False, lockfuncs.none(self.account2, self.obj1))
|
||||||
|
self.assertEquals(True, lockfuncs.self(self.obj1, self.obj1))
|
||||||
|
self.assertEquals(True, lockfuncs.self(self.account, self.account))
|
||||||
|
self.assertEquals(False, lockfuncs.superuser(self.account, None))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_account_perm(self):
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.account2, None, 'foo'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.account2, None, 'Developer'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.account2, None, 'Developers'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.account2, None, 'Admin'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.account2, None, 'Admins'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.account2, None, 'Player'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.account2, None, 'Players'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.account2, None, 'Builder'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.account2, None, 'Builders'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.account2, None, 'Builder'))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_puppet_perm(self):
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.char2, None, 'foo'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.char2, None, 'Developer'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.char2, None, 'Develoeprs'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Admin'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Admins'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Player'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Players'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Builder'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Builders'))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_account_perm_above(self):
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.char2, None, 'Builder'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.char2, None, 'Builders'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.char2, None, 'Player'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm_above(self.char2, None, 'Admin'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm_above(self.char2, None, 'Admins'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm_above(self.char2, None, 'Developers'))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_quell_perm(self):
|
||||||
|
self.account2.db._quell = True
|
||||||
|
self.assertEquals(False, lockfuncs.false(self.char2, None))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.char2, None, 'Developer'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.char2, None, 'Developers'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.char2, None, 'Admin'))
|
||||||
|
self.assertEquals(False, lockfuncs.perm(self.char2, None, 'Admins'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Player'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Players'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Builder'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.char2, None, 'Builders'))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_quell_above_perm(self):
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.char2, None, 'Player'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.char2, None, 'Builder'))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_object_perm(self):
|
||||||
self.obj2.permissions.add('Admin')
|
self.obj2.permissions.add('Admin')
|
||||||
self.assertEquals(True, lockfuncs.true(self.obj2, self.obj1))
|
self.assertEquals(False, lockfuncs.perm(self.obj2, None, 'Developer'))
|
||||||
self.assertEquals(False, lockfuncs.false(self.obj2, self.obj1))
|
self.assertEquals(False, lockfuncs.perm(self.obj2, None, 'Developers'))
|
||||||
self.assertEquals(True, lockfuncs.perm(self.obj2, self.obj1, 'Admin'))
|
self.assertEquals(True, lockfuncs.perm(self.obj2, None, 'Admin'))
|
||||||
self.assertEquals(True, lockfuncs.perm_above(self.obj2, self.obj1, 'Builder'))
|
self.assertEquals(True, lockfuncs.perm(self.obj2, None, 'Admins'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.obj2, None, 'Player'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.obj2, None, 'Players'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.obj2, None, 'Builder'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm(self.obj2, None, 'Builders'))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_object_above_perm(self):
|
||||||
|
self.obj2.permissions.add('Admin')
|
||||||
|
self.assertEquals(False, lockfuncs.perm_above(self.obj2, None, 'Admins'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.obj2, None, 'Builder'))
|
||||||
|
self.assertEquals(True, lockfuncs.perm_above(self.obj2, None, 'Builders'))
|
||||||
|
|
||||||
|
@override_settings(PERMISSION_HIERARCHY=settings_default.PERMISSION_HIERARCHY)
|
||||||
|
def test_pperm(self):
|
||||||
|
self.obj2.permissions.add('Developer')
|
||||||
|
self.char2.permissions.add('Developer')
|
||||||
|
self.assertEquals(False, lockfuncs.pperm(self.obj2, None, 'Players'))
|
||||||
|
self.assertEquals(True, lockfuncs.pperm(self.char2, None, 'Players'))
|
||||||
|
self.assertEquals(True, lockfuncs.pperm(self.account, None, 'Admins'))
|
||||||
|
self.assertEquals(True, lockfuncs.pperm_above(self.account, None, 'Builders'))
|
||||||
|
self.assertEquals(False, lockfuncs.pperm_above(self.account2, None, 'Admins'))
|
||||||
|
self.assertEquals(True, lockfuncs.pperm_above(self.char2, None, 'Players'))
|
||||||
|
|
||||||
|
def test_dbref(self):
|
||||||
dbref = self.obj2.dbref
|
dbref = self.obj2.dbref
|
||||||
self.assertEquals(True, lockfuncs.dbref(self.obj2, self.obj1, '%s' % dbref))
|
self.assertEquals(True, lockfuncs.dbref(self.obj2, None, '%s' % dbref))
|
||||||
|
self.assertEquals(False, lockfuncs.id(self.obj2, None, '%s' % (dbref + '1')))
|
||||||
|
dbref = self.account2.dbref
|
||||||
|
self.assertEquals(True, lockfuncs.pdbref(self.account2, None, '%s' % dbref))
|
||||||
|
self.assertEquals(False, lockfuncs.pid(self.account2, None, '%s' % (dbref + '1')))
|
||||||
|
|
||||||
|
def test_attr(self):
|
||||||
self.obj2.db.testattr = 45
|
self.obj2.db.testattr = 45
|
||||||
self.assertEquals(True, lockfuncs.attr(self.obj2, self.obj1, 'testattr', '45'))
|
self.assertEquals(True, lockfuncs.attr(self.obj2, None, 'testattr', '45'))
|
||||||
self.assertEquals(False, lockfuncs.attr_gt(self.obj2, self.obj1, 'testattr', '45'))
|
self.assertEquals(False, lockfuncs.attr_gt(self.obj2, None, 'testattr', '45'))
|
||||||
self.assertEquals(True, lockfuncs.attr_ge(self.obj2, self.obj1, 'testattr', '45'))
|
self.assertEquals(True, lockfuncs.attr_ge(self.obj2, None, 'testattr', '45'))
|
||||||
self.assertEquals(False, lockfuncs.attr_lt(self.obj2, self.obj1, 'testattr', '45'))
|
self.assertEquals(False, lockfuncs.attr_lt(self.obj2, None, 'testattr', '45'))
|
||||||
self.assertEquals(True, lockfuncs.attr_le(self.obj2, self.obj1, 'testattr', '45'))
|
self.assertEquals(True, lockfuncs.attr_le(self.obj2, None, 'testattr', '45'))
|
||||||
self.assertEquals(False, lockfuncs.attr_ne(self.obj2, self.obj1, 'testattr', '45'))
|
|
||||||
|
self.assertEquals(True, lockfuncs.objattr(None, self.obj2, 'testattr', '45'))
|
||||||
|
self.assertEquals(True, lockfuncs.objattr(None, self.obj2, 'testattr', '45'))
|
||||||
|
self.assertEquals(False, lockfuncs.objattr(None, self.obj2, 'testattr', '45', compare='lt'))
|
||||||
|
|
||||||
|
def test_locattr(self):
|
||||||
|
self.obj2.location.db.locattr = 'test'
|
||||||
|
self.assertEquals(True, lockfuncs.locattr(self.obj2, None, 'locattr', 'test'))
|
||||||
|
self.assertEquals(False, lockfuncs.locattr(self.obj2, None, 'fail', 'testfail'))
|
||||||
|
self.assertEquals(True, lockfuncs.objlocattr(None, self.obj2, 'locattr', 'test'))
|
||||||
|
|
||||||
|
def test_tag(self):
|
||||||
|
self.obj2.tags.add("test1")
|
||||||
|
self.obj2.tags.add("test2", "category1")
|
||||||
|
self.assertEquals(True, lockfuncs.tag(self.obj2, None, 'test1'))
|
||||||
|
self.assertEquals(True, lockfuncs.tag(self.obj2, None, 'test2', 'category1'))
|
||||||
|
self.assertEquals(False, lockfuncs.tag(self.obj2, None, 'test1', 'category1'))
|
||||||
|
self.assertEquals(False, lockfuncs.tag(self.obj2, None, 'test1', 'category2'))
|
||||||
|
self.assertEquals(True, lockfuncs.objtag(None, self.obj2, 'test2', 'category1'))
|
||||||
|
self.assertEquals(False, lockfuncs.objtag(None, self.obj2, 'test2'))
|
||||||
|
|
||||||
|
def test_inside_holds(self):
|
||||||
|
self.assertEquals(True, lockfuncs.inside(self.char1, self.room1))
|
||||||
|
self.assertEquals(False, lockfuncs.inside(self.char1, self.room2))
|
||||||
|
self.assertEquals(True, lockfuncs.holds(self.room1, self.char1))
|
||||||
|
self.assertEquals(False, lockfuncs.holds(self.room2, self.char1))
|
||||||
|
|
||||||
|
def test_has_account(self):
|
||||||
|
self.assertEquals(True, lockfuncs.has_account(self.char1, None))
|
||||||
|
self.assertEquals(False, lockfuncs.has_account(self.obj1, None))
|
||||||
|
|
||||||
|
@override_settings(IRC_ENABLED=True, TESTVAL=[1, 2, 3])
|
||||||
|
def test_serversetting(self):
|
||||||
|
# import pudb
|
||||||
|
# pudb.set_trace()
|
||||||
|
self.assertEquals(True, lockfuncs.serversetting(None, None, 'IRC_ENABLED', 'True'))
|
||||||
|
self.assertEquals(True, lockfuncs.serversetting(None, None, 'TESTVAL', '[1, 2, 3]'))
|
||||||
|
self.assertEquals(False, lockfuncs.serversetting(None, None, 'TESTVAL', '[1, 2, 4]'))
|
||||||
|
self.assertEquals(False, lockfuncs.serversetting(None, None, 'TESTVAL', '123'))
|
||||||
|
|
|
||||||
|
|
@ -76,10 +76,14 @@ class ObjectDBManager(TypedObjectManager):
|
||||||
# simplest case - search by dbref
|
# simplest case - search by dbref
|
||||||
dbref = self.dbref(ostring)
|
dbref = self.dbref(ostring)
|
||||||
if dbref:
|
if dbref:
|
||||||
return dbref
|
try:
|
||||||
|
return self.get(id=dbref)
|
||||||
|
except self.model.DoesNotExist:
|
||||||
|
pass
|
||||||
|
|
||||||
# not a dbref. Search by name.
|
# not a dbref. Search by name.
|
||||||
cand_restriction = candidates is not None and Q(pk__in=[_GA(obj, "id") for obj in make_iter(candidates)
|
cand_restriction = candidates is not None and Q(
|
||||||
if obj]) or Q()
|
pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
|
||||||
if exact:
|
if exact:
|
||||||
return self.filter(cand_restriction & Q(db_account__username__iexact=ostring))
|
return self.filter(cand_restriction & Q(db_account__username__iexact=ostring))
|
||||||
else: # fuzzy matching
|
else: # fuzzy matching
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ entities.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
import inflect
|
||||||
from builtins import object
|
from builtins import object
|
||||||
from future.utils import with_metaclass
|
from future.utils import with_metaclass
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
@ -22,9 +24,11 @@ from evennia.commands import cmdhandler
|
||||||
from evennia.utils import search
|
from evennia.utils import search
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils.utils import (variable_from_module, lazy_property,
|
from evennia.utils.utils import (variable_from_module, lazy_property,
|
||||||
make_iter, to_unicode, is_iter)
|
make_iter, to_unicode, is_iter, list_to_string,
|
||||||
|
to_str)
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
|
_INFLECT = inflect.engine()
|
||||||
_MULTISESSION_MODE = settings.MULTISESSION_MODE
|
_MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||||
|
|
||||||
_ScriptDB = None
|
_ScriptDB = None
|
||||||
|
|
@ -206,6 +210,14 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
def sessions(self):
|
def sessions(self):
|
||||||
return ObjectSessionHandler(self)
|
return ObjectSessionHandler(self)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_connected(self):
|
||||||
|
# we get an error for objects subscribed to channels without this
|
||||||
|
if self.account: # seems sane to pass on the account
|
||||||
|
return self.account.is_connected
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_account(self):
|
def has_account(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -281,9 +293,39 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
return "{}(#{})".format(self.name, self.id)
|
return "{}(#{})".format(self.name, self.id)
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
def get_numbered_name(self, count, looker, **kwargs):
|
||||||
|
"""
|
||||||
|
Return the numbered (singular, plural) forms of this object's key. This is by default called
|
||||||
|
by return_appearance and is used for grouping multiple same-named of this object. Note that
|
||||||
|
this will be called on *every* member of a group even though the plural name will be only
|
||||||
|
shown once. Also the singular display version, such as 'an apple', 'a tree' is determined
|
||||||
|
from this method.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
count (int): Number of objects of this type
|
||||||
|
looker (Object): Onlooker. Not used by default.
|
||||||
|
Kwargs:
|
||||||
|
key (str): Optional key to pluralize, use this instead of the object's key.
|
||||||
|
Returns:
|
||||||
|
singular (str): The singular form to display.
|
||||||
|
plural (str): The determined plural form of the key, including the count.
|
||||||
|
"""
|
||||||
|
key = kwargs.get("key", self.key)
|
||||||
|
plural = _INFLECT.plural(key, 2)
|
||||||
|
plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural)
|
||||||
|
singular = _INFLECT.an(key)
|
||||||
|
if not self.aliases.get(plural, category="plural_key"):
|
||||||
|
# we need to wipe any old plurals/an/a in case key changed in the interrim
|
||||||
|
self.aliases.clear(category="plural_key")
|
||||||
|
self.aliases.add(plural, category="plural_key")
|
||||||
|
# save the singular form as an alias here too so we can display "an egg" and also
|
||||||
|
# look at 'an egg'.
|
||||||
|
self.aliases.add(singular, category="plural_key")
|
||||||
|
return singular, plural
|
||||||
|
|
||||||
def search(self, searchdata,
|
def search(self, searchdata,
|
||||||
global_search=False,
|
global_search=False,
|
||||||
use_nicks=True, # should this default to off?
|
use_nicks=True,
|
||||||
typeclass=None,
|
typeclass=None,
|
||||||
location=None,
|
location=None,
|
||||||
attribute_name=None,
|
attribute_name=None,
|
||||||
|
|
@ -335,7 +377,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
below.
|
below.
|
||||||
exact (bool): if unset (default) - prefers to match to beginning of
|
exact (bool): if unset (default) - prefers to match to beginning of
|
||||||
string rather than not matching at all. If set, requires
|
string rather than not matching at all. If set, requires
|
||||||
exact mathing of entire string.
|
exact matching of entire string.
|
||||||
candidates (list of objects): this is an optional custom list of objects
|
candidates (list of objects): this is an optional custom list of objects
|
||||||
to search (filter) between. It is ignored if `global_search`
|
to search (filter) between. It is ignored if `global_search`
|
||||||
is given. If not set, this list will automatically be defined
|
is given. If not set, this list will automatically be defined
|
||||||
|
|
@ -519,6 +561,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
obj.at_msg_send(text=text, to_obj=self, **kwargs)
|
obj.at_msg_send(text=text, to_obj=self, **kwargs)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
|
kwargs["options"] = options
|
||||||
try:
|
try:
|
||||||
if not self.at_msg_receive(text=text, **kwargs):
|
if not self.at_msg_receive(text=text, **kwargs):
|
||||||
# if at_msg_receive returns false, we abort message to this object
|
# if at_msg_receive returns false, we abort message to this object
|
||||||
|
|
@ -526,12 +569,20 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
|
|
||||||
kwargs["options"] = options
|
if text is not None:
|
||||||
|
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||||
|
# sanitize text before sending across the wire
|
||||||
|
try:
|
||||||
|
text = to_str(text, force_string=True)
|
||||||
|
except Exception:
|
||||||
|
text = repr(text)
|
||||||
|
kwargs['text'] = text
|
||||||
|
|
||||||
# relay to session(s)
|
# relay to session(s)
|
||||||
sessions = make_iter(session) if session else self.sessions.all()
|
sessions = make_iter(session) if session else self.sessions.all()
|
||||||
for session in sessions:
|
for session in sessions:
|
||||||
session.data_out(text=text, **kwargs)
|
session.data_out(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
def for_contents(self, func, exclude=None, **kwargs):
|
def for_contents(self, func, exclude=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1433,7 +1484,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
# get and identify all objects
|
# get and identify all objects
|
||||||
visible = (con for con in self.contents if con != looker and
|
visible = (con for con in self.contents if con != looker and
|
||||||
con.access(looker, "view"))
|
con.access(looker, "view"))
|
||||||
exits, users, things = [], [], []
|
exits, users, things = [], [], defaultdict(list)
|
||||||
for con in visible:
|
for con in visible:
|
||||||
key = con.get_display_name(looker)
|
key = con.get_display_name(looker)
|
||||||
if con.destination:
|
if con.destination:
|
||||||
|
|
@ -1441,16 +1492,28 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
elif con.has_account:
|
elif con.has_account:
|
||||||
users.append("|c%s|n" % key)
|
users.append("|c%s|n" % key)
|
||||||
else:
|
else:
|
||||||
things.append(key)
|
# things can be pluralized
|
||||||
|
things[key].append(con)
|
||||||
# get description, build string
|
# get description, build string
|
||||||
string = "|c%s|n\n" % self.get_display_name(looker)
|
string = "|c%s|n\n" % self.get_display_name(looker)
|
||||||
desc = self.db.desc
|
desc = self.db.desc
|
||||||
if desc:
|
if desc:
|
||||||
string += "%s" % desc
|
string += "%s" % desc
|
||||||
if exits:
|
if exits:
|
||||||
string += "\n|wExits:|n " + ", ".join(exits)
|
string += "\n|wExits:|n " + list_to_string(exits)
|
||||||
if users or things:
|
if users or things:
|
||||||
string += "\n|wYou see:|n " + ", ".join(users + things)
|
# handle pluralization of things (never pluralize users)
|
||||||
|
thing_strings = []
|
||||||
|
for key, itemlist in sorted(things.iteritems()):
|
||||||
|
nitem = len(itemlist)
|
||||||
|
if nitem == 1:
|
||||||
|
key, _ = itemlist[0].get_numbered_name(nitem, looker, key=key)
|
||||||
|
else:
|
||||||
|
key = [item.get_numbered_name(nitem, looker, key=key)[1] for item in itemlist][0]
|
||||||
|
thing_strings.append(key)
|
||||||
|
|
||||||
|
string += "\n|wYou see:|n " + list_to_string(users + thing_strings)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def at_look(self, target, **kwargs):
|
def at_look(self, target, **kwargs):
|
||||||
|
|
@ -1705,7 +1768,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
for recv in receivers) if receivers else None,
|
for recv in receivers) if receivers else None,
|
||||||
"speech": message}
|
"speech": message}
|
||||||
self_mapping.update(custom_mapping)
|
self_mapping.update(custom_mapping)
|
||||||
self.msg(text=(msg_self.format(**self_mapping), {"type": msg_type}))
|
self.msg(text=(msg_self.format(**self_mapping), {"type": msg_type}), from_obj=self)
|
||||||
|
|
||||||
if receivers and msg_receivers:
|
if receivers and msg_receivers:
|
||||||
receiver_mapping = {"self": "You",
|
receiver_mapping = {"self": "You",
|
||||||
|
|
@ -1723,7 +1786,8 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
for recv in receivers) if receivers else None}
|
for recv in receivers) if receivers else None}
|
||||||
receiver_mapping.update(individual_mapping)
|
receiver_mapping.update(individual_mapping)
|
||||||
receiver_mapping.update(custom_mapping)
|
receiver_mapping.update(custom_mapping)
|
||||||
receiver.msg(text=(msg_receivers.format(**receiver_mapping), {"type": msg_type}))
|
receiver.msg(text=(msg_receivers.format(**receiver_mapping),
|
||||||
|
{"type": msg_type}), from_obj=self)
|
||||||
|
|
||||||
if self.location and msg_location:
|
if self.location and msg_location:
|
||||||
location_mapping = {"self": "You",
|
location_mapping = {"self": "You",
|
||||||
|
|
@ -1811,7 +1875,7 @@ class DefaultCharacter(DefaultObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.msg("\nYou become |c%s|n.\n" % self.name)
|
self.msg("\nYou become |c%s|n.\n" % self.name)
|
||||||
self.msg(self.at_look(self.location))
|
self.msg((self.at_look(self.location), {'type':'look'}), options = None)
|
||||||
|
|
||||||
def message(obj, from_obj):
|
def message(obj, from_obj):
|
||||||
obj.msg("%s has entered the game." % self.get_display_name(obj), from_obj=from_obj)
|
obj.msg("%s has entered the game." % self.get_display_name(obj), from_obj=from_obj)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ Module containing the task handler for Evennia deferred tasks, persistent or not
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from twisted.internet import reactor, task
|
from twisted.internet import reactor
|
||||||
|
from twisted.internet.task import deferLater
|
||||||
from evennia.server.models import ServerConfig
|
from evennia.server.models import ServerConfig
|
||||||
from evennia.utils.logger import log_err
|
from evennia.utils.logger import log_err
|
||||||
from evennia.utils.dbserialize import dbserialize, dbunserialize
|
from evennia.utils.dbserialize import dbserialize, dbunserialize
|
||||||
|
|
@ -143,7 +144,7 @@ class TaskHandler(object):
|
||||||
args = [task_id]
|
args = [task_id]
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
|
|
||||||
return task.deferLater(reactor, timedelay, callback, *args, **kwargs)
|
return deferLater(reactor, timedelay, callback, *args, **kwargs)
|
||||||
|
|
||||||
def remove(self, task_id):
|
def remove(self, task_id):
|
||||||
"""Remove a persistent task without executing it.
|
"""Remove a persistent task without executing it.
|
||||||
|
|
@ -189,7 +190,7 @@ class TaskHandler(object):
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
for task_id, (date, callbac, args, kwargs) in self.tasks.items():
|
for task_id, (date, callbac, args, kwargs) in self.tasks.items():
|
||||||
seconds = max(0, (date - now).total_seconds())
|
seconds = max(0, (date - now).total_seconds())
|
||||||
task.deferLater(reactor, seconds, self.do_task, task_id)
|
deferLater(reactor, seconds, self.do_task, task_id)
|
||||||
|
|
||||||
|
|
||||||
# Create the soft singleton
|
# Create the soft singleton
|
||||||
|
|
|
||||||
|
|
@ -1,670 +0,0 @@
|
||||||
"""
|
|
||||||
Contains the protocols, commands, and client factory needed for the Server
|
|
||||||
and Portal to communicate with each other, letting Portal work as a proxy.
|
|
||||||
Both sides use this same protocol.
|
|
||||||
|
|
||||||
The separation works like this:
|
|
||||||
|
|
||||||
Portal - (AMP client) handles protocols. It contains a list of connected
|
|
||||||
sessions in a dictionary for identifying the respective account
|
|
||||||
connected. If it loses the AMP connection it will automatically
|
|
||||||
try to reconnect.
|
|
||||||
|
|
||||||
Server - (AMP server) Handles all mud operations. The server holds its own list
|
|
||||||
of sessions tied to account objects. This is synced against the portal
|
|
||||||
at startup and when a session connects/disconnects
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
# imports needed on both server and portal side
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from collections import defaultdict, namedtuple
|
|
||||||
from itertools import count
|
|
||||||
from cStringIO import StringIO
|
|
||||||
try:
|
|
||||||
import cPickle as pickle
|
|
||||||
except ImportError:
|
|
||||||
import pickle
|
|
||||||
from twisted.protocols import amp
|
|
||||||
from twisted.internet import protocol
|
|
||||||
from twisted.internet.defer import Deferred
|
|
||||||
from evennia.utils import logger
|
|
||||||
from evennia.utils.utils import to_str, variable_from_module
|
|
||||||
import zlib # Used in Compressed class
|
|
||||||
|
|
||||||
DUMMYSESSION = namedtuple('DummySession', ['sessid'])(0)
|
|
||||||
|
|
||||||
# communication bits
|
|
||||||
# (chr(9) and chr(10) are \t and \n, so skipping them)
|
|
||||||
|
|
||||||
PCONN = chr(1) # portal session connect
|
|
||||||
PDISCONN = chr(2) # portal session disconnect
|
|
||||||
PSYNC = chr(3) # portal session sync
|
|
||||||
SLOGIN = chr(4) # server session login
|
|
||||||
SDISCONN = chr(5) # server session disconnect
|
|
||||||
SDISCONNALL = chr(6) # server session disconnect all
|
|
||||||
SSHUTD = chr(7) # server shutdown
|
|
||||||
SSYNC = chr(8) # server session sync
|
|
||||||
SCONN = chr(11) # server creating new connection (for irc bots and etc)
|
|
||||||
PCONNSYNC = chr(12) # portal post-syncing a session
|
|
||||||
PDISCONNALL = chr(13) # portal session disconnect all
|
|
||||||
AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed)
|
|
||||||
|
|
||||||
BATCH_RATE = 250 # max commands/sec before switching to batch-sending
|
|
||||||
BATCH_TIMEOUT = 0.5 # how often to poll to empty batch queue, in seconds
|
|
||||||
|
|
||||||
# buffers
|
|
||||||
_SENDBATCH = defaultdict(list)
|
|
||||||
_MSGBUFFER = defaultdict(list)
|
|
||||||
|
|
||||||
|
|
||||||
def get_restart_mode(restart_file):
|
|
||||||
"""
|
|
||||||
Parse the server/portal restart status
|
|
||||||
|
|
||||||
Args:
|
|
||||||
restart_file (str): Path to restart.dat file.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
restart_mode (bool): If the file indicates the server is in
|
|
||||||
restart mode or not.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if os.path.exists(restart_file):
|
|
||||||
flag = open(restart_file, 'r').read()
|
|
||||||
return flag == "True"
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class AmpServerFactory(protocol.ServerFactory):
|
|
||||||
"""
|
|
||||||
This factory creates the Server as a new AMPProtocol instance for accepting
|
|
||||||
connections from the Portal.
|
|
||||||
"""
|
|
||||||
noisy = False
|
|
||||||
|
|
||||||
def __init__(self, server):
|
|
||||||
"""
|
|
||||||
Initialize the factory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
server (Server): The Evennia server service instance.
|
|
||||||
protocol (Protocol): The protocol the factory creates
|
|
||||||
instances of.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.server = server
|
|
||||||
self.protocol = AMPProtocol
|
|
||||||
|
|
||||||
def buildProtocol(self, addr):
|
|
||||||
"""
|
|
||||||
Start a new connection, and store it on the service object.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
addr (str): Connection address. Not used.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
protocol (Protocol): The created protocol.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.server.amp_protocol = AMPProtocol()
|
|
||||||
self.server.amp_protocol.factory = self
|
|
||||||
return self.server.amp_protocol
|
|
||||||
|
|
||||||
|
|
||||||
class AmpClientFactory(protocol.ReconnectingClientFactory):
|
|
||||||
"""
|
|
||||||
This factory creates an instance of the Portal, an AMPProtocol
|
|
||||||
instances to use to connect
|
|
||||||
|
|
||||||
"""
|
|
||||||
# Initial reconnect delay in seconds.
|
|
||||||
initialDelay = 1
|
|
||||||
factor = 1.5
|
|
||||||
maxDelay = 1
|
|
||||||
noisy = False
|
|
||||||
|
|
||||||
def __init__(self, portal):
|
|
||||||
"""
|
|
||||||
Initializes the client factory.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
portal (Portal): Portal instance.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.portal = portal
|
|
||||||
self.protocol = AMPProtocol
|
|
||||||
|
|
||||||
def startedConnecting(self, connector):
|
|
||||||
"""
|
|
||||||
Called when starting to try to connect to the MUD server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
connector (Connector): Twisted Connector instance representing
|
|
||||||
this connection.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def buildProtocol(self, addr):
|
|
||||||
"""
|
|
||||||
Creates an AMPProtocol instance when connecting to the server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
addr (str): Connection address. Not used.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.resetDelay()
|
|
||||||
self.portal.amp_protocol = AMPProtocol()
|
|
||||||
self.portal.amp_protocol.factory = self
|
|
||||||
return self.portal.amp_protocol
|
|
||||||
|
|
||||||
def clientConnectionLost(self, connector, reason):
|
|
||||||
"""
|
|
||||||
Called when the AMP connection to the MUD server is lost.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
connector (Connector): Twisted Connector instance representing
|
|
||||||
this connection.
|
|
||||||
reason (str): Eventual text describing why connection was lost.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if hasattr(self, "server_restart_mode"):
|
|
||||||
self.portal.sessions.announce_all(" Server restarting ...")
|
|
||||||
self.maxDelay = 2
|
|
||||||
else:
|
|
||||||
# Don't translate this; avoid loading django on portal side.
|
|
||||||
self.maxDelay = 10
|
|
||||||
self.portal.sessions.announce_all(" ... Portal lost connection to Server.")
|
|
||||||
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
|
|
||||||
|
|
||||||
def clientConnectionFailed(self, connector, reason):
|
|
||||||
"""
|
|
||||||
Called when an AMP connection attempt to the MUD server fails.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
connector (Connector): Twisted Connector instance representing
|
|
||||||
this connection.
|
|
||||||
reason (str): Eventual text describing why connection failed.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if hasattr(self, "server_restart_mode"):
|
|
||||||
self.maxDelay = 2
|
|
||||||
else:
|
|
||||||
self.maxDelay = 10
|
|
||||||
self.portal.sessions.announce_all(" ...")
|
|
||||||
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
|
|
||||||
|
|
||||||
|
|
||||||
# AMP Communication Command types
|
|
||||||
|
|
||||||
class Compressed(amp.String):
|
|
||||||
"""
|
|
||||||
This is a customn AMP command Argument that both handles too-long
|
|
||||||
sends as well as uses zlib for compression across the wire. The
|
|
||||||
batch-grouping of too-long sends is borrowed from the "mediumbox"
|
|
||||||
recipy at twisted-hacks's ~glyph/+junk/amphacks/mediumbox.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
def fromBox(self, name, strings, objects, proto):
|
|
||||||
"""
|
|
||||||
Converts from box representation to python. We
|
|
||||||
group very long data into batches.
|
|
||||||
"""
|
|
||||||
value = StringIO()
|
|
||||||
value.write(strings.get(name))
|
|
||||||
for counter in count(2):
|
|
||||||
# count from 2 upwards
|
|
||||||
chunk = strings.get("%s.%d" % (name, counter))
|
|
||||||
if chunk is None:
|
|
||||||
break
|
|
||||||
value.write(chunk)
|
|
||||||
objects[name] = value.getvalue()
|
|
||||||
|
|
||||||
def toBox(self, name, strings, objects, proto):
|
|
||||||
"""
|
|
||||||
Convert from data to box. We handled too-long
|
|
||||||
batched data and put it together here.
|
|
||||||
"""
|
|
||||||
value = StringIO(objects[name])
|
|
||||||
strings[name] = value.read(AMP_MAXLEN)
|
|
||||||
for counter in count(2):
|
|
||||||
chunk = value.read(AMP_MAXLEN)
|
|
||||||
if not chunk:
|
|
||||||
break
|
|
||||||
strings["%s.%d" % (name, counter)] = chunk
|
|
||||||
|
|
||||||
def toString(self, inObject):
|
|
||||||
"""
|
|
||||||
Convert to send on the wire, with compression.
|
|
||||||
"""
|
|
||||||
return zlib.compress(inObject, 9)
|
|
||||||
|
|
||||||
def fromString(self, inString):
|
|
||||||
"""
|
|
||||||
Convert (decompress) from the wire to Python.
|
|
||||||
"""
|
|
||||||
return zlib.decompress(inString)
|
|
||||||
|
|
||||||
|
|
||||||
class MsgPortal2Server(amp.Command):
|
|
||||||
"""
|
|
||||||
Message Portal -> Server
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "MsgPortal2Server"
|
|
||||||
arguments = [('packed_data', Compressed())]
|
|
||||||
errors = {Exception: 'EXCEPTION'}
|
|
||||||
response = []
|
|
||||||
|
|
||||||
|
|
||||||
class MsgServer2Portal(amp.Command):
|
|
||||||
"""
|
|
||||||
Message Server -> Portal
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "MsgServer2Portal"
|
|
||||||
arguments = [('packed_data', Compressed())]
|
|
||||||
errors = {Exception: 'EXCEPTION'}
|
|
||||||
response = []
|
|
||||||
|
|
||||||
|
|
||||||
class AdminPortal2Server(amp.Command):
|
|
||||||
"""
|
|
||||||
Administration Portal -> Server
|
|
||||||
|
|
||||||
Sent when the portal needs to perform admin operations on the
|
|
||||||
server, such as when a new session connects or resyncs
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "AdminPortal2Server"
|
|
||||||
arguments = [('packed_data', Compressed())]
|
|
||||||
errors = {Exception: 'EXCEPTION'}
|
|
||||||
response = []
|
|
||||||
|
|
||||||
|
|
||||||
class AdminServer2Portal(amp.Command):
|
|
||||||
"""
|
|
||||||
Administration Server -> Portal
|
|
||||||
|
|
||||||
Sent when the server needs to perform admin operations on the
|
|
||||||
portal.
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "AdminServer2Portal"
|
|
||||||
arguments = [('packed_data', Compressed())]
|
|
||||||
errors = {Exception: 'EXCEPTION'}
|
|
||||||
response = []
|
|
||||||
|
|
||||||
|
|
||||||
class FunctionCall(amp.Command):
|
|
||||||
"""
|
|
||||||
Bidirectional Server <-> Portal
|
|
||||||
|
|
||||||
Sent when either process needs to call an arbitrary function in
|
|
||||||
the other. This does not use the batch-send functionality.
|
|
||||||
|
|
||||||
"""
|
|
||||||
key = "FunctionCall"
|
|
||||||
arguments = [('module', amp.String()),
|
|
||||||
('function', amp.String()),
|
|
||||||
('args', amp.String()),
|
|
||||||
('kwargs', amp.String())]
|
|
||||||
errors = {Exception: 'EXCEPTION'}
|
|
||||||
response = [('result', amp.String())]
|
|
||||||
|
|
||||||
|
|
||||||
# Helper functions for pickling.
|
|
||||||
|
|
||||||
def dumps(data):
|
|
||||||
return to_str(pickle.dumps(to_str(data), pickle.HIGHEST_PROTOCOL))
|
|
||||||
|
|
||||||
|
|
||||||
def loads(data):
|
|
||||||
return pickle.loads(to_str(data))
|
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
# Core AMP protocol for communication Server <-> Portal
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
|
|
||||||
class AMPProtocol(amp.AMP):
|
|
||||||
"""
|
|
||||||
This is the protocol that the MUD server and the proxy server
|
|
||||||
communicate to each other with. AMP is a bi-directional protocol,
|
|
||||||
so both the proxy and the MUD use the same commands and protocol.
|
|
||||||
|
|
||||||
AMP specifies responder methods here and connect them to
|
|
||||||
amp.Command subclasses that specify the datatypes of the
|
|
||||||
input/output of these methods.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# helper methods
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Initialize protocol with some things that need to be in place
|
|
||||||
already before connecting both on portal and server.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.send_batch_counter = 0
|
|
||||||
self.send_reset_time = time.time()
|
|
||||||
self.send_mode = True
|
|
||||||
self.send_task = None
|
|
||||||
|
|
||||||
def connectionMade(self):
|
|
||||||
"""
|
|
||||||
This is called when an AMP connection is (re-)established
|
|
||||||
between server and portal. AMP calls it on both sides, so we
|
|
||||||
need to make sure to only trigger resync from the portal side.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# this makes for a factor x10 faster sends across the wire
|
|
||||||
self.transport.setTcpNoDelay(True)
|
|
||||||
|
|
||||||
if hasattr(self.factory, "portal"):
|
|
||||||
# only the portal has the 'portal' property, so we know we are
|
|
||||||
# on the portal side and can initialize the connection.
|
|
||||||
sessdata = self.factory.portal.sessions.get_all_sync_data()
|
|
||||||
self.send_AdminPortal2Server(DUMMYSESSION,
|
|
||||||
PSYNC,
|
|
||||||
sessiondata=sessdata)
|
|
||||||
self.factory.portal.sessions.at_server_connection()
|
|
||||||
if hasattr(self.factory, "server_restart_mode"):
|
|
||||||
del self.factory.server_restart_mode
|
|
||||||
|
|
||||||
def connectionLost(self, reason):
|
|
||||||
"""
|
|
||||||
We swallow connection errors here. The reason is that during a
|
|
||||||
normal reload/shutdown there will almost always be cases where
|
|
||||||
either the portal or server shuts down before a message has
|
|
||||||
returned its (empty) return, triggering a connectionLost error
|
|
||||||
that is irrelevant. If a true connection error happens, the
|
|
||||||
portal will continuously try to reconnect, showing the problem
|
|
||||||
that way.
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Error handling
|
|
||||||
|
|
||||||
def errback(self, e, info):
|
|
||||||
"""
|
|
||||||
Error callback.
|
|
||||||
Handles errors to avoid dropping connections on server tracebacks.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
e (Failure): Deferred error instance.
|
|
||||||
info (str): Error string.
|
|
||||||
|
|
||||||
"""
|
|
||||||
e.trap(Exception)
|
|
||||||
logger.log_err("AMP Error for %(info)s: %(e)s" % {'info': info,
|
|
||||||
'e': e.getErrorMessage()})
|
|
||||||
|
|
||||||
def send_data(self, command, sessid, **kwargs):
|
|
||||||
"""
|
|
||||||
Send data across the wire.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
command (AMP Command): A protocol send command.
|
|
||||||
sessid (int): A unique Session id.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
deferred (deferred or None): A deferred with an errback.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
Data will be sent across the wire pickled as a tuple
|
|
||||||
(sessid, kwargs).
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.callRemote(command,
|
|
||||||
packed_data=dumps((sessid, kwargs))
|
|
||||||
).addErrback(self.errback, command.key)
|
|
||||||
|
|
||||||
# Message definition + helper methods to call/create each message type
|
|
||||||
|
|
||||||
# Portal -> Server Msg
|
|
||||||
|
|
||||||
@MsgPortal2Server.responder
|
|
||||||
def server_receive_msgportal2server(self, packed_data):
|
|
||||||
"""
|
|
||||||
Receives message arriving to server. This method is executed
|
|
||||||
on the Server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
packed_data (str): Data to receive (a pickled tuple (sessid,kwargs))
|
|
||||||
|
|
||||||
"""
|
|
||||||
sessid, kwargs = loads(packed_data)
|
|
||||||
session = self.factory.server.sessions.get(sessid, None)
|
|
||||||
if session:
|
|
||||||
self.factory.server.sessions.data_in(session, **kwargs)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def send_MsgPortal2Server(self, session, **kwargs):
|
|
||||||
"""
|
|
||||||
Access method called by the Portal and executed on the Portal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (session): Session
|
|
||||||
kwargs (any, optional): Optional data.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
deferred (Deferred): Asynchronous return.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.send_data(MsgPortal2Server, session.sessid, **kwargs)
|
|
||||||
|
|
||||||
# Server -> Portal message
|
|
||||||
|
|
||||||
@MsgServer2Portal.responder
|
|
||||||
def portal_receive_server2portal(self, packed_data):
|
|
||||||
"""
|
|
||||||
Receives message arriving to Portal from Server.
|
|
||||||
This method is executed on the Portal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
packed_data (str): Pickled data (sessid, kwargs) coming over the wire.
|
|
||||||
"""
|
|
||||||
sessid, kwargs = loads(packed_data)
|
|
||||||
session = self.factory.portal.sessions.get(sessid, None)
|
|
||||||
if session:
|
|
||||||
self.factory.portal.sessions.data_out(session, **kwargs)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def send_MsgServer2Portal(self, session, **kwargs):
|
|
||||||
"""
|
|
||||||
Access method - executed on the Server for sending data
|
|
||||||
to Portal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (Session): Unique Session.
|
|
||||||
kwargs (any, optiona): Extra data.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.send_data(MsgServer2Portal, session.sessid, **kwargs)
|
|
||||||
|
|
||||||
# Server administration from the Portal side
|
|
||||||
@AdminPortal2Server.responder
|
|
||||||
def server_receive_adminportal2server(self, packed_data):
|
|
||||||
"""
|
|
||||||
Receives admin data from the Portal (allows the portal to
|
|
||||||
perform admin operations on the server). This is executed on
|
|
||||||
the Server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
packed_data (str): Incoming, pickled data.
|
|
||||||
|
|
||||||
"""
|
|
||||||
sessid, kwargs = loads(packed_data)
|
|
||||||
operation = kwargs.pop("operation", "")
|
|
||||||
server_sessionhandler = self.factory.server.sessions
|
|
||||||
|
|
||||||
if operation == PCONN: # portal_session_connect
|
|
||||||
# create a new session and sync it
|
|
||||||
server_sessionhandler.portal_connect(kwargs.get("sessiondata"))
|
|
||||||
|
|
||||||
elif operation == PCONNSYNC: # portal_session_sync
|
|
||||||
server_sessionhandler.portal_session_sync(kwargs.get("sessiondata"))
|
|
||||||
|
|
||||||
elif operation == PDISCONN: # portal_session_disconnect
|
|
||||||
# session closed from portal sid
|
|
||||||
session = server_sessionhandler.get(sessid)
|
|
||||||
if session:
|
|
||||||
server_sessionhandler.portal_disconnect(session)
|
|
||||||
|
|
||||||
elif operation == PDISCONNALL: # portal_disconnect_all
|
|
||||||
# portal orders all sessions to close
|
|
||||||
server_sessionhandler.portal_disconnect_all()
|
|
||||||
|
|
||||||
elif operation == PSYNC: # portal_session_sync
|
|
||||||
# force a resync of sessions when portal reconnects to
|
|
||||||
# server (e.g. after a server reboot) the data kwarg
|
|
||||||
# contains a dict {sessid: {arg1:val1,...}}
|
|
||||||
# representing the attributes to sync for each
|
|
||||||
# session.
|
|
||||||
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
|
|
||||||
else:
|
|
||||||
raise Exception("operation %(op)s not recognized." % {'op': operation})
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def send_AdminPortal2Server(self, session, operation="", **kwargs):
|
|
||||||
"""
|
|
||||||
Send Admin instructions from the Portal to the Server.
|
|
||||||
Executed
|
|
||||||
on the Portal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (Session): Session.
|
|
||||||
operation (char, optional): Identifier for the server operation, as defined by the
|
|
||||||
global variables in `evennia/server/amp.py`.
|
|
||||||
data (str or dict, optional): Data used in the administrative operation.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.send_data(AdminPortal2Server, session.sessid, operation=operation, **kwargs)
|
|
||||||
|
|
||||||
# Portal administration from the Server side
|
|
||||||
|
|
||||||
@AdminServer2Portal.responder
|
|
||||||
def portal_receive_adminserver2portal(self, packed_data):
|
|
||||||
"""
|
|
||||||
|
|
||||||
Receives and handles admin operations sent to the Portal
|
|
||||||
This is executed on the Portal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
packed_data (str): Data received, a pickled tuple (sessid, kwargs).
|
|
||||||
|
|
||||||
"""
|
|
||||||
sessid, kwargs = loads(packed_data)
|
|
||||||
operation = kwargs.pop("operation")
|
|
||||||
portal_sessionhandler = self.factory.portal.sessions
|
|
||||||
|
|
||||||
if operation == SLOGIN: # server_session_login
|
|
||||||
# a session has authenticated; sync it.
|
|
||||||
session = portal_sessionhandler.get(sessid)
|
|
||||||
if session:
|
|
||||||
portal_sessionhandler.server_logged_in(session, kwargs.get("sessiondata"))
|
|
||||||
|
|
||||||
elif operation == SDISCONN: # server_session_disconnect
|
|
||||||
# the server is ordering to disconnect the session
|
|
||||||
session = portal_sessionhandler.get(sessid)
|
|
||||||
if session:
|
|
||||||
portal_sessionhandler.server_disconnect(session, reason=kwargs.get("reason"))
|
|
||||||
|
|
||||||
elif operation == SDISCONNALL: # server_session_disconnect_all
|
|
||||||
# server orders all sessions to disconnect
|
|
||||||
portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
|
|
||||||
|
|
||||||
elif operation == SSHUTD: # server_shutdown
|
|
||||||
# the server orders the portal to shut down
|
|
||||||
self.factory.portal.shutdown(restart=False)
|
|
||||||
|
|
||||||
elif operation == SSYNC: # server_session_sync
|
|
||||||
# server wants to save session data to the portal,
|
|
||||||
# maybe because it's about to shut down.
|
|
||||||
portal_sessionhandler.server_session_sync(kwargs.get("sessiondata"),
|
|
||||||
kwargs.get("clean", True))
|
|
||||||
# set a flag in case we are about to shut down soon
|
|
||||||
self.factory.server_restart_mode = True
|
|
||||||
|
|
||||||
elif operation == SCONN: # server_force_connection (for irc/etc)
|
|
||||||
portal_sessionhandler.server_connect(**kwargs)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise Exception("operation %(op)s not recognized." % {'op': operation})
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def send_AdminServer2Portal(self, session, operation="", **kwargs):
|
|
||||||
"""
|
|
||||||
Administrative access method called by the Server to send an
|
|
||||||
instruction to the Portal.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
session (Session): Session.
|
|
||||||
operation (char, optional): Identifier for the server
|
|
||||||
operation, as defined by the global variables in
|
|
||||||
`evennia/server/amp.py`.
|
|
||||||
data (str or dict, optional): Data going into the adminstrative.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.send_data(AdminServer2Portal, session.sessid, operation=operation, **kwargs)
|
|
||||||
|
|
||||||
# Extra functions
|
|
||||||
|
|
||||||
@FunctionCall.responder
|
|
||||||
def receive_functioncall(self, module, function, func_args, func_kwargs):
|
|
||||||
"""
|
|
||||||
This allows Portal- and Server-process to call an arbitrary
|
|
||||||
function in the other process. It is intended for use by
|
|
||||||
plugin modules.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
module (str or module): The module containing the
|
|
||||||
`function` to call.
|
|
||||||
function (str): The name of the function to call in
|
|
||||||
`module`.
|
|
||||||
func_args (str): Pickled args tuple for use in `function` call.
|
|
||||||
func_kwargs (str): Pickled kwargs dict for use in `function` call.
|
|
||||||
|
|
||||||
"""
|
|
||||||
args = loads(func_args)
|
|
||||||
kwargs = loads(func_kwargs)
|
|
||||||
|
|
||||||
# call the function (don't catch tracebacks here)
|
|
||||||
result = variable_from_module(module, function)(*args, **kwargs)
|
|
||||||
|
|
||||||
if isinstance(result, Deferred):
|
|
||||||
# if result is a deferred, attach handler to properly
|
|
||||||
# wrap the return value
|
|
||||||
result.addCallback(lambda r: {"result": dumps(r)})
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
return {'result': dumps(result)}
|
|
||||||
|
|
||||||
def send_FunctionCall(self, modulepath, functionname, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Access method called by either process. This will call an arbitrary
|
|
||||||
function on the other process (On Portal if calling from Server and
|
|
||||||
vice versa).
|
|
||||||
|
|
||||||
Inputs:
|
|
||||||
modulepath (str) - python path to module holding function to call
|
|
||||||
functionname (str) - name of function in given module
|
|
||||||
*args, **kwargs will be used as arguments/keyword args for the
|
|
||||||
remote function call
|
|
||||||
Returns:
|
|
||||||
A deferred that fires with the return value of the remote
|
|
||||||
function call
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.callRemote(FunctionCall,
|
|
||||||
module=modulepath,
|
|
||||||
function=functionname,
|
|
||||||
args=dumps(args),
|
|
||||||
kwargs=dumps(kwargs)).addCallback(
|
|
||||||
lambda r: loads(r["result"])).addErrback(self.errback, "FunctionCall")
|
|
||||||
239
evennia/server/amp_client.py
Normal file
239
evennia/server/amp_client.py
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
"""
|
||||||
|
The Evennia Server service acts as an AMP-client when talking to the
|
||||||
|
Portal. This module sets up the Client-side communication.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from evennia.server.portal import amp
|
||||||
|
from twisted.internet import protocol
|
||||||
|
from evennia.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
class AMPClientFactory(protocol.ReconnectingClientFactory):
|
||||||
|
"""
|
||||||
|
This factory creates an instance of an AMP client connection. This handles communication from
|
||||||
|
the be the Evennia 'Server' service to the 'Portal'. The client will try to auto-reconnect on a
|
||||||
|
connection error.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Initial reconnect delay in seconds.
|
||||||
|
initialDelay = 1
|
||||||
|
factor = 1.5
|
||||||
|
maxDelay = 1
|
||||||
|
noisy = False
|
||||||
|
|
||||||
|
def __init__(self, server):
|
||||||
|
"""
|
||||||
|
Initializes the client factory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
server (server): server instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.server = server
|
||||||
|
self.protocol = AMPServerClientProtocol
|
||||||
|
self.maxDelay = 10
|
||||||
|
# not really used unless connecting to multiple servers, but
|
||||||
|
# avoids having to check for its existence on the protocol
|
||||||
|
self.broadcasts = []
|
||||||
|
|
||||||
|
def startedConnecting(self, connector):
|
||||||
|
"""
|
||||||
|
Called when starting to try to connect to the MUD server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
connector (Connector): Twisted Connector instance representing
|
||||||
|
this connection.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
"""
|
||||||
|
Creates an AMPProtocol instance when connecting to the server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr (str): Connection address. Not used.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.resetDelay()
|
||||||
|
self.server.amp_protocol = AMPServerClientProtocol()
|
||||||
|
self.server.amp_protocol.factory = self
|
||||||
|
return self.server.amp_protocol
|
||||||
|
|
||||||
|
def clientConnectionLost(self, connector, reason):
|
||||||
|
"""
|
||||||
|
Called when the AMP connection to the MUD server is lost.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
connector (Connector): Twisted Connector instance representing
|
||||||
|
this connection.
|
||||||
|
reason (str): Eventual text describing why connection was lost.
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.log_info("Server disconnected from the portal.")
|
||||||
|
protocol.ReconnectingClientFactory.clientConnectionLost(self, connector, reason)
|
||||||
|
|
||||||
|
def clientConnectionFailed(self, connector, reason):
|
||||||
|
"""
|
||||||
|
Called when an AMP connection attempt to the MUD server fails.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
connector (Connector): Twisted Connector instance representing
|
||||||
|
this connection.
|
||||||
|
reason (str): Eventual text describing why connection failed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
logger.log_msg("Attempting to reconnect to Portal ...")
|
||||||
|
protocol.ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)
|
||||||
|
|
||||||
|
|
||||||
|
class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
|
||||||
|
"""
|
||||||
|
This protocol describes the Server service (acting as an AMP-client)'s communication with the
|
||||||
|
Portal (which acts as the AMP-server)
|
||||||
|
|
||||||
|
"""
|
||||||
|
# sending AMP data
|
||||||
|
|
||||||
|
def connectionMade(self):
|
||||||
|
"""
|
||||||
|
Called when a new connection is established.
|
||||||
|
|
||||||
|
"""
|
||||||
|
info_dict = self.factory.server.get_info_dict()
|
||||||
|
super(AMPServerClientProtocol, self).connectionMade()
|
||||||
|
# first thing we do is to request the Portal to sync all sessions
|
||||||
|
# back with the Server side. We also need the startup mode (reload, reset, shutdown)
|
||||||
|
self.send_AdminServer2Portal(
|
||||||
|
amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict)
|
||||||
|
|
||||||
|
def data_to_portal(self, command, sessid, **kwargs):
|
||||||
|
"""
|
||||||
|
Send data across the wire to the Portal
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command (AMP Command): A protocol send command.
|
||||||
|
sessid (int): A unique Session id.
|
||||||
|
kwargs (any): Any data to pickle into the command.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
deferred (deferred or None): A deferred with an errback.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Data will be sent across the wire pickled as a tuple
|
||||||
|
(sessid, kwargs).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.callRemote(command, packed_data=amp.dumps((sessid, kwargs))).addErrback(
|
||||||
|
self.errback, command.key)
|
||||||
|
|
||||||
|
def send_MsgServer2Portal(self, session, **kwargs):
|
||||||
|
"""
|
||||||
|
Access method - executed on the Server for sending data
|
||||||
|
to Portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (Session): Unique Session.
|
||||||
|
kwargs (any, optiona): Extra data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.data_to_portal(amp.MsgServer2Portal, session.sessid, **kwargs)
|
||||||
|
|
||||||
|
def send_AdminServer2Portal(self, session, operation="", **kwargs):
|
||||||
|
"""
|
||||||
|
Administrative access method called by the Server to send an
|
||||||
|
instruction to the Portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (Session): Session.
|
||||||
|
operation (char, optional): Identifier for the server
|
||||||
|
operation, as defined by the global variables in
|
||||||
|
`evennia/server/amp.py`.
|
||||||
|
kwargs (dict, optional): Data going into the adminstrative.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.data_to_portal(amp.AdminServer2Portal, session.sessid,
|
||||||
|
operation=operation, **kwargs)
|
||||||
|
|
||||||
|
# receiving AMP data
|
||||||
|
|
||||||
|
@amp.MsgStatus.responder
|
||||||
|
def server_receive_status(self, question):
|
||||||
|
return {"status": "OK"}
|
||||||
|
|
||||||
|
@amp.MsgPortal2Server.responder
|
||||||
|
@amp.catch_traceback
|
||||||
|
def server_receive_msgportal2server(self, packed_data):
|
||||||
|
"""
|
||||||
|
Receives message arriving to server. This method is executed
|
||||||
|
on the Server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packed_data (str): Data to receive (a pickled tuple (sessid,kwargs))
|
||||||
|
|
||||||
|
"""
|
||||||
|
sessid, kwargs = self.data_in(packed_data)
|
||||||
|
session = self.factory.server.sessions.get(sessid, None)
|
||||||
|
if session:
|
||||||
|
self.factory.server.sessions.data_in(session, **kwargs)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@amp.AdminPortal2Server.responder
|
||||||
|
@amp.catch_traceback
|
||||||
|
def server_receive_adminportal2server(self, packed_data):
|
||||||
|
"""
|
||||||
|
Receives admin data from the Portal (allows the portal to
|
||||||
|
perform admin operations on the server). This is executed on
|
||||||
|
the Server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packed_data (str): Incoming, pickled data.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sessid, kwargs = self.data_in(packed_data)
|
||||||
|
operation = kwargs.pop("operation", "")
|
||||||
|
server_sessionhandler = self.factory.server.sessions
|
||||||
|
|
||||||
|
if operation == amp.PCONN: # portal_session_connect
|
||||||
|
# create a new session and sync it
|
||||||
|
server_sessionhandler.portal_connect(kwargs.get("sessiondata"))
|
||||||
|
|
||||||
|
elif operation == amp.PCONNSYNC: # portal_session_sync
|
||||||
|
server_sessionhandler.portal_session_sync(kwargs.get("sessiondata"))
|
||||||
|
|
||||||
|
elif operation == amp.PDISCONN: # portal_session_disconnect
|
||||||
|
# session closed from portal sid
|
||||||
|
session = server_sessionhandler.get(sessid)
|
||||||
|
if session:
|
||||||
|
server_sessionhandler.portal_disconnect(session)
|
||||||
|
|
||||||
|
elif operation == amp.PDISCONNALL: # portal_disconnect_all
|
||||||
|
# portal orders all sessions to close
|
||||||
|
server_sessionhandler.portal_disconnect_all()
|
||||||
|
|
||||||
|
elif operation == amp.PSYNC: # portal_session_sync
|
||||||
|
# force a resync of sessions from the portal side. This happens on
|
||||||
|
# first server-connect.
|
||||||
|
server_restart_mode = kwargs.get("server_restart_mode", "shutdown")
|
||||||
|
self.factory.server.run_init_hooks(server_restart_mode)
|
||||||
|
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
|
||||||
|
|
||||||
|
elif operation == amp.SRELOAD: # server reload
|
||||||
|
# shut down in reload mode
|
||||||
|
server_sessionhandler.all_sessions_portal_sync()
|
||||||
|
server_sessionhandler.server.shutdown(mode='reload')
|
||||||
|
|
||||||
|
elif operation == amp.SRESET:
|
||||||
|
# shut down in reset mode
|
||||||
|
server_sessionhandler.all_sessions_portal_sync()
|
||||||
|
server_sessionhandler.server.shutdown(mode='reset')
|
||||||
|
|
||||||
|
elif operation == amp.SSHUTD: # server shutdown
|
||||||
|
# shutdown in stop mode
|
||||||
|
server_sessionhandler.server.shutdown(mode='shutdown')
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("operation %(op)s not recognized." % {'op': operation})
|
||||||
|
return {}
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,357 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
"""
|
|
||||||
|
|
||||||
This runner is controlled by the evennia launcher and should normally
|
|
||||||
not be launched directly. It manages the two main Evennia processes
|
|
||||||
(Server and Portal) and most importantly runs a passive, threaded loop
|
|
||||||
that makes sure to restart Server whenever it shuts down.
|
|
||||||
|
|
||||||
Since twistd does not allow for returning an optional exit code we
|
|
||||||
need to handle the current reload state for server and portal with
|
|
||||||
flag-files instead. The files, one each for server and portal either
|
|
||||||
contains True or False indicating if the process should be restarted
|
|
||||||
upon returning, or not. A process returning != 0 will always stop, no
|
|
||||||
matter the value of this file.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import print_function
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from argparse import ArgumentParser
|
|
||||||
from subprocess import Popen
|
|
||||||
import Queue
|
|
||||||
import thread
|
|
||||||
import evennia
|
|
||||||
|
|
||||||
try:
|
|
||||||
# check if launched with pypy
|
|
||||||
import __pypy__ as is_pypy
|
|
||||||
except ImportError:
|
|
||||||
is_pypy = False
|
|
||||||
|
|
||||||
SERVER = None
|
|
||||||
PORTAL = None
|
|
||||||
|
|
||||||
EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
||||||
EVENNIA_BIN = os.path.join(EVENNIA_ROOT, "bin")
|
|
||||||
EVENNIA_LIB = os.path.dirname(evennia.__file__)
|
|
||||||
|
|
||||||
SERVER_PY_FILE = os.path.join(EVENNIA_LIB, 'server', 'server.py')
|
|
||||||
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, 'server', 'portal', 'portal.py')
|
|
||||||
|
|
||||||
GAMEDIR = None
|
|
||||||
SERVERDIR = "server"
|
|
||||||
SERVER_PIDFILE = None
|
|
||||||
PORTAL_PIDFILE = None
|
|
||||||
SERVER_RESTART = None
|
|
||||||
PORTAL_RESTART = None
|
|
||||||
SERVER_LOGFILE = None
|
|
||||||
PORTAL_LOGFILE = None
|
|
||||||
HTTP_LOGFILE = None
|
|
||||||
PPROFILER_LOGFILE = None
|
|
||||||
SPROFILER_LOGFILE = None
|
|
||||||
|
|
||||||
# messages
|
|
||||||
|
|
||||||
CMDLINE_HELP = \
|
|
||||||
"""
|
|
||||||
This program manages the running Evennia processes. It is called
|
|
||||||
by evennia and should not be started manually. Its main task is to
|
|
||||||
sit and watch the Server and restart it whenever the user reloads.
|
|
||||||
The runner depends on four files for its operation, two PID files
|
|
||||||
and two RESTART files for Server and Portal respectively; these
|
|
||||||
are stored in the game's server/ directory.
|
|
||||||
"""
|
|
||||||
|
|
||||||
PROCESS_ERROR = \
|
|
||||||
"""
|
|
||||||
{component} process error: {traceback}.
|
|
||||||
"""
|
|
||||||
|
|
||||||
PROCESS_IOERROR = \
|
|
||||||
"""
|
|
||||||
{component} IOError: {traceback}
|
|
||||||
One possible explanation is that 'twistd' was not found.
|
|
||||||
"""
|
|
||||||
|
|
||||||
PROCESS_RESTART = "{component} restarting ..."
|
|
||||||
|
|
||||||
PROCESS_DOEXIT = "Deferring to external runner."
|
|
||||||
|
|
||||||
# Functions
|
|
||||||
|
|
||||||
|
|
||||||
def set_restart_mode(restart_file, flag="reload"):
|
|
||||||
"""
|
|
||||||
This sets a flag file for the restart mode.
|
|
||||||
"""
|
|
||||||
with open(restart_file, 'w') as f:
|
|
||||||
f.write(str(flag))
|
|
||||||
|
|
||||||
|
|
||||||
def getenv():
|
|
||||||
"""
|
|
||||||
Get current environment and add PYTHONPATH
|
|
||||||
"""
|
|
||||||
sep = ";" if os.name == "nt" else ":"
|
|
||||||
env = os.environ.copy()
|
|
||||||
sys.path.insert(0, GAMEDIR)
|
|
||||||
env['PYTHONPATH'] = sep.join(sys.path)
|
|
||||||
return env
|
|
||||||
|
|
||||||
|
|
||||||
def get_restart_mode(restart_file):
|
|
||||||
"""
|
|
||||||
Parse the server/portal restart status
|
|
||||||
"""
|
|
||||||
if os.path.exists(restart_file):
|
|
||||||
with open(restart_file, 'r') as f:
|
|
||||||
return f.read()
|
|
||||||
return "shutdown"
|
|
||||||
|
|
||||||
|
|
||||||
def get_pid(pidfile):
|
|
||||||
"""
|
|
||||||
Get the PID (Process ID) by trying to access
|
|
||||||
an PID file.
|
|
||||||
"""
|
|
||||||
pid = None
|
|
||||||
if os.path.exists(pidfile):
|
|
||||||
with open(pidfile, 'r') as f:
|
|
||||||
pid = f.read()
|
|
||||||
return pid
|
|
||||||
|
|
||||||
|
|
||||||
def cycle_logfile(logfile):
|
|
||||||
"""
|
|
||||||
Rotate the old log files to <filename>.old
|
|
||||||
"""
|
|
||||||
logfile_old = logfile + '.old'
|
|
||||||
if os.path.exists(logfile):
|
|
||||||
# Cycle the old logfiles to *.old
|
|
||||||
if os.path.exists(logfile_old):
|
|
||||||
# E.g. Windows don't support rename-replace
|
|
||||||
os.remove(logfile_old)
|
|
||||||
os.rename(logfile, logfile_old)
|
|
||||||
|
|
||||||
# Start program management
|
|
||||||
|
|
||||||
|
|
||||||
def start_services(server_argv, portal_argv, doexit=False):
|
|
||||||
"""
|
|
||||||
This calls a threaded loop that launches the Portal and Server
|
|
||||||
and then restarts them when they finish.
|
|
||||||
"""
|
|
||||||
global SERVER, PORTAL
|
|
||||||
processes = Queue.Queue()
|
|
||||||
|
|
||||||
def server_waiter(queue):
|
|
||||||
try:
|
|
||||||
rc = Popen(server_argv, env=getenv()).wait()
|
|
||||||
except Exception as e:
|
|
||||||
print(PROCESS_ERROR.format(component="Server", traceback=e))
|
|
||||||
return
|
|
||||||
# this signals the controller that the program finished
|
|
||||||
queue.put(("server_stopped", rc))
|
|
||||||
|
|
||||||
def portal_waiter(queue):
|
|
||||||
try:
|
|
||||||
rc = Popen(portal_argv, env=getenv()).wait()
|
|
||||||
except Exception as e:
|
|
||||||
print(PROCESS_ERROR.format(component="Portal", traceback=e))
|
|
||||||
return
|
|
||||||
# this signals the controller that the program finished
|
|
||||||
queue.put(("portal_stopped", rc))
|
|
||||||
|
|
||||||
if portal_argv:
|
|
||||||
try:
|
|
||||||
if not doexit and get_restart_mode(PORTAL_RESTART) == "True":
|
|
||||||
# start portal as interactive, reloadable thread
|
|
||||||
PORTAL = thread.start_new_thread(portal_waiter, (processes, ))
|
|
||||||
else:
|
|
||||||
# normal operation: start portal as a daemon;
|
|
||||||
# we don't care to monitor it for restart
|
|
||||||
PORTAL = Popen(portal_argv, env=getenv())
|
|
||||||
except IOError as e:
|
|
||||||
print(PROCESS_IOERROR.format(component="Portal", traceback=e))
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
if server_argv:
|
|
||||||
if doexit:
|
|
||||||
SERVER = Popen(server_argv, env=getenv())
|
|
||||||
else:
|
|
||||||
# start server as a reloadable thread
|
|
||||||
SERVER = thread.start_new_thread(server_waiter, (processes, ))
|
|
||||||
except IOError as e:
|
|
||||||
print(PROCESS_IOERROR.format(component="Server", traceback=e))
|
|
||||||
return
|
|
||||||
|
|
||||||
if doexit:
|
|
||||||
# Exit immediately
|
|
||||||
return
|
|
||||||
|
|
||||||
# Reload loop
|
|
||||||
while True:
|
|
||||||
|
|
||||||
# this blocks until something is actually returned.
|
|
||||||
from twisted.internet.error import ReactorNotRunning
|
|
||||||
try:
|
|
||||||
try:
|
|
||||||
message, rc = processes.get()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# this only matters in interactive mode
|
|
||||||
break
|
|
||||||
|
|
||||||
# restart only if process stopped cleanly
|
|
||||||
if (message == "server_stopped" and int(rc) == 0 and
|
|
||||||
get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset")):
|
|
||||||
print(PROCESS_RESTART.format(component="Server"))
|
|
||||||
SERVER = thread.start_new_thread(server_waiter, (processes, ))
|
|
||||||
continue
|
|
||||||
|
|
||||||
# normally the portal is not reloaded since it's run as a daemon.
|
|
||||||
if (message == "portal_stopped" and int(rc) == 0 and
|
|
||||||
get_restart_mode(PORTAL_RESTART) == "True"):
|
|
||||||
print(PROCESS_RESTART.format(component="Portal"))
|
|
||||||
PORTAL = thread.start_new_thread(portal_waiter, (processes, ))
|
|
||||||
continue
|
|
||||||
break
|
|
||||||
except ReactorNotRunning:
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""
|
|
||||||
This handles the command line input of the runner, usually created by
|
|
||||||
the evennia launcher
|
|
||||||
"""
|
|
||||||
|
|
||||||
parser = ArgumentParser(description=CMDLINE_HELP)
|
|
||||||
parser.add_argument('--noserver', action='store_true', dest='noserver',
|
|
||||||
default=False, help='Do not start Server process')
|
|
||||||
parser.add_argument('--noportal', action='store_true', dest='noportal',
|
|
||||||
default=False, help='Do not start Portal process')
|
|
||||||
parser.add_argument('--logserver', action='store_true', dest='logserver',
|
|
||||||
default=False, help='Log Server output to logfile')
|
|
||||||
parser.add_argument('--iserver', action='store_true', dest='iserver',
|
|
||||||
default=False, help='Server in interactive mode')
|
|
||||||
parser.add_argument('--iportal', action='store_true', dest='iportal',
|
|
||||||
default=False, help='Portal in interactive mode')
|
|
||||||
parser.add_argument('--pserver', action='store_true', dest='pserver',
|
|
||||||
default=False, help='Profile Server')
|
|
||||||
parser.add_argument('--pportal', action='store_true', dest='pportal',
|
|
||||||
default=False, help='Profile Portal')
|
|
||||||
parser.add_argument('--nologcycle', action='store_false', dest='nologcycle',
|
|
||||||
default=True, help='Do not cycle log files')
|
|
||||||
parser.add_argument('--doexit', action='store_true', dest='doexit',
|
|
||||||
default=False, help='Immediately exit after processes have started.')
|
|
||||||
parser.add_argument('gamedir', help="path to game dir")
|
|
||||||
parser.add_argument('twistdbinary', help="path to twistd binary")
|
|
||||||
parser.add_argument('slogfile', help="path to server log file")
|
|
||||||
parser.add_argument('plogfile', help="path to portal log file")
|
|
||||||
parser.add_argument('hlogfile', help="path to http log file")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
global GAMEDIR
|
|
||||||
global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE
|
|
||||||
global SERVER_PIDFILE, PORTAL_PIDFILE
|
|
||||||
global SERVER_RESTART, PORTAL_RESTART
|
|
||||||
global SPROFILER_LOGFILE, PPROFILER_LOGFILE
|
|
||||||
|
|
||||||
GAMEDIR = args.gamedir
|
|
||||||
sys.path.insert(1, os.path.join(GAMEDIR, SERVERDIR))
|
|
||||||
|
|
||||||
SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid")
|
|
||||||
PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid")
|
|
||||||
SERVER_RESTART = os.path.join(GAMEDIR, SERVERDIR, "server.restart")
|
|
||||||
PORTAL_RESTART = os.path.join(GAMEDIR, SERVERDIR, "portal.restart")
|
|
||||||
SERVER_LOGFILE = args.slogfile
|
|
||||||
PORTAL_LOGFILE = args.plogfile
|
|
||||||
HTTP_LOGFILE = args.hlogfile
|
|
||||||
TWISTED_BINARY = args.twistdbinary
|
|
||||||
SPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "server.prof")
|
|
||||||
PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof")
|
|
||||||
|
|
||||||
# set up default project calls
|
|
||||||
server_argv = [TWISTED_BINARY,
|
|
||||||
'--nodaemon',
|
|
||||||
'--logfile=%s' % SERVER_LOGFILE,
|
|
||||||
'--pidfile=%s' % SERVER_PIDFILE,
|
|
||||||
'--python=%s' % SERVER_PY_FILE]
|
|
||||||
portal_argv = [TWISTED_BINARY,
|
|
||||||
'--logfile=%s' % PORTAL_LOGFILE,
|
|
||||||
'--pidfile=%s' % PORTAL_PIDFILE,
|
|
||||||
'--python=%s' % PORTAL_PY_FILE]
|
|
||||||
|
|
||||||
# Profiling settings (read file from python shell e.g with
|
|
||||||
# p = pstats.Stats('server.prof')
|
|
||||||
pserver_argv = ['--savestats',
|
|
||||||
'--profiler=cprofile',
|
|
||||||
'--profile=%s' % SPROFILER_LOGFILE]
|
|
||||||
pportal_argv = ['--savestats',
|
|
||||||
'--profiler=cprofile',
|
|
||||||
'--profile=%s' % PPROFILER_LOGFILE]
|
|
||||||
|
|
||||||
# Server
|
|
||||||
|
|
||||||
pid = get_pid(SERVER_PIDFILE)
|
|
||||||
if pid and not args.noserver:
|
|
||||||
print("\nEvennia Server is already running as process %(pid)s. Not restarted." % {'pid': pid})
|
|
||||||
args.noserver = True
|
|
||||||
if args.noserver:
|
|
||||||
server_argv = None
|
|
||||||
else:
|
|
||||||
set_restart_mode(SERVER_RESTART, "shutdown")
|
|
||||||
if not args.logserver:
|
|
||||||
# don't log to server logfile
|
|
||||||
del server_argv[2]
|
|
||||||
print("\nStarting Evennia Server (output to stdout).")
|
|
||||||
else:
|
|
||||||
if not args.nologcycle:
|
|
||||||
cycle_logfile(SERVER_LOGFILE)
|
|
||||||
print("\nStarting Evennia Server (output to server logfile).")
|
|
||||||
if args.pserver:
|
|
||||||
server_argv.extend(pserver_argv)
|
|
||||||
print("\nRunning Evennia Server under cProfile.")
|
|
||||||
|
|
||||||
# Portal
|
|
||||||
|
|
||||||
pid = get_pid(PORTAL_PIDFILE)
|
|
||||||
if pid and not args.noportal:
|
|
||||||
print("\nEvennia Portal is already running as process %(pid)s. Not restarted." % {'pid': pid})
|
|
||||||
args.noportal = True
|
|
||||||
if args.noportal:
|
|
||||||
portal_argv = None
|
|
||||||
else:
|
|
||||||
if args.iportal:
|
|
||||||
# make portal interactive
|
|
||||||
portal_argv[1] = '--nodaemon'
|
|
||||||
set_restart_mode(PORTAL_RESTART, True)
|
|
||||||
print("\nStarting Evennia Portal in non-Daemon mode (output to stdout).")
|
|
||||||
else:
|
|
||||||
if not args.nologcycle:
|
|
||||||
cycle_logfile(PORTAL_LOGFILE)
|
|
||||||
cycle_logfile(HTTP_LOGFILE)
|
|
||||||
set_restart_mode(PORTAL_RESTART, False)
|
|
||||||
print("\nStarting Evennia Portal in Daemon mode (output to portal logfile).")
|
|
||||||
if args.pportal:
|
|
||||||
portal_argv.extend(pportal_argv)
|
|
||||||
print("\nRunning Evennia Portal under cProfile.")
|
|
||||||
if args.doexit:
|
|
||||||
print(PROCESS_DOEXIT)
|
|
||||||
|
|
||||||
# Windows fixes (Windows don't support pidfiles natively)
|
|
||||||
if os.name == 'nt':
|
|
||||||
if server_argv:
|
|
||||||
del server_argv[-2]
|
|
||||||
if portal_argv:
|
|
||||||
del portal_argv[-2]
|
|
||||||
|
|
||||||
# Start processes
|
|
||||||
start_services(server_argv, portal_argv, doexit=args.doexit)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
@ -160,10 +160,10 @@ def client_options(session, *args, **kwargs):
|
||||||
raw (bool): Turn off parsing
|
raw (bool): Turn off parsing
|
||||||
|
|
||||||
"""
|
"""
|
||||||
flags = session.protocol_flags
|
old_flags = session.protocol_flags
|
||||||
if not kwargs or kwargs.get("get", False):
|
if not kwargs or kwargs.get("get", False):
|
||||||
# return current settings
|
# return current settings
|
||||||
options = dict((key, flags[key]) for key in flags
|
options = dict((key, old_flags[key]) for key in old_flags
|
||||||
if key.upper() in ("ANSI", "XTERM256", "MXP",
|
if key.upper() in ("ANSI", "XTERM256", "MXP",
|
||||||
"UTF-8", "SCREENREADER", "ENCODING",
|
"UTF-8", "SCREENREADER", "ENCODING",
|
||||||
"MCCP", "SCREENHEIGHT",
|
"MCCP", "SCREENHEIGHT",
|
||||||
|
|
@ -189,6 +189,7 @@ def client_options(session, *args, **kwargs):
|
||||||
return True if val.lower() in ("true", "on", "1") else False
|
return True if val.lower() in ("true", "on", "1") else False
|
||||||
return bool(val)
|
return bool(val)
|
||||||
|
|
||||||
|
flags = {}
|
||||||
for key, value in kwargs.iteritems():
|
for key, value in kwargs.iteritems():
|
||||||
key = key.lower()
|
key = key.lower()
|
||||||
if key == "client":
|
if key == "client":
|
||||||
|
|
@ -230,9 +231,11 @@ def client_options(session, *args, **kwargs):
|
||||||
err = _ERROR_INPUT.format(
|
err = _ERROR_INPUT.format(
|
||||||
name="client_settings", session=session, inp=key)
|
name="client_settings", session=session, inp=key)
|
||||||
session.msg(text=err)
|
session.msg(text=err)
|
||||||
session.protocol_flags = flags
|
|
||||||
# we must update the portal as well
|
session.protocol_flags.update(flags)
|
||||||
session.sessionhandler.session_portal_sync(session)
|
# we must update the protocol flags on the portal session copy as well
|
||||||
|
session.sessionhandler.session_portal_partial_sync(
|
||||||
|
{session.sessid: {"protocol_flags": flags}})
|
||||||
|
|
||||||
|
|
||||||
# GMCP alias
|
# GMCP alias
|
||||||
|
|
|
||||||
418
evennia/server/portal/amp.py
Normal file
418
evennia/server/portal/amp.py
Normal file
|
|
@ -0,0 +1,418 @@
|
||||||
|
"""
|
||||||
|
The AMP (Asynchronous Message Protocol)-communication commands and constants used by Evennia.
|
||||||
|
|
||||||
|
This module acts as a central place for AMP-servers and -clients to get commands to use.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import print_function
|
||||||
|
from functools import wraps
|
||||||
|
import time
|
||||||
|
from twisted.protocols import amp
|
||||||
|
from collections import defaultdict, namedtuple
|
||||||
|
from cStringIO import StringIO
|
||||||
|
from itertools import count
|
||||||
|
import zlib # Used in Compressed class
|
||||||
|
try:
|
||||||
|
import cPickle as pickle
|
||||||
|
except ImportError:
|
||||||
|
import pickle
|
||||||
|
|
||||||
|
from twisted.internet.defer import DeferredList, Deferred
|
||||||
|
from evennia.utils.utils import to_str, variable_from_module
|
||||||
|
|
||||||
|
# delayed import
|
||||||
|
_LOGGER = None
|
||||||
|
|
||||||
|
# communication bits
|
||||||
|
# (chr(9) and chr(10) are \t and \n, so skipping them)
|
||||||
|
|
||||||
|
PCONN = chr(1) # portal session connect
|
||||||
|
PDISCONN = chr(2) # portal session disconnect
|
||||||
|
PSYNC = chr(3) # portal session sync
|
||||||
|
SLOGIN = chr(4) # server session login
|
||||||
|
SDISCONN = chr(5) # server session disconnect
|
||||||
|
SDISCONNALL = chr(6) # server session disconnect all
|
||||||
|
SSHUTD = chr(7) # server shutdown
|
||||||
|
SSYNC = chr(8) # server session sync
|
||||||
|
SCONN = chr(11) # server creating new connection (for irc bots and etc)
|
||||||
|
PCONNSYNC = chr(12) # portal post-syncing a session
|
||||||
|
PDISCONNALL = chr(13) # portal session disconnect all
|
||||||
|
SRELOAD = chr(14) # server shutdown in reload mode
|
||||||
|
SSTART = chr(15) # server start (portal must already be running anyway)
|
||||||
|
PSHUTD = chr(16) # portal (+server) shutdown
|
||||||
|
SSHUTD = chr(17) # server shutdown
|
||||||
|
PSTATUS = chr(18) # ping server or portal status
|
||||||
|
SRESET = chr(19) # server shutdown in reset mode
|
||||||
|
|
||||||
|
AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed)
|
||||||
|
BATCH_RATE = 250 # max commands/sec before switching to batch-sending
|
||||||
|
BATCH_TIMEOUT = 0.5 # how often to poll to empty batch queue, in seconds
|
||||||
|
|
||||||
|
# buffers
|
||||||
|
_SENDBATCH = defaultdict(list)
|
||||||
|
_MSGBUFFER = defaultdict(list)
|
||||||
|
|
||||||
|
# resources
|
||||||
|
|
||||||
|
DUMMYSESSION = namedtuple('DummySession', ['sessid'])(0)
|
||||||
|
|
||||||
|
|
||||||
|
_HTTP_WARNING = """
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: text/html
|
||||||
|
|
||||||
|
<html><body>
|
||||||
|
This is Evennia's interal AMP port. It handles communication
|
||||||
|
between Evennia's different processes.<h3><p>This port should NOT be
|
||||||
|
publicly visible.</p></h3>
|
||||||
|
</body></html>""".strip()
|
||||||
|
|
||||||
|
|
||||||
|
# Helper functions for pickling.
|
||||||
|
|
||||||
|
def dumps(data):
|
||||||
|
return to_str(pickle.dumps(to_str(data), pickle.HIGHEST_PROTOCOL))
|
||||||
|
|
||||||
|
|
||||||
|
def loads(data):
|
||||||
|
return pickle.loads(to_str(data))
|
||||||
|
|
||||||
|
|
||||||
|
@wraps
|
||||||
|
def catch_traceback(func):
|
||||||
|
"Helper decorator"
|
||||||
|
def decorator(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
func(*args, **kwargs)
|
||||||
|
except Exception as err:
|
||||||
|
global _LOGGER
|
||||||
|
if not _LOGGER:
|
||||||
|
from evennia.utils import logger as _LOGGER
|
||||||
|
_LOGGER.log_trace()
|
||||||
|
raise # make sure the error is visible on the other side of the connection too
|
||||||
|
print(err)
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
# AMP Communication Command types
|
||||||
|
|
||||||
|
class Compressed(amp.String):
|
||||||
|
"""
|
||||||
|
This is a custom AMP command Argument that both handles too-long
|
||||||
|
sends as well as uses zlib for compression across the wire. The
|
||||||
|
batch-grouping of too-long sends is borrowed from the "mediumbox"
|
||||||
|
recipy at twisted-hacks's ~glyph/+junk/amphacks/mediumbox.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def fromBox(self, name, strings, objects, proto):
|
||||||
|
"""
|
||||||
|
Converts from box representation to python. We
|
||||||
|
group very long data into batches.
|
||||||
|
"""
|
||||||
|
value = StringIO()
|
||||||
|
value.write(strings.get(name))
|
||||||
|
for counter in count(2):
|
||||||
|
# count from 2 upwards
|
||||||
|
chunk = strings.get("%s.%d" % (name, counter))
|
||||||
|
if chunk is None:
|
||||||
|
break
|
||||||
|
value.write(chunk)
|
||||||
|
objects[name] = value.getvalue()
|
||||||
|
|
||||||
|
def toBox(self, name, strings, objects, proto):
|
||||||
|
"""
|
||||||
|
Convert from data to box. We handled too-long
|
||||||
|
batched data and put it together here.
|
||||||
|
"""
|
||||||
|
value = StringIO(objects[name])
|
||||||
|
strings[name] = value.read(AMP_MAXLEN)
|
||||||
|
for counter in count(2):
|
||||||
|
chunk = value.read(AMP_MAXLEN)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
strings["%s.%d" % (name, counter)] = chunk
|
||||||
|
|
||||||
|
def toString(self, inObject):
|
||||||
|
"""
|
||||||
|
Convert to send on the wire, with compression.
|
||||||
|
"""
|
||||||
|
return zlib.compress(inObject, 9)
|
||||||
|
|
||||||
|
def fromString(self, inString):
|
||||||
|
"""
|
||||||
|
Convert (decompress) from the wire to Python.
|
||||||
|
"""
|
||||||
|
return zlib.decompress(inString)
|
||||||
|
|
||||||
|
|
||||||
|
class MsgLauncher2Portal(amp.Command):
|
||||||
|
"""
|
||||||
|
Message Launcher -> Portal
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "MsgLauncher2Portal"
|
||||||
|
arguments = [('operation', amp.String()),
|
||||||
|
('arguments', amp.String())]
|
||||||
|
errors = {Exception: 'EXCEPTION'}
|
||||||
|
response = []
|
||||||
|
|
||||||
|
|
||||||
|
class MsgPortal2Server(amp.Command):
|
||||||
|
"""
|
||||||
|
Message Portal -> Server
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "MsgPortal2Server"
|
||||||
|
arguments = [('packed_data', Compressed())]
|
||||||
|
errors = {Exception: 'EXCEPTION'}
|
||||||
|
response = []
|
||||||
|
|
||||||
|
|
||||||
|
class MsgServer2Portal(amp.Command):
|
||||||
|
"""
|
||||||
|
Message Server -> Portal
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "MsgServer2Portal"
|
||||||
|
arguments = [('packed_data', Compressed())]
|
||||||
|
errors = {Exception: 'EXCEPTION'}
|
||||||
|
response = []
|
||||||
|
|
||||||
|
|
||||||
|
class AdminPortal2Server(amp.Command):
|
||||||
|
"""
|
||||||
|
Administration Portal -> Server
|
||||||
|
|
||||||
|
Sent when the portal needs to perform admin operations on the
|
||||||
|
server, such as when a new session connects or resyncs
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "AdminPortal2Server"
|
||||||
|
arguments = [('packed_data', Compressed())]
|
||||||
|
errors = {Exception: 'EXCEPTION'}
|
||||||
|
response = []
|
||||||
|
|
||||||
|
|
||||||
|
class AdminServer2Portal(amp.Command):
|
||||||
|
"""
|
||||||
|
Administration Server -> Portal
|
||||||
|
|
||||||
|
Sent when the server needs to perform admin operations on the
|
||||||
|
portal.
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "AdminServer2Portal"
|
||||||
|
arguments = [('packed_data', Compressed())]
|
||||||
|
errors = {Exception: 'EXCEPTION'}
|
||||||
|
response = []
|
||||||
|
|
||||||
|
|
||||||
|
class MsgStatus(amp.Command):
|
||||||
|
"""
|
||||||
|
Check Status between AMP services
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "MsgStatus"
|
||||||
|
arguments = [('status', amp.String())]
|
||||||
|
errors = {Exception: 'EXCEPTION'}
|
||||||
|
response = [('status', amp.String())]
|
||||||
|
|
||||||
|
|
||||||
|
class FunctionCall(amp.Command):
|
||||||
|
"""
|
||||||
|
Bidirectional Server <-> Portal
|
||||||
|
|
||||||
|
Sent when either process needs to call an arbitrary function in
|
||||||
|
the other. This does not use the batch-send functionality.
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = "FunctionCall"
|
||||||
|
arguments = [('module', amp.String()),
|
||||||
|
('function', amp.String()),
|
||||||
|
('args', amp.String()),
|
||||||
|
('kwargs', amp.String())]
|
||||||
|
errors = {Exception: 'EXCEPTION'}
|
||||||
|
response = [('result', amp.String())]
|
||||||
|
|
||||||
|
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
# Core AMP protocol for communication Server <-> Portal
|
||||||
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
class AMPMultiConnectionProtocol(amp.AMP):
|
||||||
|
"""
|
||||||
|
AMP protocol that safely handle multiple connections to the same
|
||||||
|
server without dropping old ones - new clients will receive
|
||||||
|
all server returns (broadcast). Will also correctly handle
|
||||||
|
erroneous HTTP requests on the port and return a HTTP error response.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# helper methods
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize protocol with some things that need to be in place
|
||||||
|
already before connecting both on portal and server.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.send_batch_counter = 0
|
||||||
|
self.send_reset_time = time.time()
|
||||||
|
self.send_mode = True
|
||||||
|
self.send_task = None
|
||||||
|
|
||||||
|
def dataReceived(self, data):
|
||||||
|
"""
|
||||||
|
Handle non-AMP messages, such as HTTP communication.
|
||||||
|
"""
|
||||||
|
if data[0] != b'\0':
|
||||||
|
self.transport.write(_HTTP_WARNING)
|
||||||
|
self.transport.loseConnection()
|
||||||
|
else:
|
||||||
|
super(AMPMultiConnectionProtocol, self).dataReceived(data)
|
||||||
|
|
||||||
|
def makeConnection(self, transport):
|
||||||
|
"""
|
||||||
|
Swallow connection log message here. Copied from original
|
||||||
|
in the amp protocol.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# copied from original, removing the log message
|
||||||
|
if not self._ampInitialized:
|
||||||
|
amp.AMP.__init__(self)
|
||||||
|
self._transportPeer = transport.getPeer()
|
||||||
|
self._transportHost = transport.getHost()
|
||||||
|
amp.BinaryBoxProtocol.makeConnection(self, transport)
|
||||||
|
|
||||||
|
def connectionMade(self):
|
||||||
|
"""
|
||||||
|
This is called when an AMP connection is (re-)established. AMP calls it on both sides.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.factory.broadcasts.append(self)
|
||||||
|
|
||||||
|
def connectionLost(self, reason):
|
||||||
|
"""
|
||||||
|
We swallow connection errors here. The reason is that during a
|
||||||
|
normal reload/shutdown there will almost always be cases where
|
||||||
|
either the portal or server shuts down before a message has
|
||||||
|
returned its (empty) return, triggering a connectionLost error
|
||||||
|
that is irrelevant. If a true connection error happens, the
|
||||||
|
portal will continuously try to reconnect, showing the problem
|
||||||
|
that way.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
self.factory.broadcasts.remove(self)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Error handling
|
||||||
|
|
||||||
|
def errback(self, e, info):
|
||||||
|
"""
|
||||||
|
Error callback.
|
||||||
|
Handles errors to avoid dropping connections on server tracebacks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
e (Failure): Deferred error instance.
|
||||||
|
info (str): Error string.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global _LOGGER
|
||||||
|
if not _LOGGER:
|
||||||
|
from evennia.utils import logger as _LOGGER
|
||||||
|
e.trap(Exception)
|
||||||
|
_LOGGER.log_err("AMP Error for %(info)s: %(e)s" % {'info': info,
|
||||||
|
'e': e.getErrorMessage()})
|
||||||
|
|
||||||
|
def data_in(self, packed_data):
|
||||||
|
"""
|
||||||
|
Process incoming packed data.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packed_data (bytes): Zip-packed data.
|
||||||
|
Returns:
|
||||||
|
unpaced_data (any): Unpacked package
|
||||||
|
|
||||||
|
"""
|
||||||
|
return loads(packed_data)
|
||||||
|
|
||||||
|
def broadcast(self, command, sessid, **kwargs):
|
||||||
|
"""
|
||||||
|
Send data across the wire to all connections.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command (AMP Command): A protocol send command.
|
||||||
|
sessid (int): A unique Session id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
deferred (deferred or None): A deferred with an errback.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Data will be sent across the wire pickled as a tuple
|
||||||
|
(sessid, kwargs).
|
||||||
|
|
||||||
|
"""
|
||||||
|
deferreds = []
|
||||||
|
for protcl in self.factory.broadcasts:
|
||||||
|
deferreds.append(protcl.callRemote(command, **kwargs).addErrback(
|
||||||
|
self.errback, command.key))
|
||||||
|
|
||||||
|
return DeferredList(deferreds)
|
||||||
|
|
||||||
|
# generic function send/recvs
|
||||||
|
|
||||||
|
def send_FunctionCall(self, modulepath, functionname, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Access method called by either process. This will call an arbitrary
|
||||||
|
function on the other process (On Portal if calling from Server and
|
||||||
|
vice versa).
|
||||||
|
|
||||||
|
Inputs:
|
||||||
|
modulepath (str) - python path to module holding function to call
|
||||||
|
functionname (str) - name of function in given module
|
||||||
|
*args, **kwargs will be used as arguments/keyword args for the
|
||||||
|
remote function call
|
||||||
|
Returns:
|
||||||
|
A deferred that fires with the return value of the remote
|
||||||
|
function call
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.callRemote(FunctionCall,
|
||||||
|
module=modulepath,
|
||||||
|
function=functionname,
|
||||||
|
args=dumps(args),
|
||||||
|
kwargs=dumps(kwargs)).addCallback(
|
||||||
|
lambda r: loads(r["result"])).addErrback(self.errback, "FunctionCall")
|
||||||
|
|
||||||
|
@FunctionCall.responder
|
||||||
|
@catch_traceback
|
||||||
|
def receive_functioncall(self, module, function, func_args, func_kwargs):
|
||||||
|
"""
|
||||||
|
This allows Portal- and Server-process to call an arbitrary
|
||||||
|
function in the other process. It is intended for use by
|
||||||
|
plugin modules.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
module (str or module): The module containing the
|
||||||
|
`function` to call.
|
||||||
|
function (str): The name of the function to call in
|
||||||
|
`module`.
|
||||||
|
func_args (str): Pickled args tuple for use in `function` call.
|
||||||
|
func_kwargs (str): Pickled kwargs dict for use in `function` call.
|
||||||
|
|
||||||
|
"""
|
||||||
|
args = loads(func_args)
|
||||||
|
kwargs = loads(func_kwargs)
|
||||||
|
|
||||||
|
# call the function (don't catch tracebacks here)
|
||||||
|
result = variable_from_module(module, function)(*args, **kwargs)
|
||||||
|
|
||||||
|
if isinstance(result, Deferred):
|
||||||
|
# if result is a deferred, attach handler to properly
|
||||||
|
# wrap the return value
|
||||||
|
result.addCallback(lambda r: {"result": dumps(r)})
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return {'result': dumps(result)}
|
||||||
455
evennia/server/portal/amp_server.py
Normal file
455
evennia/server/portal/amp_server.py
Normal file
|
|
@ -0,0 +1,455 @@
|
||||||
|
"""
|
||||||
|
The Evennia Portal service acts as an AMP-server, handling AMP
|
||||||
|
communication to the AMP clients connecting to it (by default
|
||||||
|
these are the Evennia Server and the evennia launcher).
|
||||||
|
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from twisted.internet import protocol
|
||||||
|
from evennia.server.portal import amp
|
||||||
|
from django.conf import settings
|
||||||
|
from subprocess import Popen, STDOUT
|
||||||
|
from evennia.utils import logger
|
||||||
|
|
||||||
|
|
||||||
|
def _is_windows():
|
||||||
|
return os.name == 'nt'
|
||||||
|
|
||||||
|
|
||||||
|
def getenv():
|
||||||
|
"""
|
||||||
|
Get current environment and add PYTHONPATH.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
env (dict): Environment global dict.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sep = ";" if _is_windows() else ":"
|
||||||
|
env = os.environ.copy()
|
||||||
|
env['PYTHONPATH'] = sep.join(sys.path)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
class AMPServerFactory(protocol.ServerFactory):
|
||||||
|
|
||||||
|
"""
|
||||||
|
This factory creates AMP Server connection. This acts as the 'Portal'-side communication to the
|
||||||
|
'Server' process.
|
||||||
|
|
||||||
|
"""
|
||||||
|
noisy = False
|
||||||
|
|
||||||
|
def logPrefix(self):
|
||||||
|
"How this is named in logs"
|
||||||
|
return "AMP"
|
||||||
|
|
||||||
|
def __init__(self, portal):
|
||||||
|
"""
|
||||||
|
Initialize the factory. This is called as the Portal service starts.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
portal (Portal): The Evennia Portal service instance.
|
||||||
|
protocol (Protocol): The protocol the factory creates
|
||||||
|
instances of.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.portal = portal
|
||||||
|
self.protocol = AMPServerProtocol
|
||||||
|
self.broadcasts = []
|
||||||
|
self.server_connection = None
|
||||||
|
self.launcher_connection = None
|
||||||
|
self.disconnect_callbacks = {}
|
||||||
|
self.server_connect_callbacks = []
|
||||||
|
|
||||||
|
def buildProtocol(self, addr):
|
||||||
|
"""
|
||||||
|
Start a new connection, and store it on the service object.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
addr (str): Connection address. Not used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
protocol (Protocol): The created protocol.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.portal.amp_protocol = AMPServerProtocol()
|
||||||
|
self.portal.amp_protocol.factory = self
|
||||||
|
return self.portal.amp_protocol
|
||||||
|
|
||||||
|
|
||||||
|
class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
|
||||||
|
"""
|
||||||
|
Protocol subclass for the AMP-server run by the Portal.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def connectionLost(self, reason):
|
||||||
|
"""
|
||||||
|
Set up a simple callback mechanism to let the amp-server wait for a connection to close.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# wipe broadcast and data memory
|
||||||
|
super(AMPServerProtocol, self).connectionLost(reason)
|
||||||
|
if self.factory.server_connection == self:
|
||||||
|
self.factory.server_connection = None
|
||||||
|
self.factory.portal.server_info_dict = {}
|
||||||
|
if self.factory.launcher_connection == self:
|
||||||
|
self.factory.launcher_connection = None
|
||||||
|
|
||||||
|
callback, args, kwargs = self.factory.disconnect_callbacks.pop(self, (None, None, None))
|
||||||
|
if callback:
|
||||||
|
try:
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
|
||||||
|
def get_status(self):
|
||||||
|
"""
|
||||||
|
Return status for the Evennia infrastructure.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
status (tuple): The portal/server status and pids
|
||||||
|
(portal_live, server_live, portal_PID, server_PID).
|
||||||
|
|
||||||
|
"""
|
||||||
|
server_connected = bool(self.factory.server_connection and
|
||||||
|
self.factory.server_connection.transport.connected)
|
||||||
|
portal_info_dict = self.factory.portal.get_info_dict()
|
||||||
|
server_info_dict = self.factory.portal.server_info_dict
|
||||||
|
server_pid = self.factory.portal.server_process_id
|
||||||
|
portal_pid = os.getpid()
|
||||||
|
return (True, server_connected, portal_pid, server_pid, portal_info_dict, server_info_dict)
|
||||||
|
|
||||||
|
def data_to_server(self, command, sessid, **kwargs):
|
||||||
|
"""
|
||||||
|
Send data across the wire to the Server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
command (AMP Command): A protocol send command.
|
||||||
|
sessid (int): A unique Session id.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
deferred (deferred or None): A deferred with an errback.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
Data will be sent across the wire pickled as a tuple
|
||||||
|
(sessid, kwargs).
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.factory.server_connection:
|
||||||
|
return self.factory.server_connection.callRemote(
|
||||||
|
command, packed_data=amp.dumps((sessid, kwargs))).addErrback(
|
||||||
|
self.errback, command.key)
|
||||||
|
else:
|
||||||
|
# if no server connection is available, broadcast
|
||||||
|
return self.broadcast(command, sessid, packed_data=amp.dumps((sessid, kwargs)))
|
||||||
|
|
||||||
|
def start_server(self, server_twistd_cmd):
|
||||||
|
"""
|
||||||
|
(Re-)Launch the Evennia server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
server_twisted_cmd (list): The server start instruction
|
||||||
|
to pass to POpen to start the server.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# start the Server
|
||||||
|
process = None
|
||||||
|
with open(settings.SERVER_LOG_FILE, 'a') as logfile:
|
||||||
|
# we link stdout to a file in order to catch
|
||||||
|
# eventual errors happening before the Server has
|
||||||
|
# opened its logger.
|
||||||
|
try:
|
||||||
|
if _is_windows():
|
||||||
|
# Windows requires special care
|
||||||
|
create_no_window = 0x08000000
|
||||||
|
process = Popen(server_twistd_cmd, env=getenv(), bufsize=-1,
|
||||||
|
stdout=logfile, stderr=STDOUT,
|
||||||
|
creationflags=create_no_window)
|
||||||
|
|
||||||
|
else:
|
||||||
|
process = Popen(server_twistd_cmd, env=getenv(), bufsize=-1,
|
||||||
|
stdout=logfile, stderr=STDOUT)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
|
||||||
|
self.factory.portal.server_twistd_cmd = server_twistd_cmd
|
||||||
|
logfile.flush()
|
||||||
|
if process and not _is_windows():
|
||||||
|
# avoid zombie-process on Unix/BSD
|
||||||
|
process.wait()
|
||||||
|
return
|
||||||
|
|
||||||
|
def wait_for_disconnect(self, callback, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a callback for when this connection is lost.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback (callable): Will be called with *args, **kwargs
|
||||||
|
once this protocol is disconnected.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.factory.disconnect_callbacks[self] = (callback, args, kwargs)
|
||||||
|
|
||||||
|
def wait_for_server_connect(self, callback, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Add a callback for when the Server is sure to have connected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callback (callable): Will be called with *args, **kwargs
|
||||||
|
once the Server handshake with Portal is complete.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.factory.server_connect_callbacks.append((callback, args, kwargs))
|
||||||
|
|
||||||
|
def stop_server(self, mode='shutdown'):
|
||||||
|
"""
|
||||||
|
Shut down server in one or more modes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode (str): One of 'shutdown', 'reload' or 'reset'.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if mode == 'reload':
|
||||||
|
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRELOAD)
|
||||||
|
elif mode == 'reset':
|
||||||
|
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SRESET)
|
||||||
|
elif mode == 'shutdown':
|
||||||
|
self.send_AdminPortal2Server(amp.DUMMYSESSION, operation=amp.SSHUTD)
|
||||||
|
self.factory.portal.server_restart_mode = mode
|
||||||
|
|
||||||
|
# sending amp data
|
||||||
|
|
||||||
|
def send_Status2Launcher(self):
|
||||||
|
"""
|
||||||
|
Send a status stanza to the launcher.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if self.factory.launcher_connection:
|
||||||
|
self.factory.launcher_connection.callRemote(
|
||||||
|
amp.MsgStatus,
|
||||||
|
status=amp.dumps(self.get_status())).addErrback(
|
||||||
|
self.errback, amp.MsgStatus.key)
|
||||||
|
|
||||||
|
def send_MsgPortal2Server(self, session, **kwargs):
|
||||||
|
"""
|
||||||
|
Access method called by the Portal and executed on the Portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (session): Session
|
||||||
|
kwargs (any, optional): Optional data.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
deferred (Deferred): Asynchronous return.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.data_to_server(amp.MsgPortal2Server, session.sessid, **kwargs)
|
||||||
|
|
||||||
|
def send_AdminPortal2Server(self, session, operation="", **kwargs):
|
||||||
|
"""
|
||||||
|
Send Admin instructions from the Portal to the Server.
|
||||||
|
Executed on the Portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session (Session): Session.
|
||||||
|
operation (char, optional): Identifier for the server operation, as defined by the
|
||||||
|
global variables in `evennia/server/amp.py`.
|
||||||
|
data (str or dict, optional): Data used in the administrative operation.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.data_to_server(amp.AdminPortal2Server, session.sessid,
|
||||||
|
operation=operation, **kwargs)
|
||||||
|
|
||||||
|
# receive amp data
|
||||||
|
|
||||||
|
@amp.MsgStatus.responder
|
||||||
|
@amp.catch_traceback
|
||||||
|
def portal_receive_status(self, status):
|
||||||
|
"""
|
||||||
|
Returns run-status for the server/portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
status (str): Not used.
|
||||||
|
Returns:
|
||||||
|
status (dict): The status is a tuple
|
||||||
|
(portal_running, server_running, portal_pid, server_pid).
|
||||||
|
|
||||||
|
"""
|
||||||
|
return {"status": amp.dumps(self.get_status())}
|
||||||
|
|
||||||
|
@amp.MsgLauncher2Portal.responder
|
||||||
|
@amp.catch_traceback
|
||||||
|
def portal_receive_launcher2portal(self, operation, arguments):
|
||||||
|
"""
|
||||||
|
Receives message arriving from evennia_launcher.
|
||||||
|
This method is executed on the Portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
operation (str): The action to perform.
|
||||||
|
arguments (str): Possible argument to the instruction, or the empty string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
result (dict): The result back to the launcher.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This is the entrypoint for controlling the entire Evennia system from the evennia
|
||||||
|
launcher. It can obviously only accessed when the Portal is already up and running.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.factory.launcher_connection = self
|
||||||
|
|
||||||
|
_, server_connected, _, _, _, _ = self.get_status()
|
||||||
|
|
||||||
|
# logger.log_msg("Evennia Launcher->Portal operation %s received" % (ord(operation)))
|
||||||
|
|
||||||
|
if operation == amp.SSTART: # portal start #15
|
||||||
|
# first, check if server is already running
|
||||||
|
if not server_connected:
|
||||||
|
self.wait_for_server_connect(self.send_Status2Launcher)
|
||||||
|
self.start_server(amp.loads(arguments))
|
||||||
|
|
||||||
|
elif operation == amp.SRELOAD: # reload server #14
|
||||||
|
if server_connected:
|
||||||
|
# We let the launcher restart us once they get the signal
|
||||||
|
self.factory.server_connection.wait_for_disconnect(
|
||||||
|
self.send_Status2Launcher)
|
||||||
|
self.stop_server(mode='reload')
|
||||||
|
else:
|
||||||
|
self.wait_for_server_connect(self.send_Status2Launcher)
|
||||||
|
self.start_server(amp.loads(arguments))
|
||||||
|
|
||||||
|
elif operation == amp.SRESET: # reload server #19
|
||||||
|
if server_connected:
|
||||||
|
self.factory.server_connection.wait_for_disconnect(
|
||||||
|
self.send_Status2Launcher)
|
||||||
|
self.stop_server(mode='reset')
|
||||||
|
else:
|
||||||
|
self.wait_for_server_connect(self.send_Status2Launcher)
|
||||||
|
self.start_server(amp.loads(arguments))
|
||||||
|
|
||||||
|
elif operation == amp.SSHUTD: # server-only shutdown #17
|
||||||
|
if server_connected:
|
||||||
|
self.factory.server_connection.wait_for_disconnect(
|
||||||
|
self.send_Status2Launcher)
|
||||||
|
self.stop_server(mode='shutdown')
|
||||||
|
|
||||||
|
elif operation == amp.PSHUTD: # portal + server shutdown #16
|
||||||
|
if server_connected:
|
||||||
|
self.factory.server_connection.wait_for_disconnect(
|
||||||
|
self.factory.portal.shutdown, restart=False)
|
||||||
|
else:
|
||||||
|
self.factory.portal.shutdown(restart=False)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("operation %(op)s not recognized." % {'op': operation})
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@amp.MsgServer2Portal.responder
|
||||||
|
@amp.catch_traceback
|
||||||
|
def portal_receive_server2portal(self, packed_data):
|
||||||
|
"""
|
||||||
|
Receives message arriving to Portal from Server.
|
||||||
|
This method is executed on the Portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packed_data (str): Pickled data (sessid, kwargs) coming over the wire.
|
||||||
|
|
||||||
|
"""
|
||||||
|
sessid, kwargs = self.data_in(packed_data)
|
||||||
|
session = self.factory.portal.sessions.get(sessid, None)
|
||||||
|
if session:
|
||||||
|
self.factory.portal.sessions.data_out(session, **kwargs)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@amp.AdminServer2Portal.responder
|
||||||
|
@amp.catch_traceback
|
||||||
|
def portal_receive_adminserver2portal(self, packed_data):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Receives and handles admin operations sent to the Portal
|
||||||
|
This is executed on the Portal.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
packed_data (str): Data received, a pickled tuple (sessid, kwargs).
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.factory.server_connection = self
|
||||||
|
|
||||||
|
sessid, kwargs = self.data_in(packed_data)
|
||||||
|
operation = kwargs.pop("operation")
|
||||||
|
portal_sessionhandler = self.factory.portal.sessions
|
||||||
|
|
||||||
|
if operation == amp.SLOGIN: # server_session_login
|
||||||
|
# a session has authenticated; sync it.
|
||||||
|
session = portal_sessionhandler.get(sessid)
|
||||||
|
if session:
|
||||||
|
portal_sessionhandler.server_logged_in(session, kwargs.get("sessiondata"))
|
||||||
|
|
||||||
|
elif operation == amp.SDISCONN: # server_session_disconnect
|
||||||
|
# the server is ordering to disconnect the session
|
||||||
|
session = portal_sessionhandler.get(sessid)
|
||||||
|
if session:
|
||||||
|
portal_sessionhandler.server_disconnect(session, reason=kwargs.get("reason"))
|
||||||
|
|
||||||
|
elif operation == amp.SDISCONNALL: # server_session_disconnect_all
|
||||||
|
# server orders all sessions to disconnect
|
||||||
|
portal_sessionhandler.server_disconnect_all(reason=kwargs.get("reason"))
|
||||||
|
|
||||||
|
elif operation == amp.SRELOAD: # server reload
|
||||||
|
self.factory.server_connection.wait_for_disconnect(
|
||||||
|
self.start_server, self.factory.portal.server_twistd_cmd)
|
||||||
|
self.stop_server(mode='reload')
|
||||||
|
|
||||||
|
elif operation == amp.SRESET: # server reset
|
||||||
|
self.factory.server_connection.wait_for_disconnect(
|
||||||
|
self.start_server, self.factory.portal.server_twistd_cmd)
|
||||||
|
self.stop_server(mode='reset')
|
||||||
|
|
||||||
|
elif operation == amp.SSHUTD: # server-only shutdown
|
||||||
|
self.stop_server(mode='shutdown')
|
||||||
|
|
||||||
|
elif operation == amp.PSHUTD: # full server+server shutdown
|
||||||
|
self.factory.server_connection.wait_for_disconnect(
|
||||||
|
self.factory.portal.shutdown, restart=False)
|
||||||
|
self.stop_server(mode='shutdown')
|
||||||
|
|
||||||
|
elif operation == amp.PSYNC: # portal sync
|
||||||
|
# Server has (re-)connected and wants the session data from portal
|
||||||
|
self.factory.portal.server_info_dict = kwargs.get("info_dict", {})
|
||||||
|
self.factory.portal.server_process_id = kwargs.get("spid", None)
|
||||||
|
# this defaults to 'shutdown' or whatever value set in server_stop
|
||||||
|
server_restart_mode = self.factory.portal.server_restart_mode
|
||||||
|
|
||||||
|
sessdata = self.factory.portal.sessions.get_all_sync_data()
|
||||||
|
self.send_AdminPortal2Server(amp.DUMMYSESSION,
|
||||||
|
amp.PSYNC,
|
||||||
|
server_restart_mode=server_restart_mode,
|
||||||
|
sessiondata=sessdata)
|
||||||
|
self.factory.portal.sessions.at_server_connection()
|
||||||
|
|
||||||
|
if self.factory.server_connection:
|
||||||
|
# this is an indication the server has successfully connected, so
|
||||||
|
# we trigger any callbacks (usually to tell the launcher server is up)
|
||||||
|
for callback, args, kwargs in self.factory.server_connect_callbacks:
|
||||||
|
try:
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
self.factory.server_connect_callbacks = []
|
||||||
|
|
||||||
|
elif operation == amp.SSYNC: # server_session_sync
|
||||||
|
# server wants to save session data to the portal,
|
||||||
|
# maybe because it's about to shut down.
|
||||||
|
portal_sessionhandler.server_session_sync(kwargs.get("sessiondata"),
|
||||||
|
kwargs.get("clean", True))
|
||||||
|
|
||||||
|
# set a flag in case we are about to shut down soon
|
||||||
|
self.factory.server_restart_mode = True
|
||||||
|
|
||||||
|
elif operation == amp.SCONN: # server_force_connection (for irc/etc)
|
||||||
|
portal_sessionhandler.server_connect(**kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise Exception("operation %(op)s not recognized." % {'op': operation})
|
||||||
|
return {}
|
||||||
|
|
@ -7,17 +7,15 @@ sets up all the networking features. (this is done automatically
|
||||||
by game/evennia.py).
|
by game/evennia.py).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function
|
|
||||||
from builtins import object
|
from builtins import object
|
||||||
|
|
||||||
import time
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from twisted.application import internet, service
|
from twisted.application import internet, service
|
||||||
from twisted.internet import protocol, reactor
|
from twisted.internet import protocol, reactor
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.python.log import ILogObserver
|
||||||
from twisted.web import server
|
|
||||||
import django
|
import django
|
||||||
django.setup()
|
django.setup()
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -27,6 +25,7 @@ evennia._init()
|
||||||
|
|
||||||
from evennia.utils.utils import get_evennia_version, mod_import, make_iter
|
from evennia.utils.utils import get_evennia_version, mod_import, make_iter
|
||||||
from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS
|
from evennia.server.portal.portalsessionhandler import PORTAL_SESSIONS
|
||||||
|
from evennia.utils import logger
|
||||||
from evennia.server.webserver import EvenniaReverseProxyResource
|
from evennia.server.webserver import EvenniaReverseProxyResource
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
|
|
||||||
|
|
@ -40,11 +39,6 @@ except Exception:
|
||||||
PORTAL_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)]
|
PORTAL_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.PORTAL_SERVICES_PLUGIN_MODULES)]
|
||||||
LOCKDOWN_MODE = settings.LOCKDOWN_MODE
|
LOCKDOWN_MODE = settings.LOCKDOWN_MODE
|
||||||
|
|
||||||
PORTAL_PIDFILE = ""
|
|
||||||
if os.name == 'nt':
|
|
||||||
# For Windows we need to handle pid files manually.
|
|
||||||
PORTAL_PIDFILE = os.path.join(settings.GAME_DIR, "server", 'portal.pid')
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Evennia Portal settings
|
# Evennia Portal settings
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
@ -80,10 +74,15 @@ AMP_PORT = settings.AMP_PORT
|
||||||
AMP_INTERFACE = settings.AMP_INTERFACE
|
AMP_INTERFACE = settings.AMP_INTERFACE
|
||||||
AMP_ENABLED = AMP_HOST and AMP_PORT and AMP_INTERFACE
|
AMP_ENABLED = AMP_HOST and AMP_PORT and AMP_INTERFACE
|
||||||
|
|
||||||
|
INFO_DICT = {"servername": SERVERNAME, "version": VERSION, "errors": "", "info": "",
|
||||||
|
"lockdown_mode": "", "amp": "", "telnet": [], "telnet_ssl": [], "ssh": [],
|
||||||
|
"webclient": [], "webserver_proxy": [], "webserver_internal": []}
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
# Portal Service object
|
# Portal Service object
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
class Portal(object):
|
class Portal(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
@ -108,12 +107,19 @@ class Portal(object):
|
||||||
self.amp_protocol = None # set by amp factory
|
self.amp_protocol = None # set by amp factory
|
||||||
self.sessions = PORTAL_SESSIONS
|
self.sessions = PORTAL_SESSIONS
|
||||||
self.sessions.portal = self
|
self.sessions.portal = self
|
||||||
|
self.process_id = os.getpid()
|
||||||
|
|
||||||
|
self.server_process_id = None
|
||||||
|
self.server_restart_mode = "shutdown"
|
||||||
|
self.server_info_dict = {}
|
||||||
|
|
||||||
# set a callback if the server is killed abruptly,
|
# set a callback if the server is killed abruptly,
|
||||||
# by Ctrl-C, reboot etc.
|
# by Ctrl-C, reboot etc.
|
||||||
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True)
|
reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True)
|
||||||
|
|
||||||
self.game_running = False
|
def get_info_dict(self):
|
||||||
|
"Return the Portal info, for display."
|
||||||
|
return INFO_DICT
|
||||||
|
|
||||||
def set_restart_mode(self, mode=None):
|
def set_restart_mode(self, mode=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -128,7 +134,6 @@ class Portal(object):
|
||||||
if mode is None:
|
if mode is None:
|
||||||
return
|
return
|
||||||
with open(PORTAL_RESTART, 'w') as f:
|
with open(PORTAL_RESTART, 'w') as f:
|
||||||
print("writing mode=%(mode)s to %(portal_restart)s" % {'mode': mode, 'portal_restart': PORTAL_RESTART})
|
|
||||||
f.write(str(mode))
|
f.write(str(mode))
|
||||||
|
|
||||||
def shutdown(self, restart=None, _reactor_stopping=False):
|
def shutdown(self, restart=None, _reactor_stopping=False):
|
||||||
|
|
@ -155,9 +160,7 @@ class Portal(object):
|
||||||
return
|
return
|
||||||
self.sessions.disconnect_all()
|
self.sessions.disconnect_all()
|
||||||
self.set_restart_mode(restart)
|
self.set_restart_mode(restart)
|
||||||
if os.name == 'nt' and os.path.exists(PORTAL_PIDFILE):
|
|
||||||
# for Windows we need to remove pid files manually
|
|
||||||
os.remove(PORTAL_PIDFILE)
|
|
||||||
if not _reactor_stopping:
|
if not _reactor_stopping:
|
||||||
# shutting down the reactor will trigger another signal. We set
|
# shutting down the reactor will trigger another signal. We set
|
||||||
# a flag to avoid loops.
|
# a flag to avoid loops.
|
||||||
|
|
@ -175,14 +178,18 @@ class Portal(object):
|
||||||
# what to execute from.
|
# what to execute from.
|
||||||
application = service.Application('Portal')
|
application = service.Application('Portal')
|
||||||
|
|
||||||
|
# custom logging
|
||||||
|
logfile = logger.WeeklyLogFile(os.path.basename(settings.PORTAL_LOG_FILE),
|
||||||
|
os.path.dirname(settings.PORTAL_LOG_FILE))
|
||||||
|
application.setComponent(ILogObserver, logger.PortalLogObserver(logfile).emit)
|
||||||
|
|
||||||
# The main Portal server program. This sets up the database
|
# The main Portal server program. This sets up the database
|
||||||
# and is where we store all the other services.
|
# and is where we store all the other services.
|
||||||
PORTAL = Portal(application)
|
PORTAL = Portal(application)
|
||||||
|
|
||||||
print('-' * 50)
|
|
||||||
print(' %(servername)s Portal (%(version)s) started.' % {'servername': SERVERNAME, 'version': VERSION})
|
|
||||||
if LOCKDOWN_MODE:
|
if LOCKDOWN_MODE:
|
||||||
print(' LOCKDOWN_MODE active: Only local connections.')
|
|
||||||
|
INFO_DICT["lockdown_mode"] = ' LOCKDOWN_MODE active: Only local connections.'
|
||||||
|
|
||||||
if AMP_ENABLED:
|
if AMP_ENABLED:
|
||||||
|
|
||||||
|
|
@ -190,14 +197,14 @@ if AMP_ENABLED:
|
||||||
# the portal and the mud server. Only reason to ever deactivate
|
# the portal and the mud server. Only reason to ever deactivate
|
||||||
# it would be during testing and debugging.
|
# it would be during testing and debugging.
|
||||||
|
|
||||||
from evennia.server import amp
|
from evennia.server.portal import amp_server
|
||||||
|
|
||||||
print(' amp (to Server): %s (internal)' % AMP_PORT)
|
INFO_DICT["amp"] = 'amp: %s' % AMP_PORT
|
||||||
|
|
||||||
factory = amp.AmpClientFactory(PORTAL)
|
factory = amp_server.AMPServerFactory(PORTAL)
|
||||||
amp_client = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
|
amp_service = internet.TCPServer(AMP_PORT, factory, interface=AMP_INTERFACE)
|
||||||
amp_client.setName('evennia_amp')
|
amp_service.setName("PortalAMPServer")
|
||||||
PORTAL.services.addService(amp_client)
|
PORTAL.services.addService(amp_service)
|
||||||
|
|
||||||
|
|
||||||
# We group all the various services under the same twisted app.
|
# We group all the various services under the same twisted app.
|
||||||
|
|
@ -215,7 +222,7 @@ if TELNET_ENABLED:
|
||||||
ifacestr = "-%s" % interface
|
ifacestr = "-%s" % interface
|
||||||
for port in TELNET_PORTS:
|
for port in TELNET_PORTS:
|
||||||
pstring = "%s:%s" % (ifacestr, port)
|
pstring = "%s:%s" % (ifacestr, port)
|
||||||
factory = protocol.ServerFactory()
|
factory = telnet.TelnetServerFactory()
|
||||||
factory.noisy = False
|
factory.noisy = False
|
||||||
factory.protocol = telnet.TelnetProtocol
|
factory.protocol = telnet.TelnetProtocol
|
||||||
factory.sessionhandler = PORTAL_SESSIONS
|
factory.sessionhandler = PORTAL_SESSIONS
|
||||||
|
|
@ -223,14 +230,14 @@ if TELNET_ENABLED:
|
||||||
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
telnet_service.setName('EvenniaTelnet%s' % pstring)
|
||||||
PORTAL.services.addService(telnet_service)
|
PORTAL.services.addService(telnet_service)
|
||||||
|
|
||||||
print(' telnet%s: %s (external)' % (ifacestr, port))
|
INFO_DICT["telnet"].append('telnet%s: %s' % (ifacestr, port))
|
||||||
|
|
||||||
|
|
||||||
if SSL_ENABLED:
|
if SSL_ENABLED:
|
||||||
|
|
||||||
# Start SSL game connection (requires PyOpenSSL).
|
# Start Telnet+SSL game connection (requires PyOpenSSL).
|
||||||
|
|
||||||
from evennia.server.portal import ssl
|
from evennia.server.portal import telnet_ssl
|
||||||
|
|
||||||
for interface in SSL_INTERFACES:
|
for interface in SSL_INTERFACES:
|
||||||
ifacestr = ""
|
ifacestr = ""
|
||||||
|
|
@ -241,15 +248,21 @@ if SSL_ENABLED:
|
||||||
factory = protocol.ServerFactory()
|
factory = protocol.ServerFactory()
|
||||||
factory.noisy = False
|
factory.noisy = False
|
||||||
factory.sessionhandler = PORTAL_SESSIONS
|
factory.sessionhandler = PORTAL_SESSIONS
|
||||||
factory.protocol = ssl.SSLProtocol
|
factory.protocol = telnet_ssl.SSLProtocol
|
||||||
|
|
||||||
|
ssl_context = telnet_ssl.getSSLContext()
|
||||||
|
if ssl_context:
|
||||||
ssl_service = internet.SSLServer(port,
|
ssl_service = internet.SSLServer(port,
|
||||||
factory,
|
factory,
|
||||||
ssl.getSSLContext(),
|
telnet_ssl.getSSLContext(),
|
||||||
interface=interface)
|
interface=interface)
|
||||||
ssl_service.setName('EvenniaSSL%s' % pstring)
|
ssl_service.setName('EvenniaSSL%s' % pstring)
|
||||||
PORTAL.services.addService(ssl_service)
|
PORTAL.services.addService(ssl_service)
|
||||||
|
|
||||||
print(" ssl%s: %s (external)" % (ifacestr, port))
|
INFO_DICT["telnet_ssl"].append("telnet+ssl%s: %s" % (ifacestr, port))
|
||||||
|
else:
|
||||||
|
INFO_DICT["telnet_ssl"].append(
|
||||||
|
"telnet+ssl%s: %s (deactivated - keys/cert unset)" % (ifacestr, port))
|
||||||
|
|
||||||
|
|
||||||
if SSH_ENABLED:
|
if SSH_ENABLED:
|
||||||
|
|
@ -273,7 +286,7 @@ if SSH_ENABLED:
|
||||||
ssh_service.setName('EvenniaSSH%s' % pstring)
|
ssh_service.setName('EvenniaSSH%s' % pstring)
|
||||||
PORTAL.services.addService(ssh_service)
|
PORTAL.services.addService(ssh_service)
|
||||||
|
|
||||||
print(" ssh%s: %s (external)" % (ifacestr, port))
|
INFO_DICT["ssh"].append("ssh%s: %s" % (ifacestr, port))
|
||||||
|
|
||||||
|
|
||||||
if WEBSERVER_ENABLED:
|
if WEBSERVER_ENABLED:
|
||||||
|
|
@ -296,7 +309,7 @@ if WEBSERVER_ENABLED:
|
||||||
ajax_webclient = webclient_ajax.AjaxWebClient()
|
ajax_webclient = webclient_ajax.AjaxWebClient()
|
||||||
ajax_webclient.sessionhandler = PORTAL_SESSIONS
|
ajax_webclient.sessionhandler = PORTAL_SESSIONS
|
||||||
web_root.putChild("webclientdata", ajax_webclient)
|
web_root.putChild("webclientdata", ajax_webclient)
|
||||||
webclientstr = "\n + webclient (ajax only)"
|
webclientstr = "webclient (ajax only)"
|
||||||
|
|
||||||
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
|
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
|
||||||
# start websocket client port for the webclient
|
# start websocket client port for the webclient
|
||||||
|
|
@ -309,32 +322,33 @@ if WEBSERVER_ENABLED:
|
||||||
if w_interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
|
if w_interface not in ('0.0.0.0', '::') or len(WEBSERVER_INTERFACES) > 1:
|
||||||
w_ifacestr = "-%s" % interface
|
w_ifacestr = "-%s" % interface
|
||||||
port = WEBSOCKET_CLIENT_PORT
|
port = WEBSOCKET_CLIENT_PORT
|
||||||
factory = protocol.ServerFactory()
|
|
||||||
|
class Websocket(protocol.ServerFactory):
|
||||||
|
"Only here for better naming in logs"
|
||||||
|
pass
|
||||||
|
|
||||||
|
factory = Websocket()
|
||||||
factory.noisy = False
|
factory.noisy = False
|
||||||
factory.protocol = webclient.WebSocketClient
|
factory.protocol = webclient.WebSocketClient
|
||||||
factory.sessionhandler = PORTAL_SESSIONS
|
factory.sessionhandler = PORTAL_SESSIONS
|
||||||
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=w_interface)
|
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=w_interface)
|
||||||
websocket_service.setName('EvenniaWebSocket%s:%s' % (w_ifacestr, proxyport))
|
websocket_service.setName('EvenniaWebSocket%s:%s' % (w_ifacestr, port))
|
||||||
PORTAL.services.addService(websocket_service)
|
PORTAL.services.addService(websocket_service)
|
||||||
websocket_started = True
|
websocket_started = True
|
||||||
webclientstr = "\n + webclient-websocket%s: %s (external)" % (w_ifacestr, proxyport)
|
webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port)
|
||||||
|
INFO_DICT["webclient"].append(webclientstr)
|
||||||
|
|
||||||
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||||
|
web_root.is_portal = True
|
||||||
proxy_service = internet.TCPServer(proxyport,
|
proxy_service = internet.TCPServer(proxyport,
|
||||||
web_root,
|
web_root,
|
||||||
interface=interface)
|
interface=interface)
|
||||||
proxy_service.setName('EvenniaWebProxy%s' % pstring)
|
proxy_service.setName('EvenniaWebProxy%s' % pstring)
|
||||||
PORTAL.services.addService(proxy_service)
|
PORTAL.services.addService(proxy_service)
|
||||||
print(" website-proxy%s: %s (external) %s" % (ifacestr, proxyport, webclientstr))
|
INFO_DICT["webserver_proxy"].append("webserver-proxy%s: %s" % (ifacestr, proxyport))
|
||||||
|
INFO_DICT["webserver_internal"].append("webserver: %s" % serverport)
|
||||||
|
|
||||||
|
|
||||||
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
|
for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
|
||||||
# external plugin services to start
|
# external plugin services to start
|
||||||
plugin_module.start_plugin_services(PORTAL)
|
plugin_module.start_plugin_services(PORTAL)
|
||||||
|
|
||||||
print('-' * 50) # end of terminal output
|
|
||||||
|
|
||||||
if os.name == 'nt':
|
|
||||||
# Windows only: Set PID file manually
|
|
||||||
with open(PORTAL_PIDFILE, 'w') as f:
|
|
||||||
f.write(str(os.getpid()))
|
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,15 @@ from evennia.utils.logger import log_trace
|
||||||
# module import
|
# module import
|
||||||
_MOD_IMPORT = None
|
_MOD_IMPORT = None
|
||||||
|
|
||||||
# throttles
|
# global throttles
|
||||||
_MAX_CONNECTION_RATE = float(settings.MAX_CONNECTION_RATE)
|
_MAX_CONNECTION_RATE = float(settings.MAX_CONNECTION_RATE)
|
||||||
|
# per-session throttles
|
||||||
_MAX_COMMAND_RATE = float(settings.MAX_COMMAND_RATE)
|
_MAX_COMMAND_RATE = float(settings.MAX_COMMAND_RATE)
|
||||||
_MAX_CHAR_LIMIT = int(settings.MAX_CHAR_LIMIT)
|
_MAX_CHAR_LIMIT = int(settings.MAX_CHAR_LIMIT)
|
||||||
|
|
||||||
_MIN_TIME_BETWEEN_CONNECTS = 1.0 / float(settings.MAX_CONNECTION_RATE)
|
_MIN_TIME_BETWEEN_CONNECTS = 1.0 / float(_MAX_CONNECTION_RATE)
|
||||||
|
_MIN_TIME_BETWEEN_COMMANDS = 1.0 / float(_MAX_COMMAND_RATE)
|
||||||
|
|
||||||
_ERROR_COMMAND_OVERFLOW = settings.COMMAND_RATE_WARNING
|
_ERROR_COMMAND_OVERFLOW = settings.COMMAND_RATE_WARNING
|
||||||
_ERROR_MAX_CHAR = settings.MAX_CHAR_LIMIT_WARNING
|
_ERROR_MAX_CHAR = settings.MAX_CHAR_LIMIT_WARNING
|
||||||
|
|
||||||
|
|
@ -58,9 +61,6 @@ class PortalSessionHandler(SessionHandler):
|
||||||
|
|
||||||
self.connection_last = self.uptime
|
self.connection_last = self.uptime
|
||||||
self.connection_task = None
|
self.connection_task = None
|
||||||
self.command_counter = 0
|
|
||||||
self.command_counter_reset = self.uptime
|
|
||||||
self.command_overflow = False
|
|
||||||
|
|
||||||
def at_server_connection(self):
|
def at_server_connection(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -354,8 +354,6 @@ class PortalSessionHandler(SessionHandler):
|
||||||
Data is serialized before passed on.
|
Data is serialized before passed on.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# from evennia.server.profiling.timetrace import timetrace # DEBUG
|
|
||||||
# text = timetrace(text, "portalsessionhandler.data_in") # DEBUG
|
|
||||||
try:
|
try:
|
||||||
text = kwargs['text']
|
text = kwargs['text']
|
||||||
if (_MAX_CHAR_LIMIT > 0) and len(text) > _MAX_CHAR_LIMIT:
|
if (_MAX_CHAR_LIMIT > 0) and len(text) > _MAX_CHAR_LIMIT:
|
||||||
|
|
@ -367,30 +365,38 @@ class PortalSessionHandler(SessionHandler):
|
||||||
pass
|
pass
|
||||||
if session:
|
if session:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
if self.command_counter > _MAX_COMMAND_RATE > 0:
|
|
||||||
# data throttle (anti DoS measure)
|
try:
|
||||||
delta_time = now - self.command_counter_reset
|
command_counter_reset = session.command_counter_reset
|
||||||
self.command_counter = 0
|
except AttributeError:
|
||||||
self.command_counter_reset = now
|
command_counter_reset = session.command_counter_reset = now
|
||||||
self.command_overflow = delta_time < 1.0
|
session.command_counter = 0
|
||||||
if self.command_overflow:
|
|
||||||
reactor.callLater(1.0, self.data_in, None)
|
# global command-rate limit
|
||||||
if self.command_overflow:
|
if max(0, now - command_counter_reset) > 1.0:
|
||||||
|
# more than a second since resetting the counter. Refresh.
|
||||||
|
session.command_counter_reset = now
|
||||||
|
session.command_counter = 0
|
||||||
|
|
||||||
|
session.command_counter += 1
|
||||||
|
|
||||||
|
if session.command_counter * _MIN_TIME_BETWEEN_COMMANDS > 1.0:
|
||||||
self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}])
|
self.data_out(session, text=[[_ERROR_COMMAND_OVERFLOW], {}])
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not self.portal.amp_protocol:
|
||||||
|
# this can happen if someone connects before AMP connection
|
||||||
|
# was established (usually on first start)
|
||||||
|
reactor.callLater(1.0, self.data_in, session, **kwargs)
|
||||||
|
return
|
||||||
|
|
||||||
# scrub data
|
# scrub data
|
||||||
kwargs = self.clean_senddata(session, kwargs)
|
kwargs = self.clean_senddata(session, kwargs)
|
||||||
|
|
||||||
# relay data to Server
|
# relay data to Server
|
||||||
self.command_counter += 1
|
|
||||||
session.cmd_last = now
|
session.cmd_last = now
|
||||||
self.portal.amp_protocol.send_MsgPortal2Server(session,
|
self.portal.amp_protocol.send_MsgPortal2Server(session,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
else:
|
|
||||||
# called by the callLater callback
|
|
||||||
if self.command_overflow:
|
|
||||||
self.command_overflow = False
|
|
||||||
reactor.callLater(1.0, self.data_in, None)
|
|
||||||
|
|
||||||
def data_out(self, session, **kwargs):
|
def data_out(self, session, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,7 @@ from twisted.conch.ssh import common
|
||||||
from twisted.conch.insults import insults
|
from twisted.conch.insults import insults
|
||||||
from twisted.conch.manhole_ssh import TerminalRealm, _Glue, ConchFactory
|
from twisted.conch.manhole_ssh import TerminalRealm, _Glue, ConchFactory
|
||||||
from twisted.conch.manhole import Manhole, recvline
|
from twisted.conch.manhole import Manhole, recvline
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer, protocol
|
||||||
from twisted.conch import interfaces as iconch
|
from twisted.conch import interfaces as iconch
|
||||||
from twisted.python import components
|
from twisted.python import components
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -52,12 +52,34 @@ from evennia.utils.utils import to_str
|
||||||
_RE_N = re.compile(r"\|n$")
|
_RE_N = re.compile(r"\|n$")
|
||||||
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
|
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
|
||||||
_GAME_DIR = settings.GAME_DIR
|
_GAME_DIR = settings.GAME_DIR
|
||||||
|
_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-private.key")
|
||||||
|
_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssh-public.key")
|
||||||
|
_KEY_LENGTH = 2048
|
||||||
|
|
||||||
CTRL_C = '\x03'
|
CTRL_C = '\x03'
|
||||||
CTRL_D = '\x04'
|
CTRL_D = '\x04'
|
||||||
CTRL_BACKSLASH = '\x1c'
|
CTRL_BACKSLASH = '\x1c'
|
||||||
CTRL_L = '\x0c'
|
CTRL_L = '\x0c'
|
||||||
|
|
||||||
|
_NO_AUTOGEN = """
|
||||||
|
Evennia could not generate SSH private- and public keys ({{err}})
|
||||||
|
Using conch default keys instead.
|
||||||
|
|
||||||
|
If this error persists, create the keys manually (using the tools for your OS)
|
||||||
|
and put them here:
|
||||||
|
{}
|
||||||
|
{}
|
||||||
|
""".format(_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
# not used atm
|
||||||
|
class SSHServerFactory(protocol.ServerFactory):
|
||||||
|
"This is only to name this better in logs"
|
||||||
|
noisy = False
|
||||||
|
|
||||||
|
def logPrefix(self):
|
||||||
|
return "SSH"
|
||||||
|
|
||||||
|
|
||||||
class SshProtocol(Manhole, session.Session):
|
class SshProtocol(Manhole, session.Session):
|
||||||
"""
|
"""
|
||||||
|
|
@ -66,6 +88,7 @@ class SshProtocol(Manhole, session.Session):
|
||||||
here.
|
here.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
noisy = False
|
||||||
|
|
||||||
def __init__(self, starttuple):
|
def __init__(self, starttuple):
|
||||||
"""
|
"""
|
||||||
|
|
@ -76,6 +99,7 @@ class SshProtocol(Manhole, session.Session):
|
||||||
starttuple (tuple): A (account, factory) tuple.
|
starttuple (tuple): A (account, factory) tuple.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
self.protocol_key = "ssh"
|
||||||
self.authenticated_account = starttuple[0]
|
self.authenticated_account = starttuple[0]
|
||||||
# obs must not be called self.factory, that gets overwritten!
|
# obs must not be called self.factory, that gets overwritten!
|
||||||
self.cfactory = starttuple[1]
|
self.cfactory = starttuple[1]
|
||||||
|
|
@ -104,7 +128,7 @@ class SshProtocol(Manhole, session.Session):
|
||||||
# since we might have authenticated already, we might set this here.
|
# since we might have authenticated already, we might set this here.
|
||||||
if self.authenticated_account:
|
if self.authenticated_account:
|
||||||
self.logged_in = True
|
self.logged_in = True
|
||||||
self.uid = self.authenticated_account.user.id
|
self.uid = self.authenticated_account.id
|
||||||
self.sessionhandler.connect(self)
|
self.sessionhandler.connect(self)
|
||||||
|
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
|
|
@ -228,7 +252,7 @@ class SshProtocol(Manhole, session.Session):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if reason:
|
if reason:
|
||||||
self.data_out(text=reason)
|
self.data_out(text=((reason, ), {}))
|
||||||
self.connectionLost(reason)
|
self.connectionLost(reason)
|
||||||
|
|
||||||
def data_out(self, **kwargs):
|
def data_out(self, **kwargs):
|
||||||
|
|
@ -302,6 +326,9 @@ class SshProtocol(Manhole, session.Session):
|
||||||
|
|
||||||
|
|
||||||
class ExtraInfoAuthServer(SSHUserAuthServer):
|
class ExtraInfoAuthServer(SSHUserAuthServer):
|
||||||
|
|
||||||
|
noisy = False
|
||||||
|
|
||||||
def auth_password(self, packet):
|
def auth_password(self, packet):
|
||||||
"""
|
"""
|
||||||
Password authentication.
|
Password authentication.
|
||||||
|
|
@ -327,6 +354,7 @@ class AccountDBPasswordChecker(object):
|
||||||
useful for the Realm.
|
useful for the Realm.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
noisy = False
|
||||||
credentialInterfaces = (credentials.IUsernamePassword,)
|
credentialInterfaces = (credentials.IUsernamePassword,)
|
||||||
|
|
||||||
def __init__(self, factory):
|
def __init__(self, factory):
|
||||||
|
|
@ -362,6 +390,8 @@ class PassAvatarIdTerminalRealm(TerminalRealm):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
noisy = False
|
||||||
|
|
||||||
def _getAvatar(self, avatarId):
|
def _getAvatar(self, avatarId):
|
||||||
comp = components.Componentized()
|
comp = components.Componentized()
|
||||||
user = self.userFactory(comp, avatarId)
|
user = self.userFactory(comp, avatarId)
|
||||||
|
|
@ -383,6 +413,8 @@ class TerminalSessionTransport_getPeer(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
noisy = False
|
||||||
|
|
||||||
def __init__(self, proto, chainedProtocol, avatar, width, height):
|
def __init__(self, proto, chainedProtocol, avatar, width, height):
|
||||||
self.proto = proto
|
self.proto = proto
|
||||||
self.avatar = avatar
|
self.avatar = avatar
|
||||||
|
|
@ -417,33 +449,32 @@ def getKeyPair(pubkeyfile, privkeyfile):
|
||||||
|
|
||||||
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
|
if not (os.path.exists(pubkeyfile) and os.path.exists(privkeyfile)):
|
||||||
# No keypair exists. Generate a new RSA keypair
|
# No keypair exists. Generate a new RSA keypair
|
||||||
print(" Generating SSH RSA keypair ...", end=' ')
|
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
KEY_LENGTH = 1024
|
rsa_key = Key(RSA.generate(_KEY_LENGTH))
|
||||||
rsaKey = Key(RSA.generate(KEY_LENGTH))
|
public_key_string = rsa_key.public().toString(type="OPENSSH")
|
||||||
publicKeyString = rsaKey.public().toString(type="OPENSSH")
|
private_key_string = rsa_key.toString(type="OPENSSH")
|
||||||
privateKeyString = rsaKey.toString(type="OPENSSH")
|
|
||||||
|
|
||||||
# save keys for the future.
|
# save keys for the future.
|
||||||
file(pubkeyfile, 'w+b').write(publicKeyString)
|
with open(privkeyfile, 'wt') as pfile:
|
||||||
file(privkeyfile, 'w+b').write(privateKeyString)
|
pfile.write(private_key_string)
|
||||||
print(" done.")
|
print("Created SSH private key in '{}'".format(_PRIVATE_KEY_FILE))
|
||||||
|
with open(pubkeyfile, 'wt') as pfile:
|
||||||
|
pfile.write(public_key_string)
|
||||||
|
print("Created SSH public key in '{}'".format(_PUBLIC_KEY_FILE))
|
||||||
else:
|
else:
|
||||||
publicKeyString = file(pubkeyfile).read()
|
with open(pubkeyfile) as pfile:
|
||||||
privateKeyString = file(privkeyfile).read()
|
public_key_string = pfile.read()
|
||||||
|
with open(privkeyfile) as pfile:
|
||||||
|
private_key_string = pfile.read()
|
||||||
|
|
||||||
return Key.fromString(publicKeyString), Key.fromString(privateKeyString)
|
return Key.fromString(public_key_string), Key.fromString(private_key_string)
|
||||||
|
|
||||||
|
|
||||||
def makeFactory(configdict):
|
def makeFactory(configdict):
|
||||||
"""
|
"""
|
||||||
Creates the ssh server factory.
|
Creates the ssh server factory.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pubkeyfile = os.path.join(_GAME_DIR, "server", "ssh-public.key")
|
|
||||||
privkeyfile = os.path.join(_GAME_DIR, "server", "ssh-private.key")
|
|
||||||
|
|
||||||
def chainProtocolFactory(username=None):
|
def chainProtocolFactory(username=None):
|
||||||
return insults.ServerProtocol(
|
return insults.ServerProtocol(
|
||||||
configdict['protocolFactory'],
|
configdict['protocolFactory'],
|
||||||
|
|
@ -458,14 +489,11 @@ def makeFactory(configdict):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# create/get RSA keypair
|
# create/get RSA keypair
|
||||||
publicKey, privateKey = getKeyPair(pubkeyfile, privkeyfile)
|
publicKey, privateKey = getKeyPair(_PUBLIC_KEY_FILE, _PRIVATE_KEY_FILE)
|
||||||
factory.publicKeys = {'ssh-rsa': publicKey}
|
factory.publicKeys = {'ssh-rsa': publicKey}
|
||||||
factory.privateKeys = {'ssh-rsa': privateKey}
|
factory.privateKeys = {'ssh-rsa': privateKey}
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
print("getKeyPair error: {err}\n WARNING: Evennia could not "
|
print(_NO_AUTOGEN.format(err=err))
|
||||||
"auto-generate SSH keypair. Using conch default keys instead.\n"
|
|
||||||
"If this error persists, create {pub} and "
|
|
||||||
"{priv} yourself using third-party tools.".format(err=err, pub=pubkeyfile, priv=privkeyfile))
|
|
||||||
|
|
||||||
factory.services = factory.services.copy()
|
factory.services = factory.services.copy()
|
||||||
factory.services['ssh-userauth'] = ExtraInfoAuthServer
|
factory.services['ssh-userauth'] = ExtraInfoAuthServer
|
||||||
|
|
|
||||||
|
|
@ -1,115 +0,0 @@
|
||||||
"""
|
|
||||||
This is a simple context factory for auto-creating
|
|
||||||
SSL keys and certificates.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
try:
|
|
||||||
import OpenSSL
|
|
||||||
from twisted.internet import ssl as twisted_ssl
|
|
||||||
except ImportError as error:
|
|
||||||
errstr = """
|
|
||||||
{err}
|
|
||||||
SSL requires the PyOpenSSL library and dependencies:
|
|
||||||
|
|
||||||
pip install pyopenssl pycrypto enum pyasn1 service_identity
|
|
||||||
|
|
||||||
Stop and start Evennia again. If no certificate can be generated, you'll
|
|
||||||
get a suggestion for a (linux) command to generate this locally.
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise ImportError(errstr.format(err=error))
|
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from evennia.server.portal.telnet import TelnetProtocol
|
|
||||||
|
|
||||||
_GAME_DIR = settings.GAME_DIR
|
|
||||||
|
|
||||||
# messages
|
|
||||||
|
|
||||||
NO_AUTOGEN = """
|
|
||||||
|
|
||||||
{err}
|
|
||||||
Evennia could not auto-generate the SSL private key. If this error
|
|
||||||
persists, create {keyfile} yourself using third-party tools.
|
|
||||||
"""
|
|
||||||
|
|
||||||
NO_AUTOCERT = """
|
|
||||||
|
|
||||||
{err}
|
|
||||||
Evennia's SSL context factory could not automatically, create an SSL
|
|
||||||
certificate {certfile}.
|
|
||||||
|
|
||||||
A private key {keyfile} was already created. Please create {certfile}
|
|
||||||
manually using the commands valid for your operating system, for
|
|
||||||
example (linux, using the openssl program):
|
|
||||||
{exestring}
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class SSLProtocol(TelnetProtocol):
|
|
||||||
"""
|
|
||||||
Communication is the same as telnet, except data transfer
|
|
||||||
is done with encryption.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super(SSLProtocol, self).__init__(*args, **kwargs)
|
|
||||||
self.protocol_name = "ssl"
|
|
||||||
|
|
||||||
|
|
||||||
def verify_SSL_key_and_cert(keyfile, certfile):
|
|
||||||
"""
|
|
||||||
This function looks for RSA key and certificate in the current
|
|
||||||
directory. If files ssl.key and ssl.cert does not exist, they
|
|
||||||
are created.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
|
|
||||||
# key/cert does not exist. Create.
|
|
||||||
import subprocess
|
|
||||||
from Crypto.PublicKey import RSA
|
|
||||||
from twisted.conch.ssh.keys import Key
|
|
||||||
|
|
||||||
print(" Creating SSL key and certificate ... ", end=' ')
|
|
||||||
|
|
||||||
try:
|
|
||||||
# create the RSA key and store it.
|
|
||||||
KEY_LENGTH = 1024
|
|
||||||
rsaKey = Key(RSA.generate(KEY_LENGTH))
|
|
||||||
keyString = rsaKey.toString(type="OPENSSH")
|
|
||||||
file(keyfile, 'w+b').write(keyString)
|
|
||||||
except Exception as err:
|
|
||||||
print(NO_AUTOGEN.format(err=err, keyfile=keyfile))
|
|
||||||
sys.exit(5)
|
|
||||||
|
|
||||||
# try to create the certificate
|
|
||||||
CERT_EXPIRE = 365 * 20 # twenty years validity
|
|
||||||
# default:
|
|
||||||
# openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300
|
|
||||||
exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (keyfile, certfile, CERT_EXPIRE)
|
|
||||||
try:
|
|
||||||
subprocess.call(exestring)
|
|
||||||
except OSError as err:
|
|
||||||
raise OSError(NO_AUTOCERT.format(err=err, certfile=certfile, keyfile=keyfile, exestring=exestring))
|
|
||||||
print("done.")
|
|
||||||
|
|
||||||
|
|
||||||
def getSSLContext():
|
|
||||||
"""
|
|
||||||
This is called by the portal when creating the SSL context
|
|
||||||
server-side.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
ssl_context (tuple): A key and certificate that is either
|
|
||||||
existing previously or or created on the fly.
|
|
||||||
|
|
||||||
"""
|
|
||||||
keyfile = os.path.join(_GAME_DIR, "server", "ssl.key")
|
|
||||||
certfile = os.path.join(_GAME_DIR, "server", "ssl.cert")
|
|
||||||
|
|
||||||
verify_SSL_key_and_cert(keyfile, certfile)
|
|
||||||
return twisted_ssl.DefaultOpenSSLContextFactory(keyfile, certfile)
|
|
||||||
|
|
@ -40,11 +40,9 @@ class SuppressGA(object):
|
||||||
|
|
||||||
self.protocol.protocol_flags["NOGOAHEAD"] = True
|
self.protocol.protocol_flags["NOGOAHEAD"] = True
|
||||||
# tell the client that we prefer to suppress GA ...
|
# tell the client that we prefer to suppress GA ...
|
||||||
self.protocol.will(SUPPRESS_GA).addCallbacks(self.do_suppress_ga, self.dont_suppress_ga)
|
self.protocol.will(SUPPRESS_GA).addCallbacks(self.will_suppress_ga, self.wont_suppress_ga)
|
||||||
# ... but also accept if the client really wants not to.
|
|
||||||
self.protocol.do(SUPPRESS_GA).addCallbacks(self.do_suppress_ga, self.dont_suppress_ga)
|
|
||||||
|
|
||||||
def dont_suppress_ga(self, option):
|
def wont_suppress_ga(self, option):
|
||||||
"""
|
"""
|
||||||
Called when client requests to not suppress GA.
|
Called when client requests to not suppress GA.
|
||||||
|
|
||||||
|
|
@ -55,9 +53,9 @@ class SuppressGA(object):
|
||||||
self.protocol.protocol_flags["NOGOAHEAD"] = False
|
self.protocol.protocol_flags["NOGOAHEAD"] = False
|
||||||
self.protocol.handshake_done()
|
self.protocol.handshake_done()
|
||||||
|
|
||||||
def do_suppress_ga(self, option):
|
def will_suppress_ga(self, option):
|
||||||
"""
|
"""
|
||||||
Client wants to suppress GA
|
Client will suppress GA
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
option (Option): Not used.
|
option (Option): Not used.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ sessions etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from twisted.internet import protocol
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol
|
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol
|
||||||
from twisted.conch.telnet import IAC, NOP, LINEMODE, GA, WILL, WONT, ECHO, NULL
|
from twisted.conch.telnet import IAC, NOP, LINEMODE, GA, WILL, WONT, ECHO, NULL
|
||||||
|
|
@ -25,6 +26,15 @@ _RE_LINEBREAK = re.compile(r"\n\r|\r\n|\n|\r", re.DOTALL + re.MULTILINE)
|
||||||
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
|
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
|
||||||
_IDLE_COMMAND = settings.IDLE_COMMAND + "\n"
|
_IDLE_COMMAND = settings.IDLE_COMMAND + "\n"
|
||||||
|
|
||||||
|
|
||||||
|
class TelnetServerFactory(protocol.ServerFactory):
|
||||||
|
"This is only to name this better in logs"
|
||||||
|
noisy = False
|
||||||
|
|
||||||
|
def logPrefix(self):
|
||||||
|
return "Telnet"
|
||||||
|
|
||||||
|
|
||||||
class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
"""
|
"""
|
||||||
Each player connecting over telnet (ie using most traditional mud
|
Each player connecting over telnet (ie using most traditional mud
|
||||||
|
|
@ -33,8 +43,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.protocol_name = "telnet"
|
|
||||||
super(TelnetProtocol, self).__init__(*args, **kwargs)
|
super(TelnetProtocol, self).__init__(*args, **kwargs)
|
||||||
|
self.protocol_key = "telnet"
|
||||||
|
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -48,9 +58,13 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
# this number is counted down for every handshake that completes.
|
# this number is counted down for every handshake that completes.
|
||||||
# when it reaches 0 the portal/server syncs their data
|
# when it reaches 0 the portal/server syncs their data
|
||||||
self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp
|
self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp
|
||||||
self.init_session(self.protocol_name, client_address, self.factory.sessionhandler)
|
|
||||||
# change encoding to ENCODINGS[0] which reflects Telnet default encoding
|
self.init_session(self.protocol_key, client_address, self.factory.sessionhandler)
|
||||||
self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else 'utf-8'
|
self.protocol_flags["ENCODING"] = settings.ENCODINGS[0] if settings.ENCODINGS else 'utf-8'
|
||||||
|
# add this new connection to sessionhandler so
|
||||||
|
# the Server becomes aware of it.
|
||||||
|
self.sessionhandler.connect(self)
|
||||||
|
# change encoding to ENCODINGS[0] which reflects Telnet default encoding
|
||||||
|
|
||||||
# suppress go-ahead
|
# suppress go-ahead
|
||||||
self.sga = suppress_ga.SuppressGA(self)
|
self.sga = suppress_ga.SuppressGA(self)
|
||||||
|
|
@ -67,13 +81,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
self.oob = telnet_oob.TelnetOOB(self)
|
self.oob = telnet_oob.TelnetOOB(self)
|
||||||
# mxp support
|
# mxp support
|
||||||
self.mxp = Mxp(self)
|
self.mxp = Mxp(self)
|
||||||
# add this new connection to sessionhandler so
|
|
||||||
# the Server becomes aware of it.
|
|
||||||
self.sessionhandler.connect(self)
|
|
||||||
|
|
||||||
# timeout the handshakes in case the client doesn't reply at all
|
|
||||||
from evennia.utils.utils import delay
|
from evennia.utils.utils import delay
|
||||||
delay(2, callback=self.handshake_done, force=True)
|
# timeout the handshakes in case the client doesn't reply at all
|
||||||
|
delay(2, callback=self.handshake_done, timeout=True)
|
||||||
|
|
||||||
# TCP/IP keepalive watches for dead links
|
# TCP/IP keepalive watches for dead links
|
||||||
self.transport.setTcpKeepAlive(1)
|
self.transport.setTcpKeepAlive(1)
|
||||||
|
|
@ -101,17 +112,18 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
self.nop_keep_alive = LoopingCall(self._send_nop_keepalive)
|
self.nop_keep_alive = LoopingCall(self._send_nop_keepalive)
|
||||||
self.nop_keep_alive.start(30, now=False)
|
self.nop_keep_alive.start(30, now=False)
|
||||||
|
|
||||||
def handshake_done(self, force=False):
|
def handshake_done(self, timeout=False):
|
||||||
"""
|
"""
|
||||||
This is called by all telnet extensions once they are finished.
|
This is called by all telnet extensions once they are finished.
|
||||||
When all have reported, a sync with the server is performed.
|
When all have reported, a sync with the server is performed.
|
||||||
The system will force-call this sync after a small time to handle
|
The system will force-call this sync after a small time to handle
|
||||||
clients that don't reply to handshakes at all.
|
clients that don't reply to handshakes at all.
|
||||||
"""
|
"""
|
||||||
|
if timeout:
|
||||||
if self.handshakes > 0:
|
if self.handshakes > 0:
|
||||||
if force:
|
self.handshakes = 0
|
||||||
self.sessionhandler.sync(self)
|
self.sessionhandler.sync(self)
|
||||||
return
|
else:
|
||||||
self.handshakes -= 1
|
self.handshakes -= 1
|
||||||
if self.handshakes <= 0:
|
if self.handshakes <= 0:
|
||||||
# do the sync
|
# do the sync
|
||||||
|
|
@ -231,9 +243,11 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
line (str): Line to send.
|
line (str): Line to send.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# escape IAC in line mode, and correctly add \r\n
|
# escape IAC in line mode, and correctly add \r\n (the TELNET end-of-line)
|
||||||
line += self.delimiter
|
line = line.replace(IAC, IAC + IAC)
|
||||||
line = line.replace(IAC, IAC + IAC).replace('\n', '\r\n')
|
line = line.replace('\n', '\r\n')
|
||||||
|
if not line.endswith("\r\n") and self.protocol_flags.get("FORCEDENDLINE", True):
|
||||||
|
line += "\r\n"
|
||||||
if not self.protocol_flags.get("NOGOAHEAD", True):
|
if not self.protocol_flags.get("NOGOAHEAD", True):
|
||||||
line += IAC + GA
|
line += IAC + GA
|
||||||
return self.transport.write(mccp_compress(self, line))
|
return self.transport.write(mccp_compress(self, line))
|
||||||
|
|
@ -306,8 +320,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
# handle arguments
|
# handle arguments
|
||||||
options = kwargs.get("options", {})
|
options = kwargs.get("options", {})
|
||||||
flags = self.protocol_flags
|
flags = self.protocol_flags
|
||||||
xterm256 = options.get("xterm256", flags.get('XTERM256', False) if flags["TTYPE"] else True)
|
xterm256 = options.get("xterm256", flags.get('XTERM256', False) if flags.get("TTYPE", False) else True)
|
||||||
useansi = options.get("ansi", flags.get('ANSI', False) if flags["TTYPE"] else True)
|
useansi = options.get("ansi", flags.get('ANSI', False) if flags.get("TTYPE", False) else True)
|
||||||
raw = options.get("raw", flags.get("RAW", False))
|
raw = options.get("raw", flags.get("RAW", False))
|
||||||
nocolor = options.get("nocolor", flags.get("NOCOLOR") or not (xterm256 or useansi))
|
nocolor = options.get("nocolor", flags.get("NOCOLOR") or not (xterm256 or useansi))
|
||||||
echo = options.get("echo", None)
|
echo = options.get("echo", None)
|
||||||
|
|
|
||||||
|
|
@ -227,26 +227,45 @@ class TelnetOOB(object):
|
||||||
GMCP messages will be outgoing on the following
|
GMCP messages will be outgoing on the following
|
||||||
form (the non-JSON cmdname at the start is what
|
form (the non-JSON cmdname at the start is what
|
||||||
IRE games use, supposedly, and what clients appear
|
IRE games use, supposedly, and what clients appear
|
||||||
to have adopted):
|
to have adopted). A cmdname without Package will end
|
||||||
|
up in the Core package, while Core package names will
|
||||||
|
be stripped on the Evennia side.
|
||||||
|
|
||||||
[cmdname, [], {}] -> cmdname
|
[cmd.name, [], {}] -> Cmd.Name
|
||||||
[cmdname, [arg], {}] -> cmdname arg
|
[cmd.name, [arg], {}] -> Cmd.Name arg
|
||||||
[cmdname, [args],{}] -> cmdname [args]
|
[cmd.name, [args],{}] -> Cmd.Name [args]
|
||||||
[cmdname, [], {kwargs}] -> cmdname {kwargs}
|
[cmd.name, [], {kwargs}] -> Cmd.Name {kwargs}
|
||||||
[cmdname, [args, {kwargs}] -> cmdname [[args],{kwargs}]
|
[cmdname, [args, {kwargs}] -> Core.Cmdname [[args],{kwargs}]
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
There are also a few default mappings between evennia outputcmds and
|
||||||
|
GMCP:
|
||||||
|
client_options -> Core.Supports.Get
|
||||||
|
get_inputfuncs -> Core.Commands.Get
|
||||||
|
get_value -> Char.Value.Get
|
||||||
|
repeat -> Char.Repeat.Update
|
||||||
|
monitor -> Char.Monitor.Update
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if cmdname in EVENNIA_TO_GMCP:
|
||||||
|
gmcp_cmdname = EVENNIA_TO_GMCP[cmdname]
|
||||||
|
elif "_" in cmdname:
|
||||||
|
gmcp_cmdname = ".".join(word.capitalize() for word in cmdname.split("_"))
|
||||||
|
else:
|
||||||
|
gmcp_cmdname = "Core.%s" % cmdname.capitalize()
|
||||||
|
|
||||||
if not (args or kwargs):
|
if not (args or kwargs):
|
||||||
gmcp_string = cmdname
|
gmcp_string = gmcp_cmdname
|
||||||
elif args:
|
elif args:
|
||||||
if len(args) == 1:
|
if len(args) == 1:
|
||||||
args = args[0]
|
args = args[0]
|
||||||
if kwargs:
|
if kwargs:
|
||||||
gmcp_string = "%s %s" % (cmdname, json.dumps([args, kwargs]))
|
gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps([args, kwargs]))
|
||||||
else:
|
else:
|
||||||
gmcp_string = "%s %s" % (cmdname, json.dumps(args))
|
gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps(args))
|
||||||
else: # only kwargs
|
else: # only kwargs
|
||||||
gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs))
|
gmcp_string = "%s %s" % (gmcp_cmdname, json.dumps(kwargs))
|
||||||
|
|
||||||
# print("gmcp string", gmcp_string) # DEBUG
|
# print("gmcp string", gmcp_string) # DEBUG
|
||||||
return gmcp_string
|
return gmcp_string
|
||||||
|
|
@ -398,14 +417,9 @@ class TelnetOOB(object):
|
||||||
kwargs.pop("options", None)
|
kwargs.pop("options", None)
|
||||||
|
|
||||||
if self.MSDP:
|
if self.MSDP:
|
||||||
msdp_cmdname = cmdname
|
encoded_oob = self.encode_msdp(cmdname, *args, **kwargs)
|
||||||
encoded_oob = self.encode_msdp(msdp_cmdname, *args, **kwargs)
|
|
||||||
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
|
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
|
||||||
|
|
||||||
if self.GMCP:
|
if self.GMCP:
|
||||||
if cmdname in EVENNIA_TO_GMCP:
|
encoded_oob = self.encode_gmcp(cmdname, *args, **kwargs)
|
||||||
gmcp_cmdname = EVENNIA_TO_GMCP[cmdname]
|
|
||||||
else:
|
|
||||||
gmcp_cmdname = "Custom.Cmd"
|
|
||||||
encoded_oob = self.encode_gmcp(gmcp_cmdname, *args, **kwargs)
|
|
||||||
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)
|
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)
|
||||||
|
|
|
||||||
146
evennia/server/portal/telnet_ssl.py
Normal file
146
evennia/server/portal/telnet_ssl.py
Normal file
|
|
@ -0,0 +1,146 @@
|
||||||
|
"""
|
||||||
|
This allows for running the telnet communication over an encrypted SSL tunnel. To use it, requires a
|
||||||
|
client supporting Telnet SSL.
|
||||||
|
|
||||||
|
The protocol will try to automatically create the private key and certificate on the server side
|
||||||
|
when starting and will warn if this was not possible. These will appear as files ssl.key and
|
||||||
|
ssl.cert in mygame/server/.
|
||||||
|
|
||||||
|
"""
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
from OpenSSL import crypto
|
||||||
|
from twisted.internet import ssl as twisted_ssl
|
||||||
|
except ImportError as error:
|
||||||
|
errstr = """
|
||||||
|
{err}
|
||||||
|
Telnet-SSL requires the PyOpenSSL library and dependencies:
|
||||||
|
|
||||||
|
pip install pyopenssl pycrypto enum pyasn1 service_identity
|
||||||
|
|
||||||
|
Stop and start Evennia again. If no certificate can be generated, you'll
|
||||||
|
get a suggestion for a (linux) command to generate this locally.
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise ImportError(errstr.format(err=error))
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from evennia.server.portal.telnet import TelnetProtocol
|
||||||
|
|
||||||
|
_GAME_DIR = settings.GAME_DIR
|
||||||
|
|
||||||
|
_PRIVATE_KEY_LENGTH = 2048
|
||||||
|
_PRIVATE_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl.key")
|
||||||
|
_PUBLIC_KEY_FILE = os.path.join(_GAME_DIR, "server", "ssl-public.key")
|
||||||
|
_CERTIFICATE_FILE = os.path.join(_GAME_DIR, "server", "ssl.cert")
|
||||||
|
_CERTIFICATE_EXPIRE = 365 * 24 * 60 * 60 * 20 # 20 years
|
||||||
|
_CERTIFICATE_ISSUER = {"C": "EV", "ST": "Evennia", "L": "Evennia", "O":
|
||||||
|
"Evennia Security", "OU": "Evennia Department", "CN": "evennia"}
|
||||||
|
|
||||||
|
# messages
|
||||||
|
|
||||||
|
NO_AUTOGEN = """
|
||||||
|
Evennia could not auto-generate the SSL private- and public keys ({{err}}).
|
||||||
|
If this error persists, create them manually (using the tools for your OS). The files
|
||||||
|
should be placed and named like this:
|
||||||
|
{}
|
||||||
|
{}
|
||||||
|
""".format(_PRIVATE_KEY_FILE, _PUBLIC_KEY_FILE)
|
||||||
|
|
||||||
|
NO_AUTOCERT = """
|
||||||
|
Evennia's could not auto-generate the SSL certificate ({{err}}).
|
||||||
|
The private key already exists here:
|
||||||
|
{}
|
||||||
|
If this error persists, create the certificate manually (using the private key and
|
||||||
|
the tools for your OS). The file should be placed and named like this:
|
||||||
|
{}
|
||||||
|
""".format(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
class SSLProtocol(TelnetProtocol):
|
||||||
|
"""
|
||||||
|
Communication is the same as telnet, except data transfer
|
||||||
|
is done with encryption set up by the portal at start time.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(SSLProtocol, self).__init__(*args, **kwargs)
|
||||||
|
self.protocol_key = "telnet/ssl"
|
||||||
|
|
||||||
|
|
||||||
|
def verify_or_create_SSL_key_and_cert(keyfile, certfile):
|
||||||
|
"""
|
||||||
|
Verify or create new key/certificate files.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyfile (str): Path to ssl.key file.
|
||||||
|
certfile (str): Parth to ssl.cert file.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If files don't already exist, they are created.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not (os.path.exists(keyfile) and os.path.exists(certfile)):
|
||||||
|
# key/cert does not exist. Create.
|
||||||
|
try:
|
||||||
|
# generate the keypair
|
||||||
|
keypair = crypto.PKey()
|
||||||
|
keypair.generate_key(crypto.TYPE_RSA, _PRIVATE_KEY_LENGTH)
|
||||||
|
|
||||||
|
with open(_PRIVATE_KEY_FILE, 'wt') as pfile:
|
||||||
|
pfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair))
|
||||||
|
print("Created SSL private key in '{}'.".format(_PRIVATE_KEY_FILE))
|
||||||
|
|
||||||
|
with open(_PUBLIC_KEY_FILE, 'wt') as pfile:
|
||||||
|
pfile.write(crypto.dump_publickey(crypto.FILETYPE_PEM, keypair))
|
||||||
|
print("Created SSL public key in '{}'.".format(_PUBLIC_KEY_FILE))
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
print(NO_AUTOGEN.format(err=err))
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
try:
|
||||||
|
# create certificate
|
||||||
|
cert = crypto.X509()
|
||||||
|
subj = cert.get_subject()
|
||||||
|
for key, value in _CERTIFICATE_ISSUER.items():
|
||||||
|
setattr(subj, key, value)
|
||||||
|
cert.set_issuer(subj)
|
||||||
|
|
||||||
|
cert.set_serial_number(1000)
|
||||||
|
cert.gmtime_adj_notBefore(0)
|
||||||
|
cert.gmtime_adj_notAfter(_CERTIFICATE_EXPIRE)
|
||||||
|
cert.set_pubkey(keypair)
|
||||||
|
cert.sign(keypair, 'sha1')
|
||||||
|
|
||||||
|
with open(_CERTIFICATE_FILE, 'wt') as cfile:
|
||||||
|
cfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||||
|
print("Created SSL certificate in '{}'.".format(_CERTIFICATE_FILE))
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
print(NO_AUTOCERT.format(err=err))
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def getSSLContext():
|
||||||
|
"""
|
||||||
|
This is called by the portal when creating the SSL context
|
||||||
|
server-side.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ssl_context (tuple): A key and certificate that is either
|
||||||
|
existing previously or created on the fly.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if verify_or_create_SSL_key_and_cert(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE):
|
||||||
|
return twisted_ssl.DefaultOpenSSLContextFactory(_PRIVATE_KEY_FILE, _CERTIFICATE_FILE)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
@ -50,6 +50,8 @@ class Ttype(object):
|
||||||
"""
|
"""
|
||||||
self.ttype_step = 0
|
self.ttype_step = 0
|
||||||
self.protocol = protocol
|
self.protocol = protocol
|
||||||
|
# we set FORCEDENDLINE for clients not supporting ttype
|
||||||
|
self.protocol.protocol_flags["FORCEDENDLINE"] = True
|
||||||
self.protocol.protocol_flags['TTYPE'] = False
|
self.protocol.protocol_flags['TTYPE'] = False
|
||||||
# is it a safe bet to assume ANSI is always supported?
|
# is it a safe bet to assume ANSI is always supported?
|
||||||
self.protocol.protocol_flags['ANSI'] = True
|
self.protocol.protocol_flags['ANSI'] = True
|
||||||
|
|
@ -66,7 +68,7 @@ class Ttype(object):
|
||||||
option (Option): Not used.
|
option (Option): Not used.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.protocol.protocol_flags['TTYPE'] = True
|
self.protocol.protocol_flags['TTYPE'] = False
|
||||||
self.protocol.handshake_done()
|
self.protocol.handshake_done()
|
||||||
|
|
||||||
def will_ttype(self, option):
|
def will_ttype(self, option):
|
||||||
|
|
@ -107,11 +109,16 @@ class Ttype(object):
|
||||||
# only support after a certain version, but all support
|
# only support after a certain version, but all support
|
||||||
# it since at least 4 years. We assume recent client here for now.
|
# it since at least 4 years. We assume recent client here for now.
|
||||||
cupper = clientname.upper()
|
cupper = clientname.upper()
|
||||||
|
xterm256 = False
|
||||||
if cupper.startswith("MUDLET"):
|
if cupper.startswith("MUDLET"):
|
||||||
# supports xterm256 stably since 1.1 (2010?)
|
# supports xterm256 stably since 1.1 (2010?)
|
||||||
xterm256 = cupper.split("MUDLET", 1)[1].strip() >= "1.1"
|
xterm256 = cupper.split("MUDLET", 1)[1].strip() >= "1.1"
|
||||||
else:
|
self.protocol.protocol_flags["FORCEDENDLINE"] = False
|
||||||
xterm256 = (cupper.startswith("XTERM") or
|
|
||||||
|
if cupper.startswith("TINTIN++"):
|
||||||
|
self.protocol.protocol_flags["FORCEDENDLINE"] = False
|
||||||
|
|
||||||
|
if (cupper.startswith("XTERM") or
|
||||||
cupper.endswith("-256COLOR") or
|
cupper.endswith("-256COLOR") or
|
||||||
cupper in ("ATLANTIS", # > 0.9.9.0 (aug 2009)
|
cupper in ("ATLANTIS", # > 0.9.9.0 (aug 2009)
|
||||||
"CMUD", # > 3.04 (mar 2009)
|
"CMUD", # > 3.04 (mar 2009)
|
||||||
|
|
@ -120,7 +127,10 @@ class Ttype(object):
|
||||||
"MUSHCLIENT", # > 4.02 (apr 2007)
|
"MUSHCLIENT", # > 4.02 (apr 2007)
|
||||||
"PUTTY", # > 0.58 (apr 2005)
|
"PUTTY", # > 0.58 (apr 2005)
|
||||||
"BEIP", # > 2.00.206 (late 2009) (BeipMu)
|
"BEIP", # > 2.00.206 (late 2009) (BeipMu)
|
||||||
"POTATO")) # > 2.00 (maybe earlier)
|
"POTATO", # > 2.00 (maybe earlier)
|
||||||
|
"TINYFUGUE" # > 4.x (maybe earlier)
|
||||||
|
)):
|
||||||
|
xterm256 = True
|
||||||
|
|
||||||
# all clients supporting TTYPE at all seem to support ANSI
|
# all clients supporting TTYPE at all seem to support ANSI
|
||||||
self.protocol.protocol_flags['ANSI'] = True
|
self.protocol.protocol_flags['ANSI'] = True
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ class WebSocketClient(Protocol, Session):
|
||||||
"""
|
"""
|
||||||
Implements the server-side of the Websocket connection.
|
Implements the server-side of the Websocket connection.
|
||||||
"""
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(WebSocketClient, self).__init__(*args, **kwargs)
|
||||||
|
self.protocol_key = "webclient/websocket"
|
||||||
|
|
||||||
def connectionMade(self):
|
def connectionMade(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -298,7 +298,7 @@ class AjaxWebClientSession(session.Session):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.protocol_name = "ajax/comet"
|
self.protocol_key = "webclient/ajax"
|
||||||
super(AjaxWebClientSession, self).__init__(*args, **kwargs)
|
super(AjaxWebClientSession, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def get_client_session(self):
|
def get_client_session(self):
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@ ERROR_NO_MIXIN = \
|
||||||
error completely.
|
error completely.
|
||||||
|
|
||||||
Warning: Don't run dummyrunner on a production database! It will
|
Warning: Don't run dummyrunner on a production database! It will
|
||||||
create a lot of spammy objects and account accounts!
|
create a lot of spammy objects and accounts!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ sets up all the networking features. (this is done automatically
|
||||||
by evennia/server/server_runner.py).
|
by evennia/server/server_runner.py).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from __future__ import print_function
|
|
||||||
from builtins import object
|
from builtins import object
|
||||||
import time
|
import time
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -17,6 +16,7 @@ from twisted.web import static
|
||||||
from twisted.application import internet, service
|
from twisted.application import internet, service
|
||||||
from twisted.internet import reactor, defer
|
from twisted.internet import reactor, defer
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
|
from twisted.python.log import ILogObserver
|
||||||
|
|
||||||
import django
|
import django
|
||||||
django.setup()
|
django.setup()
|
||||||
|
|
@ -33,6 +33,7 @@ from evennia.server.models import ServerConfig
|
||||||
from evennia.server import initial_setup
|
from evennia.server import initial_setup
|
||||||
|
|
||||||
from evennia.utils.utils import get_evennia_version, mod_import, make_iter
|
from evennia.utils.utils import get_evennia_version, mod_import, make_iter
|
||||||
|
from evennia.utils import logger
|
||||||
from evennia.comms import channelhandler
|
from evennia.comms import channelhandler
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
|
|
||||||
|
|
@ -40,11 +41,6 @@ from django.utils.translation import ugettext as _
|
||||||
|
|
||||||
_SA = object.__setattr__
|
_SA = object.__setattr__
|
||||||
|
|
||||||
SERVER_PIDFILE = ""
|
|
||||||
if os.name == 'nt':
|
|
||||||
# For Windows we need to handle pid files manually.
|
|
||||||
SERVER_PIDFILE = os.path.join(settings.GAME_DIR, "server", 'server.pid')
|
|
||||||
|
|
||||||
# a file with a flag telling the server to restart after shutdown or not.
|
# a file with a flag telling the server to restart after shutdown or not.
|
||||||
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", 'server.restart')
|
SERVER_RESTART = os.path.join(settings.GAME_DIR, "server", 'server.restart')
|
||||||
|
|
||||||
|
|
@ -53,16 +49,11 @@ SERVER_STARTSTOP_MODULE = mod_import(settings.AT_SERVER_STARTSTOP_MODULE)
|
||||||
|
|
||||||
# modules containing plugin services
|
# modules containing plugin services
|
||||||
SERVER_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)]
|
SERVER_SERVICES_PLUGIN_MODULES = [mod_import(module) for module in make_iter(settings.SERVER_SERVICES_PLUGIN_MODULES)]
|
||||||
try:
|
|
||||||
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
|
|
||||||
except ImportError:
|
|
||||||
WEB_PLUGINS_MODULE = None
|
|
||||||
print ("WARNING: settings.WEB_PLUGINS_MODULE not found - "
|
|
||||||
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf.")
|
|
||||||
|
|
||||||
#------------------------------------------------------------
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
# Evennia Server settings
|
# Evennia Server settings
|
||||||
#------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
SERVERNAME = settings.SERVERNAME
|
SERVERNAME = settings.SERVERNAME
|
||||||
VERSION = get_evennia_version()
|
VERSION = get_evennia_version()
|
||||||
|
|
@ -83,6 +74,17 @@ IRC_ENABLED = settings.IRC_ENABLED
|
||||||
RSS_ENABLED = settings.RSS_ENABLED
|
RSS_ENABLED = settings.RSS_ENABLED
|
||||||
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
WEBCLIENT_ENABLED = settings.WEBCLIENT_ENABLED
|
||||||
|
|
||||||
|
INFO_DICT = {"servername": SERVERNAME, "version": VERSION,
|
||||||
|
"amp": "", "errors": "", "info": "", "webserver": "", "irc_rss": ""}
|
||||||
|
|
||||||
|
try:
|
||||||
|
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
|
||||||
|
except ImportError:
|
||||||
|
WEB_PLUGINS_MODULE = None
|
||||||
|
INFO_DICT["errors"] = (
|
||||||
|
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
|
||||||
|
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf.")
|
||||||
|
|
||||||
|
|
||||||
# Maintenance function - this is called repeatedly by the server
|
# Maintenance function - this is called repeatedly by the server
|
||||||
|
|
||||||
|
|
@ -127,6 +129,10 @@ def _server_maintenance():
|
||||||
if _MAINTENANCE_COUNT % 3700 == 0:
|
if _MAINTENANCE_COUNT % 3700 == 0:
|
||||||
# validate channels off-sync with scripts
|
# validate channels off-sync with scripts
|
||||||
evennia.CHANNEL_HANDLER.update()
|
evennia.CHANNEL_HANDLER.update()
|
||||||
|
if _MAINTENANCE_COUNT % (3600 * 7) == 0:
|
||||||
|
# drop database connection every 7 hrs to avoid default timeouts on MySQL
|
||||||
|
# (see https://github.com/evennia/evennia/issues/1376)
|
||||||
|
connection.close()
|
||||||
|
|
||||||
# handle idle timeouts
|
# handle idle timeouts
|
||||||
if _IDLE_TIMEOUT > 0:
|
if _IDLE_TIMEOUT > 0:
|
||||||
|
|
@ -137,11 +143,6 @@ def _server_maintenance():
|
||||||
session.account.access(session.account, "noidletimeout", default=False):
|
session.account.access(session.account, "noidletimeout", default=False):
|
||||||
SESSIONS.disconnect(session, reason=reason)
|
SESSIONS.disconnect(session, reason=reason)
|
||||||
|
|
||||||
# Commenting this out, it is probably not needed
|
|
||||||
# with CONN_MAX_AGE set. Keeping it as a reminder
|
|
||||||
# if database-gone-away errors appears again /Griatch
|
|
||||||
# if _MAINTENANCE_COUNT % 18000 == 0:
|
|
||||||
# connection.close()
|
|
||||||
maintenance_task = LoopingCall(_server_maintenance)
|
maintenance_task = LoopingCall(_server_maintenance)
|
||||||
maintenance_task.start(60, now=True) # call every minute
|
maintenance_task.start(60, now=True) # call every minute
|
||||||
|
|
||||||
|
|
@ -173,6 +174,7 @@ class Evennia(object):
|
||||||
self.amp_protocol = None # set by amp factory
|
self.amp_protocol = None # set by amp factory
|
||||||
self.sessions = SESSIONS
|
self.sessions = SESSIONS
|
||||||
self.sessions.server = self
|
self.sessions.server = self
|
||||||
|
self.process_id = os.getpid()
|
||||||
|
|
||||||
# Database-specific startup optimizations.
|
# Database-specific startup optimizations.
|
||||||
self.sqlite3_prep()
|
self.sqlite3_prep()
|
||||||
|
|
@ -193,18 +195,13 @@ class Evennia(object):
|
||||||
from twisted.internet.defer import Deferred
|
from twisted.internet.defer import Deferred
|
||||||
if hasattr(self, "web_root"):
|
if hasattr(self, "web_root"):
|
||||||
d = self.web_root.empty_threadpool()
|
d = self.web_root.empty_threadpool()
|
||||||
d.addCallback(lambda _: self.shutdown(_reactor_stopping=True))
|
d.addCallback(lambda _: self.shutdown("shutdown", _reactor_stopping=True))
|
||||||
else:
|
else:
|
||||||
d = Deferred(lambda _: self.shutdown(_reactor_stopping=True))
|
d = Deferred(lambda _: self.shutdown("shutdown", _reactor_stopping=True))
|
||||||
d.addCallback(lambda _: reactor.stop())
|
d.addCallback(lambda _: reactor.stop())
|
||||||
reactor.callLater(1, d.callback, None)
|
reactor.callLater(1, d.callback, None)
|
||||||
reactor.sigInt = _wrap_sigint_handler
|
reactor.sigInt = _wrap_sigint_handler
|
||||||
|
|
||||||
self.game_running = True
|
|
||||||
|
|
||||||
# track the server time
|
|
||||||
self.run_init_hooks()
|
|
||||||
|
|
||||||
# Server startup methods
|
# Server startup methods
|
||||||
|
|
||||||
def sqlite3_prep(self):
|
def sqlite3_prep(self):
|
||||||
|
|
@ -212,7 +209,8 @@ class Evennia(object):
|
||||||
Optimize some SQLite stuff at startup since we
|
Optimize some SQLite stuff at startup since we
|
||||||
can't save it to the database.
|
can't save it to the database.
|
||||||
"""
|
"""
|
||||||
if ((".".join(str(i) for i in django.VERSION) < "1.2" and settings.DATABASES.get('default', {}).get('ENGINE') == "sqlite3") or
|
if ((".".join(str(i) for i in django.VERSION) < "1.2" and
|
||||||
|
settings.DATABASES.get('default', {}).get('ENGINE') == "sqlite3") or
|
||||||
(hasattr(settings, 'DATABASES') and
|
(hasattr(settings, 'DATABASES') and
|
||||||
settings.DATABASES.get("default", {}).get('ENGINE', None) ==
|
settings.DATABASES.get("default", {}).get('ENGINE', None) ==
|
||||||
'django.db.backends.sqlite3')):
|
'django.db.backends.sqlite3')):
|
||||||
|
|
@ -230,6 +228,8 @@ class Evennia(object):
|
||||||
typeclasses in the settings file and have them auto-update all
|
typeclasses in the settings file and have them auto-update all
|
||||||
already existing objects.
|
already existing objects.
|
||||||
"""
|
"""
|
||||||
|
global INFO_DICT
|
||||||
|
|
||||||
# setting names
|
# setting names
|
||||||
settings_names = ("CMDSET_CHARACTER", "CMDSET_ACCOUNT",
|
settings_names = ("CMDSET_CHARACTER", "CMDSET_ACCOUNT",
|
||||||
"BASE_ACCOUNT_TYPECLASS", "BASE_OBJECT_TYPECLASS",
|
"BASE_ACCOUNT_TYPECLASS", "BASE_OBJECT_TYPECLASS",
|
||||||
|
|
@ -248,7 +248,7 @@ class Evennia(object):
|
||||||
#from evennia.accounts.models import AccountDB
|
#from evennia.accounts.models import AccountDB
|
||||||
for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches):
|
for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches):
|
||||||
# update the database
|
# update the database
|
||||||
print(" %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (settings_names[i], prev, curr))
|
INFO_DICT['info'] = " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (settings_names[i], prev, curr)
|
||||||
if i == 0:
|
if i == 0:
|
||||||
ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(db_cmdset_storage=curr)
|
ObjectDB.objects.filter(db_cmdset_storage__exact=prev).update(db_cmdset_storage=curr)
|
||||||
if i == 1:
|
if i == 1:
|
||||||
|
|
@ -278,29 +278,31 @@ class Evennia(object):
|
||||||
It returns if this is not the first time the server starts.
|
It returns if this is not the first time the server starts.
|
||||||
Once finished the last_initial_setup_step is set to -1.
|
Once finished the last_initial_setup_step is set to -1.
|
||||||
"""
|
"""
|
||||||
|
global INFO_DICT
|
||||||
last_initial_setup_step = ServerConfig.objects.conf('last_initial_setup_step')
|
last_initial_setup_step = ServerConfig.objects.conf('last_initial_setup_step')
|
||||||
if not last_initial_setup_step:
|
if not last_initial_setup_step:
|
||||||
# None is only returned if the config does not exist,
|
# None is only returned if the config does not exist,
|
||||||
# i.e. this is an empty DB that needs populating.
|
# i.e. this is an empty DB that needs populating.
|
||||||
print(' Server started for the first time. Setting defaults.')
|
INFO_DICT['info'] = ' Server started for the first time. Setting defaults.'
|
||||||
initial_setup.handle_setup(0)
|
initial_setup.handle_setup(0)
|
||||||
print('-' * 50)
|
|
||||||
elif int(last_initial_setup_step) >= 0:
|
elif int(last_initial_setup_step) >= 0:
|
||||||
# a positive value means the setup crashed on one of its
|
# a positive value means the setup crashed on one of its
|
||||||
# modules and setup will resume from this step, retrying
|
# modules and setup will resume from this step, retrying
|
||||||
# the last failed module. When all are finished, the step
|
# the last failed module. When all are finished, the step
|
||||||
# is set to -1 to show it does not need to be run again.
|
# is set to -1 to show it does not need to be run again.
|
||||||
print(' Resuming initial setup from step %(last)s.' %
|
INFO_DICT['info'] = ' Resuming initial setup from step {last}.'.format(
|
||||||
{'last': last_initial_setup_step})
|
last=last_initial_setup_step)
|
||||||
initial_setup.handle_setup(int(last_initial_setup_step))
|
initial_setup.handle_setup(int(last_initial_setup_step))
|
||||||
print('-' * 50)
|
|
||||||
|
|
||||||
def run_init_hooks(self):
|
def run_init_hooks(self, mode):
|
||||||
"""
|
"""
|
||||||
Called every server start
|
Called by the amp client once receiving sync back from Portal
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode (str): One of shutdown, reload or reset
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
#from evennia.accounts.models import AccountDB
|
|
||||||
|
|
||||||
# update eventual changed defaults
|
# update eventual changed defaults
|
||||||
self.update_defaults()
|
self.update_defaults()
|
||||||
|
|
@ -308,47 +310,24 @@ class Evennia(object):
|
||||||
[o.at_init() for o in ObjectDB.get_all_cached_instances()]
|
[o.at_init() for o in ObjectDB.get_all_cached_instances()]
|
||||||
[p.at_init() for p in AccountDB.get_all_cached_instances()]
|
[p.at_init() for p in AccountDB.get_all_cached_instances()]
|
||||||
|
|
||||||
mode = self.getset_restart_mode()
|
|
||||||
|
|
||||||
# call correct server hook based on start file value
|
# call correct server hook based on start file value
|
||||||
if mode == 'reload':
|
if mode == 'reload':
|
||||||
# True was the old reload flag, kept for compatibilty
|
logger.log_msg("Server successfully reloaded.")
|
||||||
self.at_server_reload_start()
|
self.at_server_reload_start()
|
||||||
elif mode == 'reset':
|
elif mode == 'reset':
|
||||||
# only run hook, don't purge sessions
|
# only run hook, don't purge sessions
|
||||||
self.at_server_cold_start()
|
self.at_server_cold_start()
|
||||||
elif mode in ('reset', 'shutdown'):
|
logger.log_msg("Evennia Server successfully restarted in 'reset' mode.")
|
||||||
|
elif mode == 'shutdown':
|
||||||
self.at_server_cold_start()
|
self.at_server_cold_start()
|
||||||
# clear eventual lingering session storages
|
# clear eventual lingering session storages
|
||||||
ObjectDB.objects.clear_all_sessids()
|
ObjectDB.objects.clear_all_sessids()
|
||||||
|
logger.log_msg("Evennia Server successfully started.")
|
||||||
# always call this regardless of start type
|
# always call this regardless of start type
|
||||||
self.at_server_start()
|
self.at_server_start()
|
||||||
|
|
||||||
def getset_restart_mode(self, mode=None):
|
|
||||||
"""
|
|
||||||
This manages the flag file that tells the runner if the server is
|
|
||||||
reloading, resetting or shutting down.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
mode (string or None, optional): Valid values are
|
|
||||||
'reload', 'reset', 'shutdown' and `None`. If mode is `None`,
|
|
||||||
no change will be done to the flag file.
|
|
||||||
Returns:
|
|
||||||
mode (str): The currently active restart mode, either just
|
|
||||||
set or previously set.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if mode is None:
|
|
||||||
with open(SERVER_RESTART, 'r') as f:
|
|
||||||
# mode is either shutdown, reset or reload
|
|
||||||
mode = f.read()
|
|
||||||
else:
|
|
||||||
with open(SERVER_RESTART, 'w') as f:
|
|
||||||
f.write(str(mode))
|
|
||||||
return mode
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def shutdown(self, mode=None, _reactor_stopping=False):
|
def shutdown(self, mode='reload', _reactor_stopping=False):
|
||||||
"""
|
"""
|
||||||
Shuts down the server from inside it.
|
Shuts down the server from inside it.
|
||||||
|
|
||||||
|
|
@ -359,7 +338,6 @@ class Evennia(object):
|
||||||
at_shutdown hooks called but sessions will not
|
at_shutdown hooks called but sessions will not
|
||||||
be disconnected.
|
be disconnected.
|
||||||
'shutdown' - like reset, but server will not auto-restart.
|
'shutdown' - like reset, but server will not auto-restart.
|
||||||
None - keep currently set flag from flag file.
|
|
||||||
_reactor_stopping - this is set if server is stopped by a kill
|
_reactor_stopping - this is set if server is stopped by a kill
|
||||||
command OR this method was already called
|
command OR this method was already called
|
||||||
once - in both cases the reactor is
|
once - in both cases the reactor is
|
||||||
|
|
@ -370,10 +348,7 @@ class Evennia(object):
|
||||||
# once; we don't need to run the shutdown procedure again.
|
# once; we don't need to run the shutdown procedure again.
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
|
|
||||||
mode = self.getset_restart_mode(mode)
|
|
||||||
|
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
#from evennia.accounts.models import AccountDB
|
|
||||||
from evennia.server.models import ServerConfig
|
from evennia.server.models import ServerConfig
|
||||||
from evennia.utils import gametime as _GAMETIME_MODULE
|
from evennia.utils import gametime as _GAMETIME_MODULE
|
||||||
|
|
||||||
|
|
@ -382,7 +357,8 @@ class Evennia(object):
|
||||||
ServerConfig.objects.conf("server_restart_mode", "reload")
|
ServerConfig.objects.conf("server_restart_mode", "reload")
|
||||||
yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()]
|
yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()]
|
||||||
yield [p.at_server_reload() for p in AccountDB.get_all_cached_instances()]
|
yield [p.at_server_reload() for p in AccountDB.get_all_cached_instances()]
|
||||||
yield [(s.pause(manual_pause=False), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances() if s.is_active]
|
yield [(s.pause(manual_pause=False), s.at_server_reload())
|
||||||
|
for s in ScriptDB.get_all_cached_instances() if s.is_active]
|
||||||
yield self.sessions.all_sessions_portal_sync()
|
yield self.sessions.all_sessions_portal_sync()
|
||||||
self.at_server_reload_stop()
|
self.at_server_reload_stop()
|
||||||
# only save monitor state on reload, not on shutdown/reset
|
# only save monitor state on reload, not on shutdown/reset
|
||||||
|
|
@ -412,10 +388,6 @@ class Evennia(object):
|
||||||
# always called, also for a reload
|
# always called, also for a reload
|
||||||
self.at_server_stop()
|
self.at_server_stop()
|
||||||
|
|
||||||
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
|
|
||||||
# for Windows we need to remove pid files manually
|
|
||||||
os.remove(SERVER_PIDFILE)
|
|
||||||
|
|
||||||
if hasattr(self, "web_root"): # not set very first start
|
if hasattr(self, "web_root"): # not set very first start
|
||||||
yield self.web_root.empty_threadpool()
|
yield self.web_root.empty_threadpool()
|
||||||
|
|
||||||
|
|
@ -427,6 +399,10 @@ class Evennia(object):
|
||||||
# we make sure the proper gametime is saved as late as possible
|
# we make sure the proper gametime is saved as late as possible
|
||||||
ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.runtime())
|
ServerConfig.objects.conf("runtime", _GAMETIME_MODULE.runtime())
|
||||||
|
|
||||||
|
def get_info_dict(self):
|
||||||
|
"Return the server info, for display."
|
||||||
|
return INFO_DICT
|
||||||
|
|
||||||
# server start/stop hooks
|
# server start/stop hooks
|
||||||
|
|
||||||
def at_server_start(self):
|
def at_server_start(self):
|
||||||
|
|
@ -452,13 +428,15 @@ class Evennia(object):
|
||||||
if SERVER_STARTSTOP_MODULE:
|
if SERVER_STARTSTOP_MODULE:
|
||||||
SERVER_STARTSTOP_MODULE.at_server_reload_start()
|
SERVER_STARTSTOP_MODULE.at_server_reload_start()
|
||||||
|
|
||||||
def at_post_portal_sync(self):
|
def at_post_portal_sync(self, mode):
|
||||||
"""
|
"""
|
||||||
This is called just after the portal has finished syncing back data to the server
|
This is called just after the portal has finished syncing back data to the server
|
||||||
after reconnecting.
|
after reconnecting.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
mode (str): One of reload, reset or shutdown.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# one of reload, reset or shutdown
|
|
||||||
mode = self.getset_restart_mode()
|
|
||||||
|
|
||||||
from evennia.scripts.monitorhandler import MONITOR_HANDLER
|
from evennia.scripts.monitorhandler import MONITOR_HANDLER
|
||||||
MONITOR_HANDLER.restore(mode == 'reload')
|
MONITOR_HANDLER.restore(mode == 'reload')
|
||||||
|
|
@ -530,13 +508,15 @@ ServerConfig.objects.conf("server_starting_mode", True)
|
||||||
# what to execute from.
|
# what to execute from.
|
||||||
application = service.Application('Evennia')
|
application = service.Application('Evennia')
|
||||||
|
|
||||||
|
# custom logging
|
||||||
|
logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE),
|
||||||
|
os.path.dirname(settings.SERVER_LOG_FILE))
|
||||||
|
application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit)
|
||||||
|
|
||||||
# The main evennia server program. This sets up the database
|
# The main evennia server program. This sets up the database
|
||||||
# and is where we store all the other services.
|
# and is where we store all the other services.
|
||||||
EVENNIA = Evennia(application)
|
EVENNIA = Evennia(application)
|
||||||
|
|
||||||
print('-' * 50)
|
|
||||||
print(' %(servername)s Server (%(version)s) started.' % {'servername': SERVERNAME, 'version': VERSION})
|
|
||||||
|
|
||||||
if AMP_ENABLED:
|
if AMP_ENABLED:
|
||||||
|
|
||||||
# The AMP protocol handles the communication between
|
# The AMP protocol handles the communication between
|
||||||
|
|
@ -546,20 +526,20 @@ if AMP_ENABLED:
|
||||||
ifacestr = ""
|
ifacestr = ""
|
||||||
if AMP_INTERFACE != '127.0.0.1':
|
if AMP_INTERFACE != '127.0.0.1':
|
||||||
ifacestr = "-%s" % AMP_INTERFACE
|
ifacestr = "-%s" % AMP_INTERFACE
|
||||||
print(' amp (to Portal)%s: %s (internal)' % (ifacestr, AMP_PORT))
|
|
||||||
|
|
||||||
from evennia.server import amp
|
INFO_DICT["amp"] = 'amp %s: %s' % (ifacestr, AMP_PORT)
|
||||||
|
|
||||||
factory = amp.AmpServerFactory(EVENNIA)
|
from evennia.server import amp_client
|
||||||
amp_service = internet.TCPServer(AMP_PORT, factory, interface=AMP_INTERFACE)
|
|
||||||
amp_service.setName("EvenniaPortal")
|
factory = amp_client.AMPClientFactory(EVENNIA)
|
||||||
|
amp_service = internet.TCPClient(AMP_HOST, AMP_PORT, factory)
|
||||||
|
amp_service.setName('ServerAMPClient')
|
||||||
EVENNIA.services.addService(amp_service)
|
EVENNIA.services.addService(amp_service)
|
||||||
|
|
||||||
if WEBSERVER_ENABLED:
|
if WEBSERVER_ENABLED:
|
||||||
|
|
||||||
# Start a django-compatible webserver.
|
# Start a django-compatible webserver.
|
||||||
|
|
||||||
#from twisted.python import threadpool
|
|
||||||
from evennia.server.webserver import DjangoWebRoot, WSGIWebServer, Website, LockableThreadPool
|
from evennia.server.webserver import DjangoWebRoot, WSGIWebServer, Website, LockableThreadPool
|
||||||
|
|
||||||
# start a thread pool and define the root url (/) as a wsgi resource
|
# start a thread pool and define the root url (/) as a wsgi resource
|
||||||
|
|
@ -579,14 +559,16 @@ if WEBSERVER_ENABLED:
|
||||||
web_root = WEB_PLUGINS_MODULE.at_webserver_root_creation(web_root)
|
web_root = WEB_PLUGINS_MODULE.at_webserver_root_creation(web_root)
|
||||||
|
|
||||||
web_site = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
web_site = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||||
|
web_site.is_portal = False
|
||||||
|
|
||||||
|
INFO_DICT["webserver"] = ""
|
||||||
for proxyport, serverport in WEBSERVER_PORTS:
|
for proxyport, serverport in WEBSERVER_PORTS:
|
||||||
# create the webserver (we only need the port for this)
|
# create the webserver (we only need the port for this)
|
||||||
webserver = WSGIWebServer(threads, serverport, web_site, interface='127.0.0.1')
|
webserver = WSGIWebServer(threads, serverport, web_site, interface='127.0.0.1')
|
||||||
webserver.setName('EvenniaWebServer%s' % serverport)
|
webserver.setName('EvenniaWebServer%s' % serverport)
|
||||||
EVENNIA.services.addService(webserver)
|
EVENNIA.services.addService(webserver)
|
||||||
|
|
||||||
print(" webserver: %s (internal)" % serverport)
|
INFO_DICT["webserver"] += "webserver: %s" % serverport
|
||||||
|
|
||||||
ENABLED = []
|
ENABLED = []
|
||||||
if IRC_ENABLED:
|
if IRC_ENABLED:
|
||||||
|
|
@ -598,18 +580,11 @@ if RSS_ENABLED:
|
||||||
ENABLED.append('rss')
|
ENABLED.append('rss')
|
||||||
|
|
||||||
if ENABLED:
|
if ENABLED:
|
||||||
print(" " + ", ".join(ENABLED) + " enabled.")
|
INFO_DICT["irc_rss"] = ", ".join(ENABLED) + " enabled."
|
||||||
|
|
||||||
for plugin_module in SERVER_SERVICES_PLUGIN_MODULES:
|
for plugin_module in SERVER_SERVICES_PLUGIN_MODULES:
|
||||||
# external plugin protocols
|
# external plugin protocols
|
||||||
plugin_module.start_plugin_services(EVENNIA)
|
plugin_module.start_plugin_services(EVENNIA)
|
||||||
|
|
||||||
print('-' * 50) # end of terminal output
|
|
||||||
|
|
||||||
# clear server startup mode
|
# clear server startup mode
|
||||||
ServerConfig.objects.conf("server_starting_mode", delete=True)
|
ServerConfig.objects.conf("server_starting_mode", delete=True)
|
||||||
|
|
||||||
if os.name == 'nt':
|
|
||||||
# Windows only: Set PID file manually
|
|
||||||
with open(SERVER_PIDFILE, 'w') as f:
|
|
||||||
f.write(str(os.getpid()))
|
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,7 @@ class ServerSession(Session):
|
||||||
if not _ObjectDB:
|
if not _ObjectDB:
|
||||||
from evennia.objects.models import ObjectDB as _ObjectDB
|
from evennia.objects.models import ObjectDB as _ObjectDB
|
||||||
|
|
||||||
|
super(ServerSession, self).at_sync()
|
||||||
if not self.logged_in:
|
if not self.logged_in:
|
||||||
# assign the unloggedin-command set.
|
# assign the unloggedin-command set.
|
||||||
self.cmdset_storage = settings.CMDSET_UNLOGGEDIN
|
self.cmdset_storage = settings.CMDSET_UNLOGGEDIN
|
||||||
|
|
@ -400,6 +401,7 @@ class ServerSession(Session):
|
||||||
# this can happen if this is triggered e.g. a command.msg
|
# this can happen if this is triggered e.g. a command.msg
|
||||||
# that auto-adds the session, we'd get a kwarg collision.
|
# that auto-adds the session, we'd get a kwarg collision.
|
||||||
kwargs.pop("session", None)
|
kwargs.pop("session", None)
|
||||||
|
kwargs.pop("from_obj", None)
|
||||||
if text is not None:
|
if text is not None:
|
||||||
self.data_out(text=text, **kwargs)
|
self.data_out(text=text, **kwargs)
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,8 @@ class Session(object):
|
||||||
a new session is established.
|
a new session is established.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
protocol_key (str): By default, one of 'telnet', 'ssh',
|
protocol_key (str): By default, one of 'telnet', 'telnet/ssl', 'ssh',
|
||||||
'ssl' or 'web'.
|
'webclient/websocket' or 'webclient/ajax'.
|
||||||
address (str): Client address.
|
address (str): Client address.
|
||||||
sessionhandler (SessionHandler): Reference to the
|
sessionhandler (SessionHandler): Reference to the
|
||||||
main sessionhandler instance.
|
main sessionhandler instance.
|
||||||
|
|
@ -117,6 +117,12 @@ class Session(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for propname, value in sessdata.items():
|
for propname, value in sessdata.items():
|
||||||
|
if (propname == "protocol_flags" and isinstance(value, dict) and
|
||||||
|
hasattr(self, "protocol_flags") and
|
||||||
|
isinstance(self.protocol_flags, dict)):
|
||||||
|
# special handling to allow partial update of protocol flags
|
||||||
|
self.protocol_flags.update(value)
|
||||||
|
else:
|
||||||
setattr(self, propname, value)
|
setattr(self, propname, value)
|
||||||
|
|
||||||
def at_sync(self):
|
def at_sync(self):
|
||||||
|
|
@ -126,7 +132,8 @@ class Session(object):
|
||||||
on uid etc).
|
on uid etc).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.protocol_flags.update(self.account.attributs.get("_saved_protocol_flags"), {})
|
if self.account:
|
||||||
|
self.protocol_flags.update(self.account.attributes.get("_saved_protocol_flags", {}))
|
||||||
|
|
||||||
# access hooks
|
# access hooks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ from evennia.commands.cmdhandler import CMD_LOGINSTART
|
||||||
from evennia.utils.logger import log_trace
|
from evennia.utils.logger import log_trace
|
||||||
from evennia.utils.utils import (variable_from_module, is_iter,
|
from evennia.utils.utils import (variable_from_module, is_iter,
|
||||||
to_str, to_unicode,
|
to_str, to_unicode,
|
||||||
make_iter,
|
make_iter, delay,
|
||||||
callables_from_module)
|
callables_from_module)
|
||||||
from evennia.utils.inlinefuncs import parse_inlinefunc
|
from evennia.utils.inlinefuncs import parse_inlinefunc
|
||||||
|
|
||||||
|
|
@ -58,6 +58,12 @@ SSYNC = chr(8) # server session sync
|
||||||
SCONN = chr(11) # server portal connection (for bots)
|
SCONN = chr(11) # server portal connection (for bots)
|
||||||
PCONNSYNC = chr(12) # portal post-syncing session
|
PCONNSYNC = chr(12) # portal post-syncing session
|
||||||
PDISCONNALL = chr(13) # portal session discnnect all
|
PDISCONNALL = chr(13) # portal session discnnect all
|
||||||
|
SRELOAD = chr(14) # server reloading (have portal start a new server)
|
||||||
|
SSTART = chr(15) # server start (portal must already be running anyway)
|
||||||
|
PSHUTD = chr(16) # portal (+server) shutdown
|
||||||
|
SSHUTD = chr(17) # server shutdown
|
||||||
|
PSTATUS = chr(18) # ping server or portal status
|
||||||
|
SRESET = chr(19) # server shutdown in reset mode
|
||||||
|
|
||||||
# i18n
|
# i18n
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
|
|
@ -65,6 +71,7 @@ from django.utils.translation import ugettext as _
|
||||||
_SERVERNAME = settings.SERVERNAME
|
_SERVERNAME = settings.SERVERNAME
|
||||||
_MULTISESSION_MODE = settings.MULTISESSION_MODE
|
_MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||||
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||||
|
_DELAY_CMD_LOGINSTART = settings.DELAY_CMD_LOGINSTART
|
||||||
_MAX_SERVER_COMMANDS_PER_SECOND = 100.0
|
_MAX_SERVER_COMMANDS_PER_SECOND = 100.0
|
||||||
_MAX_SESSION_COMMANDS_PER_SECOND = 5.0
|
_MAX_SESSION_COMMANDS_PER_SECOND = 5.0
|
||||||
_MODEL_MAP = None
|
_MODEL_MAP = None
|
||||||
|
|
@ -274,6 +281,16 @@ class ServerSessionHandler(SessionHandler):
|
||||||
self.server = None
|
self.server = None
|
||||||
self.server_data = {"servername": _SERVERNAME}
|
self.server_data = {"servername": _SERVERNAME}
|
||||||
|
|
||||||
|
def _run_cmd_login(self, session):
|
||||||
|
"""
|
||||||
|
Launch the CMD_LOGINSTART command. This is wrapped
|
||||||
|
for delays.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not session.logged_in:
|
||||||
|
self.data_in(session, text=[[CMD_LOGINSTART], {}])
|
||||||
|
|
||||||
|
|
||||||
def portal_connect(self, portalsessiondata):
|
def portal_connect(self, portalsessiondata):
|
||||||
"""
|
"""
|
||||||
Called by Portal when a new session has connected.
|
Called by Portal when a new session has connected.
|
||||||
|
|
@ -309,8 +326,9 @@ class ServerSessionHandler(SessionHandler):
|
||||||
sess.logged_in = False
|
sess.logged_in = False
|
||||||
sess.uid = None
|
sess.uid = None
|
||||||
|
|
||||||
# show the first login command
|
# show the first login command, may delay slightly to allow
|
||||||
self.data_in(sess, text=[[CMD_LOGINSTART], {}])
|
# the handshakes to finish.
|
||||||
|
delay(_DELAY_CMD_LOGINSTART, self._run_cmd_login, sess)
|
||||||
|
|
||||||
def portal_session_sync(self, portalsessiondata):
|
def portal_session_sync(self, portalsessiondata):
|
||||||
"""
|
"""
|
||||||
|
|
@ -361,8 +379,10 @@ class ServerSessionHandler(SessionHandler):
|
||||||
self[sessid] = sess
|
self[sessid] = sess
|
||||||
sess.at_sync()
|
sess.at_sync()
|
||||||
|
|
||||||
|
mode = 'reload'
|
||||||
|
|
||||||
# tell the server hook we synced
|
# tell the server hook we synced
|
||||||
self.server.at_post_portal_sync()
|
self.server.at_post_portal_sync(mode)
|
||||||
# announce the reconnection
|
# announce the reconnection
|
||||||
self.announce_all(_(" ... Server restarted."))
|
self.announce_all(_(" ... Server restarted."))
|
||||||
|
|
||||||
|
|
@ -420,13 +440,28 @@ class ServerSessionHandler(SessionHandler):
|
||||||
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SCONN,
|
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SCONN,
|
||||||
protocol_path=protocol_path, config=configdict)
|
protocol_path=protocol_path, config=configdict)
|
||||||
|
|
||||||
|
def portal_restart_server(self):
|
||||||
|
"""
|
||||||
|
Called by server when reloading. We tell the portal to start a new server instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SRELOAD)
|
||||||
|
|
||||||
|
def portal_reset_server(self):
|
||||||
|
"""
|
||||||
|
Called by server when reloading. We tell the portal to start a new server instance.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION, operation=SRESET)
|
||||||
|
|
||||||
def portal_shutdown(self):
|
def portal_shutdown(self):
|
||||||
"""
|
"""
|
||||||
Called by server when shutting down the portal.
|
Called by server when it's time to shut down (the portal will shut us down and then shut
|
||||||
|
itself down)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
||||||
operation=SSHUTD)
|
operation=PSHUTD)
|
||||||
|
|
||||||
def login(self, session, account, force=False, testmode=False):
|
def login(self, session, account, force=False, testmode=False):
|
||||||
"""
|
"""
|
||||||
|
|
@ -538,6 +573,20 @@ class ServerSessionHandler(SessionHandler):
|
||||||
sessiondata=sessdata,
|
sessiondata=sessdata,
|
||||||
clean=False)
|
clean=False)
|
||||||
|
|
||||||
|
def session_portal_partial_sync(self, session_data):
|
||||||
|
"""
|
||||||
|
Call to make a partial update of the session, such as only a particular property.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_data (dict): Store `{sessid: {property:value}, ...}` defining one or
|
||||||
|
more sessions in detail.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.server.amp_protocol.send_AdminServer2Portal(DUMMYSESSION,
|
||||||
|
operation=SSYNC,
|
||||||
|
sessiondata=session_data,
|
||||||
|
clean=False)
|
||||||
|
|
||||||
def disconnect_all_sessions(self, reason="You have been disconnected."):
|
def disconnect_all_sessions(self, reason="You have been disconnected."):
|
||||||
"""
|
"""
|
||||||
Cleanly disconnect all of the connected sessions.
|
Cleanly disconnect all of the connected sessions.
|
||||||
|
|
@ -565,10 +614,14 @@ class ServerSessionHandler(SessionHandler):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
uid = curr_session.uid
|
uid = curr_session.uid
|
||||||
|
# we can't compare sessions directly since this will compare addresses and
|
||||||
|
# mean connecting from the same host would not catch duplicates
|
||||||
|
sid = id(curr_session)
|
||||||
doublet_sessions = [sess for sess in self.values()
|
doublet_sessions = [sess for sess in self.values()
|
||||||
if sess.logged_in and
|
if sess.logged_in and
|
||||||
sess.uid == uid and
|
sess.uid == uid and
|
||||||
sess != curr_session]
|
id(sess) != sid]
|
||||||
|
|
||||||
for session in doublet_sessions:
|
for session in doublet_sessions:
|
||||||
self.disconnect(session, reason)
|
self.disconnect(session, reason)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -212,6 +212,12 @@ class Website(server.Site):
|
||||||
"""
|
"""
|
||||||
noisy = False
|
noisy = False
|
||||||
|
|
||||||
|
def logPrefix(self):
|
||||||
|
"How to be named in logs"
|
||||||
|
if hasattr(self, "is_portal") and self.is_portal:
|
||||||
|
return "Webserver-proxy"
|
||||||
|
return "Webserver"
|
||||||
|
|
||||||
def log(self, request):
|
def log(self, request):
|
||||||
"""Conditional logging"""
|
"""Conditional logging"""
|
||||||
if _DEBUG:
|
if _DEBUG:
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ ALLOWED_HOSTS = ["*"]
|
||||||
# the Portal proxy presents to the world. The serverports are
|
# the Portal proxy presents to the world. The serverports are
|
||||||
# the internal ports the proxy uses to forward data to the Server-side
|
# the internal ports the proxy uses to forward data to the Server-side
|
||||||
# webserver (these should not be publicly open)
|
# webserver (these should not be publicly open)
|
||||||
WEBSERVER_PORTS = [(4001, 4002)]
|
WEBSERVER_PORTS = [(4001, 4005)]
|
||||||
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
|
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
|
||||||
WEBSERVER_INTERFACES = ['0.0.0.0']
|
WEBSERVER_INTERFACES = ['0.0.0.0']
|
||||||
# IP addresses that may talk to the server in a reverse proxy configuration,
|
# IP addresses that may talk to the server in a reverse proxy configuration,
|
||||||
|
|
@ -89,7 +89,7 @@ WEBSOCKET_CLIENT_ENABLED = True
|
||||||
# working through a proxy or docker port-remapping, the environment variable
|
# working through a proxy or docker port-remapping, the environment variable
|
||||||
# WEBCLIENT_CLIENT_PROXY_PORT can be used to override this port only for the
|
# WEBCLIENT_CLIENT_PROXY_PORT can be used to override this port only for the
|
||||||
# front-facing client's sake.
|
# front-facing client's sake.
|
||||||
WEBSOCKET_CLIENT_PORT = 4005
|
WEBSOCKET_CLIENT_PORT = 4002
|
||||||
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
|
# Interface addresses to listen to. If 0.0.0.0, listen to all. Use :: for IPv6.
|
||||||
WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0'
|
WEBSOCKET_CLIENT_INTERFACE = '0.0.0.0'
|
||||||
# Actual URL for webclient component to reach the websocket. You only need
|
# Actual URL for webclient component to reach the websocket. You only need
|
||||||
|
|
@ -313,6 +313,14 @@ CMD_IGNORE_PREFIXES = "@&/+"
|
||||||
# This module should contain one or more variables
|
# This module should contain one or more variables
|
||||||
# with strings defining the look of the screen.
|
# with strings defining the look of the screen.
|
||||||
CONNECTION_SCREEN_MODULE = "server.conf.connection_screens"
|
CONNECTION_SCREEN_MODULE = "server.conf.connection_screens"
|
||||||
|
# Delay to use before sending the evennia.syscmdkeys.CMD_LOGINSTART Command
|
||||||
|
# when a new session connects (this defaults the unloggedin-look for showing
|
||||||
|
# the connection screen). The delay is useful mainly for telnet, to allow
|
||||||
|
# client/server to establish client capabilities like color/mxp etc before
|
||||||
|
# sending any text. A value of 0.3 should be enough. While a good idea, it may
|
||||||
|
# cause issues with menu-logins and autoconnects since the menu will not have
|
||||||
|
# started when the autoconnects starts sending menu commands.
|
||||||
|
DELAY_CMD_LOGINSTART = 0.3
|
||||||
# An optional module that, if existing, must hold a function
|
# An optional module that, if existing, must hold a function
|
||||||
# named at_initial_setup(). This hook method can be used to customize
|
# named at_initial_setup(). This hook method can be used to customize
|
||||||
# the server's initial setup sequence (the very first startup of the system).
|
# the server's initial setup sequence (the very first startup of the system).
|
||||||
|
|
@ -462,7 +470,7 @@ BASE_SCRIPT_TYPECLASS = "typeclasses.scripts.Script"
|
||||||
DEFAULT_HOME = "#2"
|
DEFAULT_HOME = "#2"
|
||||||
# The start position for new characters. Default is Limbo (#2).
|
# The start position for new characters. Default is Limbo (#2).
|
||||||
# MULTISESSION_MODE = 0, 1 - used by default unloggedin create command
|
# MULTISESSION_MODE = 0, 1 - used by default unloggedin create command
|
||||||
# MULTISESSION_MODE = 2,3 - used by default character_create command
|
# MULTISESSION_MODE = 2, 3 - used by default character_create command
|
||||||
START_LOCATION = "#2"
|
START_LOCATION = "#2"
|
||||||
# Lookups of Attributes, Tags, Nicks, Aliases can be aggressively
|
# Lookups of Attributes, Tags, Nicks, Aliases can be aggressively
|
||||||
# cached to avoid repeated database hits. This often gives noticeable
|
# cached to avoid repeated database hits. This often gives noticeable
|
||||||
|
|
@ -496,8 +504,13 @@ TIME_FACTOR = 2.0
|
||||||
# The starting point of your game time (the epoch), in seconds.
|
# The starting point of your game time (the epoch), in seconds.
|
||||||
# In Python a value of 0 means Jan 1 1970 (use negatives for earlier
|
# In Python a value of 0 means Jan 1 1970 (use negatives for earlier
|
||||||
# start date). This will affect the returns from the utils.gametime
|
# start date). This will affect the returns from the utils.gametime
|
||||||
# module.
|
# module. If None, the server's first start-time is used as the epoch.
|
||||||
TIME_GAME_EPOCH = None
|
TIME_GAME_EPOCH = None
|
||||||
|
# Normally, game time will only increase when the server runs. If this is True,
|
||||||
|
# game time will not pause when the server reloads or goes offline. This setting
|
||||||
|
# together with a time factor of 1 should keep the game in sync with
|
||||||
|
# the real time (add a different epoch to shift time)
|
||||||
|
TIME_IGNORE_DOWNTIMES = False
|
||||||
|
|
||||||
######################################################################
|
######################################################################
|
||||||
# Inlinefunc
|
# Inlinefunc
|
||||||
|
|
@ -532,8 +545,8 @@ INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs",
|
||||||
# 3 - like mode 2, except multiple sessions can puppet one character, each
|
# 3 - like mode 2, except multiple sessions can puppet one character, each
|
||||||
# session getting the same data.
|
# session getting the same data.
|
||||||
MULTISESSION_MODE = 0
|
MULTISESSION_MODE = 0
|
||||||
# The maximum number of characters allowed for MULTISESSION_MODE 2,3. This is
|
# The maximum number of characters allowed for MULTISESSION_MODE 2, 3.
|
||||||
# checked by the default ooc char-creation command. Forced to 1 for
|
# This is checked by the default ooc char-creation command. Forced to 1 for
|
||||||
# MULTISESSION_MODE 0 and 1.
|
# MULTISESSION_MODE 0 and 1.
|
||||||
MAX_NR_CHARACTERS = 1
|
MAX_NR_CHARACTERS = 1
|
||||||
# The access hierarchy, in climbing order. A higher permission in the
|
# The access hierarchy, in climbing order. A higher permission in the
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from django.contrib import admin
|
||||||
from evennia.typeclasses.models import Tag
|
from evennia.typeclasses.models import Tag
|
||||||
from django import forms
|
from django import forms
|
||||||
from evennia.utils.picklefield import PickledFormField
|
from evennia.utils.picklefield import PickledFormField
|
||||||
from evennia.utils.dbserialize import from_pickle
|
from evennia.utils.dbserialize import from_pickle, _SaverSet
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -164,12 +164,12 @@ class AttributeForm(forms.ModelForm):
|
||||||
attr_category = forms.CharField(label="Category",
|
attr_category = forms.CharField(label="Category",
|
||||||
help_text="type of attribute, for sorting",
|
help_text="type of attribute, for sorting",
|
||||||
required=False,
|
required=False,
|
||||||
max_length=4)
|
max_length=128)
|
||||||
attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False)
|
attr_value = PickledFormField(label="Value", help_text="Value to pickle/save", required=False)
|
||||||
attr_type = forms.CharField(label="Type",
|
attr_type = forms.CharField(label="Type",
|
||||||
help_text="Internal use. Either unset (normal Attribute) or \"nick\"",
|
help_text="Internal use. Either unset (normal Attribute) or \"nick\"",
|
||||||
required=False,
|
required=False,
|
||||||
max_length=4)
|
max_length=16)
|
||||||
attr_strvalue = forms.CharField(label="String Value",
|
attr_strvalue = forms.CharField(label="String Value",
|
||||||
help_text="Only set when using the Attribute as a string-only store",
|
help_text="Only set when using the Attribute as a string-only store",
|
||||||
required=False,
|
required=False,
|
||||||
|
|
@ -213,6 +213,9 @@ class AttributeForm(forms.ModelForm):
|
||||||
self.instance.attr_key = attr_key
|
self.instance.attr_key = attr_key
|
||||||
self.instance.attr_category = attr_category
|
self.instance.attr_category = attr_category
|
||||||
self.instance.attr_value = attr_value
|
self.instance.attr_value = attr_value
|
||||||
|
# prevent set from being transformed to unicode
|
||||||
|
if isinstance(attr_value, set) or isinstance(attr_value, _SaverSet):
|
||||||
|
self.fields['attr_value'].disabled = True
|
||||||
self.instance.deserialized_value = from_pickle(attr_value)
|
self.instance.deserialized_value = from_pickle(attr_value)
|
||||||
self.instance.attr_strvalue = attr_strvalue
|
self.instance.attr_strvalue = attr_strvalue
|
||||||
self.instance.attr_type = attr_type
|
self.instance.attr_type = attr_type
|
||||||
|
|
@ -237,6 +240,17 @@ class AttributeForm(forms.ModelForm):
|
||||||
instance.attr_lockstring = self.cleaned_data['attr_lockstring']
|
instance.attr_lockstring = self.cleaned_data['attr_lockstring']
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
def clean_attr_value(self):
|
||||||
|
"""
|
||||||
|
Prevent Sets from being cleaned due to literal_eval failing on them. Otherwise they will be turned into
|
||||||
|
unicode.
|
||||||
|
"""
|
||||||
|
data = self.cleaned_data['attr_value']
|
||||||
|
initial = self.instance.attr_value
|
||||||
|
if isinstance(initial, set) or isinstance(initial, _SaverSet):
|
||||||
|
return initial
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class AttributeFormSet(forms.BaseInlineFormSet):
|
class AttributeFormSet(forms.BaseInlineFormSet):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -245,7 +245,7 @@ class AttributeHandler(object):
|
||||||
found from cache or database.
|
found from cache or database.
|
||||||
Notes:
|
Notes:
|
||||||
When given a category only, a search for all objects
|
When given a category only, a search for all objects
|
||||||
of that cateogory is done and a the category *name* is is
|
of that cateogory is done and the category *name* is
|
||||||
stored. This tells the system on subsequent calls that the
|
stored. This tells the system on subsequent calls that the
|
||||||
list of cached attributes of this category is up-to-date
|
list of cached attributes of this category is up-to-date
|
||||||
and that the cache can be queried for category matches
|
and that the cache can be queried for category matches
|
||||||
|
|
@ -282,6 +282,8 @@ class AttributeHandler(object):
|
||||||
"attribute__db_attrtype": self._attrtype,
|
"attribute__db_attrtype": self._attrtype,
|
||||||
"attribute__db_key__iexact": key.lower(),
|
"attribute__db_key__iexact": key.lower(),
|
||||||
"attribute__db_category__iexact": category.lower() if category else None}
|
"attribute__db_category__iexact": category.lower() if category else None}
|
||||||
|
if not self.obj.pk:
|
||||||
|
return []
|
||||||
conn = getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)
|
conn = getattr(self.obj, self._m2m_fieldname).through.objects.filter(**query)
|
||||||
if conn:
|
if conn:
|
||||||
attr = conn[0].attribute
|
attr = conn[0].attribute
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ import shlex
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from evennia.utils import idmapper
|
from evennia.utils import idmapper
|
||||||
from evennia.utils.utils import make_iter, variable_from_module, to_unicode
|
from evennia.utils.utils import make_iter, variable_from_module, to_unicode
|
||||||
|
from evennia.typeclasses.attributes import Attribute
|
||||||
|
from evennia.typeclasses.tags import Tag
|
||||||
|
|
||||||
__all__ = ("TypedObjectManager", )
|
__all__ = ("TypedObjectManager", )
|
||||||
_GA = object.__getattribute__
|
_GA = object.__getattribute__
|
||||||
|
|
@ -56,17 +58,19 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
||||||
dbmodel = self.model.__dbclass__.__name__.lower()
|
dbmodel = self.model.__dbclass__.__name__.lower()
|
||||||
query = [("attribute__db_attrtype", attrtype), ("attribute__db_model", dbmodel)]
|
query = [("attribute__db_attrtype", attrtype), ("attribute__db_model", dbmodel)]
|
||||||
if obj:
|
if obj:
|
||||||
query.append(("%s__id" % self.model.__name__.lower(), obj.id))
|
query.append(("%s__id" % self.model.__dbclass__.__name__.lower(), obj.id))
|
||||||
if key:
|
if key:
|
||||||
query.append(("attribute__db_key", key))
|
query.append(("attribute__db_key", key))
|
||||||
if category:
|
if category:
|
||||||
query.append(("attribute__db_category", category))
|
query.append(("attribute__db_category", category))
|
||||||
if strvalue:
|
if strvalue:
|
||||||
query.append(("attribute__db_strvalue", strvalue))
|
query.append(("attribute__db_strvalue", strvalue))
|
||||||
elif value:
|
if value:
|
||||||
# strvalue and value are mutually exclusive
|
# no reason to make strvalue/value mutually exclusive at this level
|
||||||
query.append(("attribute__db_value", value))
|
query.append(("attribute__db_value", value))
|
||||||
return [th.attribute for th in self.model.db_attributes.through.objects.filter(**dict(query))]
|
return Attribute.objects.filter(
|
||||||
|
pk__in=self.model.db_attributes.through.objects.filter(
|
||||||
|
**dict(query)).values_list("attribute_id", flat=True))
|
||||||
|
|
||||||
def get_nick(self, key=None, category=None, value=None, strvalue=None, obj=None):
|
def get_nick(self, key=None, category=None, value=None, strvalue=None, obj=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -145,6 +149,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
||||||
|
|
||||||
# Tag manager methods
|
# Tag manager methods
|
||||||
|
|
||||||
|
|
||||||
def get_tag(self, key=None, category=None, obj=None, tagtype=None, global_search=False):
|
def get_tag(self, key=None, category=None, obj=None, tagtype=None, global_search=False):
|
||||||
"""
|
"""
|
||||||
Return Tag objects by key, by category, by object (it is
|
Return Tag objects by key, by category, by object (it is
|
||||||
|
|
@ -188,7 +193,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
||||||
query.append(("tag__db_key", key))
|
query.append(("tag__db_key", key))
|
||||||
if category:
|
if category:
|
||||||
query.append(("tag__db_category", category))
|
query.append(("tag__db_category", category))
|
||||||
return [th.tag for th in self.model.db_tags.through.objects.filter(**dict(query))]
|
return Tag.objects.filter(
|
||||||
|
pk__in=self.model.db_tags.through.objects.filter(
|
||||||
|
**dict(query)).values_list("tag_id", flat=True))
|
||||||
|
|
||||||
def get_permission(self, key=None, category=None, obj=None):
|
def get_permission(self, key=None, category=None, obj=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -222,25 +229,58 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
||||||
|
|
||||||
def get_by_tag(self, key=None, category=None, tagtype=None):
|
def get_by_tag(self, key=None, category=None, tagtype=None):
|
||||||
"""
|
"""
|
||||||
Return objects having tags with a given key or category or
|
Return objects having tags with a given key or category or combination of the two.
|
||||||
combination of the two.
|
Also accepts multiple tags/category/tagtype
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str, optional): Tag key. Not case sensitive.
|
key (str or list, optional): Tag key or list of keys. Not case sensitive.
|
||||||
category (str, optional): Tag category. Not case sensitive.
|
category (str or list, optional): Tag category. Not case sensitive. If `key` is
|
||||||
tagtype (str or None, optional): 'type' of Tag, by default
|
a list, a single category can either apply to all keys in that list or this
|
||||||
|
must be a list matching the `key` list element by element. If no `key` is given,
|
||||||
|
all objects with tags of this category are returned.
|
||||||
|
tagtype (str, optional): 'type' of Tag, by default
|
||||||
this is either `None` (a normal Tag), `alias` or
|
this is either `None` (a normal Tag), `alias` or
|
||||||
`permission`.
|
`permission`. This always apply to all queried tags.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
objects (list): Objects with matching tag.
|
objects (list): Objects with matching tag.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
IndexError: If `key` and `category` are both lists and `category` is shorter
|
||||||
|
than `key`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if not (key or category):
|
||||||
|
return []
|
||||||
|
|
||||||
|
keys = make_iter(key) if key else []
|
||||||
|
categories = make_iter(category) if category else []
|
||||||
|
n_keys = len(keys)
|
||||||
|
n_categories = len(categories)
|
||||||
|
|
||||||
dbmodel = self.model.__dbclass__.__name__.lower()
|
dbmodel = self.model.__dbclass__.__name__.lower()
|
||||||
query = [("db_tags__db_tagtype", tagtype), ("db_tags__db_model", dbmodel)]
|
query = self.filter(db_tags__db_tagtype__iexact=tagtype,
|
||||||
if key:
|
db_tags__db_model__iexact=dbmodel).distinct()
|
||||||
query.append(("db_tags__db_key", key.lower()))
|
|
||||||
if category:
|
if n_keys > 0:
|
||||||
query.append(("db_tags__db_category", category.lower()))
|
# keys and/or categories given
|
||||||
return self.filter(**dict(query))
|
if n_categories == 0:
|
||||||
|
categories = [None for _ in range(n_keys)]
|
||||||
|
elif n_categories == 1 and n_keys > 1:
|
||||||
|
cat = categories[0]
|
||||||
|
categories = [cat for _ in range(n_keys)]
|
||||||
|
elif 1 < n_categories < n_keys:
|
||||||
|
raise IndexError("get_by_tag needs a single category or a list of categories "
|
||||||
|
"the same length as the list of tags.")
|
||||||
|
for ikey, key in enumerate(keys):
|
||||||
|
query = query.filter(db_tags__db_key__iexact=key,
|
||||||
|
db_tags__db_category__iexact=categories[ikey])
|
||||||
|
else:
|
||||||
|
# only one or more categories given
|
||||||
|
for category in categories:
|
||||||
|
query = query.filter(db_tags__db_category__iexact=category)
|
||||||
|
|
||||||
|
return query
|
||||||
|
|
||||||
def get_by_permission(self, key=None, category=None):
|
def get_by_permission(self, key=None, category=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -269,14 +269,15 @@ class TagHandler(object):
|
||||||
|
|
||||||
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False):
|
def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False):
|
||||||
"""
|
"""
|
||||||
Get the tag for the given key or list of tags.
|
Get the tag for the given key, category or combination of the two.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str or list): The tag or tags to retrieve.
|
key (str or list, optional): The tag or tags to retrieve.
|
||||||
default (any, optional): The value to return in case of no match.
|
default (any, optional): The value to return in case of no match.
|
||||||
category (str, optional): The Tag category to limit the
|
category (str, optional): The Tag category to limit the
|
||||||
request to. Note that `None` is the valid, default
|
request to. Note that `None` is the valid, default
|
||||||
category.
|
category. If no `key` is given, all tags of this category will be
|
||||||
|
returned.
|
||||||
return_tagobj (bool, optional): Return the Tag object itself
|
return_tagobj (bool, optional): Return the Tag object itself
|
||||||
instead of a string representation of the Tag.
|
instead of a string representation of the Tag.
|
||||||
return_list (bool, optional): Always return a list, regardless
|
return_list (bool, optional): Always return a list, regardless
|
||||||
|
|
|
||||||
59
evennia/typeclasses/tests.py
Normal file
59
evennia/typeclasses/tests.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
"""
|
||||||
|
Unit tests for typeclass base system
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Manager tests
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestTypedObjectManager(EvenniaTest):
|
||||||
|
def _manager(self, methodname, *args, **kwargs):
|
||||||
|
return list(getattr(self.obj1.__class__.objects, methodname)(*args, **kwargs))
|
||||||
|
|
||||||
|
def test_get_by_tag_no_category(self):
|
||||||
|
self.obj1.tags.add("tag1")
|
||||||
|
self.obj1.tags.add("tag2")
|
||||||
|
self.obj1.tags.add("tag2c")
|
||||||
|
self.obj2.tags.add("tag2")
|
||||||
|
self.obj2.tags.add("tag2a")
|
||||||
|
self.obj2.tags.add("tag2b")
|
||||||
|
self.obj2.tags.add("tag3 with spaces")
|
||||||
|
self.obj2.tags.add("tag4")
|
||||||
|
self.obj2.tags.add("tag2c")
|
||||||
|
self.assertEquals(self._manager("get_by_tag", "tag1"), [self.obj1])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", "tag2"), [self.obj1, self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", "tag2a"), [self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", "tag3 with spaces"), [self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", ["tag2a", "tag2b"]), [self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", ["tag2a", "tag1"]), [])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", ["tag2a", "tag4", "tag2c"]), [self.obj2])
|
||||||
|
|
||||||
|
def test_get_by_tag_and_category(self):
|
||||||
|
self.obj1.tags.add("tag5", "category1")
|
||||||
|
self.obj1.tags.add("tag6", )
|
||||||
|
self.obj1.tags.add("tag7", "category1")
|
||||||
|
self.obj1.tags.add("tag6", "category3")
|
||||||
|
self.obj1.tags.add("tag7", "category4")
|
||||||
|
self.obj2.tags.add("tag5", "category1")
|
||||||
|
self.obj2.tags.add("tag5", "category2")
|
||||||
|
self.obj2.tags.add("tag6", "category3")
|
||||||
|
self.obj2.tags.add("tag7", "category1")
|
||||||
|
self.obj2.tags.add("tag7", "category5")
|
||||||
|
self.assertEquals(self._manager("get_by_tag", "tag5", "category1"), [self.obj1, self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", "tag6", "category1"), [])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", "tag6", "category3"), [self.obj1, self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", ["tag5", "tag6"],
|
||||||
|
["category1", "category3"]), [self.obj1, self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", ["tag5", "tag7"],
|
||||||
|
"category1"), [self.obj1, self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", category="category1"), [self.obj1, self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", category="category2"), [self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", category=["category1", "category3"]),
|
||||||
|
[self.obj1, self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", category=["category1", "category2"]),
|
||||||
|
[self.obj2])
|
||||||
|
self.assertEquals(self._manager("get_by_tag", category=["category5", "category4"]), [])
|
||||||
|
|
@ -43,13 +43,18 @@ command definition too) with function definitions:
|
||||||
def node_with_other_name(caller, input_string):
|
def node_with_other_name(caller, input_string):
|
||||||
# code
|
# code
|
||||||
return text, options
|
return text, options
|
||||||
|
|
||||||
|
def another_node(caller, input_string, **kwargs):
|
||||||
|
# code
|
||||||
|
return text, options
|
||||||
```
|
```
|
||||||
|
|
||||||
Where caller is the object using the menu and input_string is the
|
Where caller is the object using the menu and input_string is the
|
||||||
command entered by the user on the *previous* node (the command
|
command entered by the user on the *previous* node (the command
|
||||||
entered to get to this node). The node function code will only be
|
entered to get to this node). The node function code will only be
|
||||||
executed once per node-visit and the system will accept nodes with
|
executed once per node-visit and the system will accept nodes with
|
||||||
both one or two arguments interchangeably.
|
both one or two arguments interchangeably. It also accepts nodes
|
||||||
|
that takes **kwargs.
|
||||||
|
|
||||||
The menu tree itself is available on the caller as
|
The menu tree itself is available on the caller as
|
||||||
`caller.ndb._menutree`. This makes it a convenient place to store
|
`caller.ndb._menutree`. This makes it a convenient place to store
|
||||||
|
|
@ -82,12 +87,14 @@ menu is immediately exited and the default "look" command is called.
|
||||||
the callable. Those kwargs will also be passed into the next node if possible.
|
the callable. Those kwargs will also be passed into the next node if possible.
|
||||||
Such a callable should return either a str or a (str, dict), where the
|
Such a callable should return either a str or a (str, dict), where the
|
||||||
string is the name of the next node to go to and the dict is the new,
|
string is the name of the next node to go to and the dict is the new,
|
||||||
(possibly modified) kwarg to pass into the next node.
|
(possibly modified) kwarg to pass into the next node. If the callable returns
|
||||||
|
None or the empty string, the current node will be revisited.
|
||||||
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
|
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
|
||||||
and runs before it. If given a node name, the node will be executed but will not
|
and runs before it. If given a node name, the node will be executed but will not
|
||||||
be considered the next node. If node/callback returns str or (str, dict), these will
|
be considered the next node. If node/callback returns str or (str, dict), these will
|
||||||
replace the `goto` step (`goto` callbacks will not fire), with the string being the
|
replace the `goto` step (`goto` callbacks will not fire), with the string being the
|
||||||
next node name and the optional dict acting as the kwargs-input for the next node.
|
next node name and the optional dict acting as the kwargs-input for the next node.
|
||||||
|
If an exec callable returns the empty string (only), the current node is re-run.
|
||||||
|
|
||||||
If key is not given, the option will automatically be identified by
|
If key is not given, the option will automatically be identified by
|
||||||
its number 1..N.
|
its number 1..N.
|
||||||
|
|
@ -167,7 +174,7 @@ from evennia import Command, CmdSet
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils.evtable import EvTable
|
from evennia.utils.evtable import EvTable
|
||||||
from evennia.utils.ansi import strip_ansi
|
from evennia.utils.ansi import strip_ansi
|
||||||
from evennia.utils.utils import mod_import, make_iter, pad, m_len
|
from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
|
|
||||||
# read from protocol NAWS later?
|
# read from protocol NAWS later?
|
||||||
|
|
@ -182,7 +189,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
||||||
|
|
||||||
# i18n
|
# i18n
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.")
|
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is either not implemented or "
|
||||||
|
"caused an error. Make another choice.")
|
||||||
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
|
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
|
||||||
_ERR_NO_OPTION_DESC = _("No description.")
|
_ERR_NO_OPTION_DESC = _("No description.")
|
||||||
_HELP_FULL = _("Commands: <menu option>, help, quit")
|
_HELP_FULL = _("Commands: <menu option>, help, quit")
|
||||||
|
|
@ -573,6 +581,7 @@ class EvMenu(object):
|
||||||
except EvMenuError:
|
except EvMenuError:
|
||||||
errmsg = _ERR_GENERAL.format(nodename=callback)
|
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||||
self.caller.msg(errmsg, self._session)
|
self.caller.msg(errmsg, self._session)
|
||||||
|
logger.log_trace()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
@ -606,9 +615,11 @@ class EvMenu(object):
|
||||||
nodetext, options = ret, None
|
nodetext, options = ret, None
|
||||||
except KeyError:
|
except KeyError:
|
||||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||||
|
logger.log_trace()
|
||||||
raise EvMenuError
|
raise EvMenuError
|
||||||
except Exception:
|
except Exception:
|
||||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
||||||
|
logger.log_trace()
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# store options to make them easier to test
|
# store options to make them easier to test
|
||||||
|
|
@ -665,24 +676,27 @@ class EvMenu(object):
|
||||||
|
|
||||||
if isinstance(ret, basestring):
|
if isinstance(ret, basestring):
|
||||||
# only return a value if a string (a goto target), ignore all other returns
|
# only return a value if a string (a goto target), ignore all other returns
|
||||||
|
if not ret:
|
||||||
|
# an empty string - rerun the same node
|
||||||
|
return self.nodename
|
||||||
return ret, kwargs
|
return ret, kwargs
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def goto(self, nodename, raw_string, **kwargs):
|
def extract_goto_exec(self, nodename, option_dict):
|
||||||
"""
|
"""
|
||||||
Run a node by name, optionally dynamically generating that name first.
|
Helper: Get callables and their eventual kwargs.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
nodename (str or callable): Name of node or a callable
|
nodename (str): The current node name (used for error reporting).
|
||||||
to be called as `function(caller, raw_string)` or `function(caller)`
|
option_dict (dict): The seleted option's dict.
|
||||||
to return the actual goto string.
|
|
||||||
raw_string (str): The raw default string entered on the
|
Returns:
|
||||||
previous node (only used if the node accepts it as an
|
goto (str, callable or None): The goto directive in the option.
|
||||||
argument)
|
goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.
|
||||||
|
execute (callable or None): Executable given by the `exec` directive.
|
||||||
|
exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def _extract_goto_exec(option_dict):
|
|
||||||
"Helper: Get callables and their eventual kwargs"
|
|
||||||
goto_kwargs, exec_kwargs = {}, {}
|
goto_kwargs, exec_kwargs = {}, {}
|
||||||
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||||
if goto and isinstance(goto, (tuple, list)):
|
if goto and isinstance(goto, (tuple, list)):
|
||||||
|
|
@ -705,6 +719,20 @@ class EvMenu(object):
|
||||||
execute = execute[0]
|
execute = execute[0]
|
||||||
return goto, goto_kwargs, execute, exec_kwargs
|
return goto, goto_kwargs, execute, exec_kwargs
|
||||||
|
|
||||||
|
def goto(self, nodename, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Run a node by name, optionally dynamically generating that name first.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodename (str or callable): Name of node or a callable
|
||||||
|
to be called as `function(caller, raw_string)` or `function(caller)`
|
||||||
|
to return the actual goto string.
|
||||||
|
raw_string (str): The raw default string entered on the
|
||||||
|
previous node (only used if the node accepts it as an
|
||||||
|
argument)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
if callable(nodename):
|
if callable(nodename):
|
||||||
# run the "goto" callable, if possible
|
# run the "goto" callable, if possible
|
||||||
inp_nodename = nodename
|
inp_nodename = nodename
|
||||||
|
|
@ -714,6 +742,9 @@ class EvMenu(object):
|
||||||
raise EvMenuError(
|
raise EvMenuError(
|
||||||
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
||||||
nodename, kwargs = nodename[:2]
|
nodename, kwargs = nodename[:2]
|
||||||
|
if not nodename:
|
||||||
|
# no nodename return. Re-run current node
|
||||||
|
nodename = self.nodename
|
||||||
try:
|
try:
|
||||||
# execute the found node, make use of the returns.
|
# execute the found node, make use of the returns.
|
||||||
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
||||||
|
|
@ -746,12 +777,12 @@ class EvMenu(object):
|
||||||
desc = dic.get("desc", dic.get("text", None))
|
desc = dic.get("desc", dic.get("text", None))
|
||||||
if "_default" in keys:
|
if "_default" in keys:
|
||||||
keys = [key for key in keys if key != "_default"]
|
keys = [key for key in keys if key != "_default"]
|
||||||
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
|
||||||
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
self.default = (goto, goto_kwargs, execute, exec_kwargs)
|
||||||
else:
|
else:
|
||||||
# use the key (only) if set, otherwise use the running number
|
# use the key (only) if set, otherwise use the running number
|
||||||
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
|
||||||
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic)
|
goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
|
||||||
if keys:
|
if keys:
|
||||||
display_options.append((keys[0], desc))
|
display_options.append((keys[0], desc))
|
||||||
for key in keys:
|
for key in keys:
|
||||||
|
|
@ -846,10 +877,6 @@ class EvMenu(object):
|
||||||
else:
|
else:
|
||||||
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
self.caller.msg(_HELP_NO_OPTION_MATCH, session=self._session)
|
||||||
|
|
||||||
if not (self.options or self.default):
|
|
||||||
# no options - we are at the end of the menu.
|
|
||||||
self.close_menu()
|
|
||||||
|
|
||||||
def display_nodetext(self):
|
def display_nodetext(self):
|
||||||
self.caller.msg(self.nodetext, session=self._session)
|
self.caller.msg(self.nodetext, session=self._session)
|
||||||
|
|
||||||
|
|
@ -949,14 +976,176 @@ class EvMenu(object):
|
||||||
node (str): The formatted node to display.
|
node (str): The formatted node to display.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if self._session:
|
||||||
|
screen_width = self._session.protocol_flags.get(
|
||||||
|
"SCREENWIDTH", {0: _MAX_TEXT_WIDTH})[0]
|
||||||
|
else:
|
||||||
|
screen_width = _MAX_TEXT_WIDTH
|
||||||
|
|
||||||
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
|
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
|
||||||
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
|
options_width_max = max(m_len(line) for line in optionstext.split("\n"))
|
||||||
total_width = max(options_width_max, nodetext_width_max)
|
total_width = min(screen_width, max(options_width_max, nodetext_width_max))
|
||||||
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
|
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
|
||||||
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
|
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
|
||||||
return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext
|
return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext
|
||||||
|
|
||||||
|
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
#
|
||||||
|
# List node (decorator turning a node into a list with
|
||||||
|
# look/edit/add functionality for the elements)
|
||||||
|
#
|
||||||
|
# -----------------------------------------------------------
|
||||||
|
|
||||||
|
def list_node(option_generator, select=None, pagesize=10):
|
||||||
|
"""
|
||||||
|
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
|
||||||
|
prepending those options added in the node.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
option_generator (callable or list): A list of strings indicating the options, or a callable
|
||||||
|
that is called as option_generator(caller) to produce such a list.
|
||||||
|
select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will
|
||||||
|
contain the `available_choices` list and `selection` will hold one of the elements in
|
||||||
|
that list. If a callable, it will be called as select(caller, menuchoice) where
|
||||||
|
menuchoice is the chosen option as a string. Should return the target node to goto after
|
||||||
|
this selection (or None to repeat the list-node). Note that if this is not given, the
|
||||||
|
decorated node must itself provide a way to continue from the node!
|
||||||
|
pagesize (int): How many options to show per page.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@list_node(['foo', 'bar'], select)
|
||||||
|
def node_index(caller):
|
||||||
|
text = "describing the list"
|
||||||
|
return text, []
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept
|
||||||
|
**kwargs, get a new kwarg 'available_choices' injected. These are the ordered list of named
|
||||||
|
options (descs) visible on the current node page.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
|
||||||
|
def _select_parser(caller, raw_string, **kwargs):
|
||||||
|
"""
|
||||||
|
Parse the select action
|
||||||
|
"""
|
||||||
|
available_choices = kwargs.get("available_choices", [])
|
||||||
|
|
||||||
|
try:
|
||||||
|
index = int(raw_string.strip()) - 1
|
||||||
|
selection = available_choices[index]
|
||||||
|
except Exception:
|
||||||
|
caller.msg("|rInvalid choice.|n")
|
||||||
|
else:
|
||||||
|
if callable(select):
|
||||||
|
try:
|
||||||
|
return select(caller, selection)
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
elif select:
|
||||||
|
# we assume a string was given, we inject the result into the kwargs
|
||||||
|
# to pass on to the next node
|
||||||
|
kwargs['selection'] = selection
|
||||||
|
return str(select)
|
||||||
|
# this means the previous node will be re-run with these same kwargs
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _list_node(caller, raw_string, **kwargs):
|
||||||
|
|
||||||
|
option_list = option_generator(caller) \
|
||||||
|
if callable(option_generator) else option_generator
|
||||||
|
|
||||||
|
npages = 0
|
||||||
|
page_index = 0
|
||||||
|
page = []
|
||||||
|
options = []
|
||||||
|
|
||||||
|
if option_list:
|
||||||
|
nall_options = len(option_list)
|
||||||
|
pages = [option_list[ind:ind + pagesize]
|
||||||
|
for ind in range(0, nall_options, pagesize)]
|
||||||
|
npages = len(pages)
|
||||||
|
|
||||||
|
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
|
||||||
|
page = pages[page_index]
|
||||||
|
|
||||||
|
text = ""
|
||||||
|
extra_text = None
|
||||||
|
|
||||||
|
# dynamic, multi-page option list. Each selection leads to the `select`
|
||||||
|
# callback being called with a result from the available choices
|
||||||
|
options.extend([{"desc": opt,
|
||||||
|
"goto": (_select_parser,
|
||||||
|
{"available_choices": page})} for opt in page])
|
||||||
|
|
||||||
|
if npages > 1:
|
||||||
|
# if the goto callable returns None, the same node is rerun, and
|
||||||
|
# kwargs not used by the callable are passed on to the node. This
|
||||||
|
# allows us to call ourselves over and over, using different kwargs.
|
||||||
|
options.append({"key": ("|Wcurrent|n", "c"),
|
||||||
|
"desc": "|W({}/{})|n".format(page_index + 1, npages),
|
||||||
|
"goto": (lambda caller: None,
|
||||||
|
{"optionpage_index": page_index})})
|
||||||
|
if page_index > 0:
|
||||||
|
options.append({"key": ("|wp|Wrevious page|n", "p"),
|
||||||
|
"goto": (lambda caller: None,
|
||||||
|
{"optionpage_index": page_index - 1})})
|
||||||
|
if page_index < npages - 1:
|
||||||
|
options.append({"key": ("|wn|Wext page|n", "n"),
|
||||||
|
"goto": (lambda caller: None,
|
||||||
|
{"optionpage_index": page_index + 1})})
|
||||||
|
|
||||||
|
# add data from the decorated node
|
||||||
|
|
||||||
|
decorated_options = []
|
||||||
|
try:
|
||||||
|
text, decorated_options = func(caller, raw_string)
|
||||||
|
except TypeError:
|
||||||
|
try:
|
||||||
|
text, decorated_options = func(caller)
|
||||||
|
except Exception:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
else:
|
||||||
|
if isinstance(decorated_options, {}):
|
||||||
|
decorated_options = [decorated_options]
|
||||||
|
else:
|
||||||
|
decorated_options = make_iter(decorated_options)
|
||||||
|
|
||||||
|
extra_options = []
|
||||||
|
for eopt in decorated_options:
|
||||||
|
cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None
|
||||||
|
if cback:
|
||||||
|
signature = eopt[cback]
|
||||||
|
if callable(signature):
|
||||||
|
# callable with no kwargs defined
|
||||||
|
eopt[cback] = (signature, {"available_choices": page})
|
||||||
|
elif is_iter(signature):
|
||||||
|
if len(signature) > 1 and isinstance(signature[1], dict):
|
||||||
|
signature[1]["available_choices"] = page
|
||||||
|
eopt[cback] = signature
|
||||||
|
elif signature:
|
||||||
|
# a callable alone in a tuple (i.e. no previous kwargs)
|
||||||
|
eopt[cback] = (signature[0], {"available_choices": page})
|
||||||
|
else:
|
||||||
|
# malformed input.
|
||||||
|
logger.log_err("EvMenu @list_node decorator found "
|
||||||
|
"malformed option to decorate: {}".format(eopt))
|
||||||
|
extra_options.append(eopt)
|
||||||
|
|
||||||
|
options.extend(extra_options)
|
||||||
|
text = text + "\n\n" + extra_text if extra_text else text
|
||||||
|
|
||||||
|
return text, options
|
||||||
|
|
||||||
|
return _list_node
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------------------------------------
|
# -------------------------------------------------------------------------------------------------
|
||||||
#
|
#
|
||||||
# Simple input shortcuts
|
# Simple input shortcuts
|
||||||
|
|
|
||||||
|
|
@ -893,6 +893,9 @@ class EvColumn(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
col = self.column
|
col = self.column
|
||||||
|
# fixed options for the column will override those requested in the call!
|
||||||
|
# this is particularly relevant to things like width/height, to avoid
|
||||||
|
# fixed-widths columns from being auto-balanced
|
||||||
kwargs.update(self.options)
|
kwargs.update(self.options)
|
||||||
# use fixed width or adjust to the largest cell
|
# use fixed width or adjust to the largest cell
|
||||||
if "width" not in kwargs:
|
if "width" not in kwargs:
|
||||||
|
|
@ -1283,24 +1286,58 @@ class EvTable(object):
|
||||||
cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable]
|
cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable]
|
||||||
cwmin = sum(cwidths_min)
|
cwmin = sum(cwidths_min)
|
||||||
|
|
||||||
if cwmin > width:
|
# get which cols have separately set widths - these should be locked
|
||||||
# we cannot shrink any more
|
# note that we need to remove cwidths_min for each lock to avoid counting
|
||||||
raise Exception("Cannot shrink table width to %s. Minimum size is %s." % (self.width, cwmin))
|
# it twice (in cwmin and in locked_cols)
|
||||||
|
locked_cols = {icol: col.options['width'] - cwidths_min[icol]
|
||||||
|
for icol, col in enumerate(self.worktable) if 'width' in col.options}
|
||||||
|
locked_width = sum(locked_cols.values())
|
||||||
|
|
||||||
|
excess = width - cwmin - locked_width
|
||||||
|
|
||||||
|
if len(locked_cols) >= ncols and excess:
|
||||||
|
# we can't adjust the width at all - all columns are locked
|
||||||
|
raise Exception("Cannot balance table to width %s - "
|
||||||
|
"all columns have a set, fixed width summing to %s!" % (
|
||||||
|
self.width, sum(cwidths)))
|
||||||
|
|
||||||
|
if excess < 0:
|
||||||
|
# the locked cols makes it impossible
|
||||||
|
raise Exception("Cannot shrink table width to %s. "
|
||||||
|
"Minimum size (and/or fixed-width columns) "
|
||||||
|
"sets minimum at %s." % (self.width, cwmin + locked_width))
|
||||||
|
|
||||||
excess = width - cwmin
|
|
||||||
if self.evenwidth:
|
if self.evenwidth:
|
||||||
# make each column of equal width
|
# make each column of equal width
|
||||||
for _ in range(excess):
|
# use cwidths as a work-array to track weights
|
||||||
|
cwidths = copy(cwidths_min)
|
||||||
|
correction = 0
|
||||||
|
while correction < excess:
|
||||||
# flood-fill the minimum table starting with the smallest columns
|
# flood-fill the minimum table starting with the smallest columns
|
||||||
ci = cwidths_min.index(min(cwidths_min))
|
ci = cwidths.index(min(cwidths))
|
||||||
|
if ci in locked_cols:
|
||||||
|
# locked column, make sure it's not picked again
|
||||||
|
cwidths[ci] += 9999
|
||||||
|
cwidths_min[ci] = locked_cols[ci]
|
||||||
|
else:
|
||||||
cwidths_min[ci] += 1
|
cwidths_min[ci] += 1
|
||||||
|
correction += 1
|
||||||
cwidths = cwidths_min
|
cwidths = cwidths_min
|
||||||
else:
|
else:
|
||||||
# make each column expand more proportional to their data size
|
# make each column expand more proportional to their data size
|
||||||
for _ in range(excess):
|
# we use cwidth as a work-array to track weights
|
||||||
|
correction = 0
|
||||||
|
while correction < excess:
|
||||||
# fill wider columns first
|
# fill wider columns first
|
||||||
ci = cwidths.index(max(cwidths))
|
ci = cwidths.index(max(cwidths))
|
||||||
|
if ci in locked_cols:
|
||||||
|
# locked column, make sure it's not picked again
|
||||||
|
cwidths[ci] -= 9999
|
||||||
|
cwidths_min[ci] = locked_cols[ci]
|
||||||
|
else:
|
||||||
cwidths_min[ci] += 1
|
cwidths_min[ci] += 1
|
||||||
|
correction += 1
|
||||||
|
# give a just changed col less prio next run
|
||||||
cwidths[ci] -= 3
|
cwidths[ci] -= 3
|
||||||
cwidths = cwidths_min
|
cwidths = cwidths_min
|
||||||
|
|
||||||
|
|
@ -1323,28 +1360,46 @@ class EvTable(object):
|
||||||
for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
|
for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
|
||||||
chmin = sum(cheights_min)
|
chmin = sum(cheights_min)
|
||||||
|
|
||||||
|
# get which cols have separately set heights - these should be locked
|
||||||
|
# note that we need to remove cheights_min for each lock to avoid counting
|
||||||
|
# it twice (in chmin and in locked_cols)
|
||||||
|
locked_cols = {icol: col.options['height'] - cheights_min[icol]
|
||||||
|
for icol, col in enumerate(self.worktable) if 'height' in col.options}
|
||||||
|
locked_height = sum(locked_cols.values())
|
||||||
|
|
||||||
|
excess = self.height - chmin - locked_height
|
||||||
|
|
||||||
if chmin > self.height:
|
if chmin > self.height:
|
||||||
# we cannot shrink any more
|
# we cannot shrink any more
|
||||||
raise Exception("Cannot shrink table height to %s. Minimum size is %s." % (self.height, chmin))
|
raise Exception("Cannot shrink table height to %s. Minimum "
|
||||||
|
"size (and/or fixed-height rows) sets minimum at %s." % (
|
||||||
|
self.height, chmin + locked_height))
|
||||||
|
|
||||||
# now we add all the extra height up to the desired table-height.
|
# now we add all the extra height up to the desired table-height.
|
||||||
# We do this so that the tallest cells gets expanded first (and
|
# We do this so that the tallest cells gets expanded first (and
|
||||||
# thus avoid getting cropped)
|
# thus avoid getting cropped)
|
||||||
|
|
||||||
excess = self.height - chmin
|
|
||||||
even = self.height % 2 == 0
|
even = self.height % 2 == 0
|
||||||
for position in range(excess):
|
correction = 0
|
||||||
|
while correction < excess:
|
||||||
# expand the cells with the most rows first
|
# expand the cells with the most rows first
|
||||||
if 0 <= position < nrowmax and nrowmax > 1:
|
if 0 <= correction < nrowmax and nrowmax > 1:
|
||||||
# avoid adding to header first round (looks bad on very small tables)
|
# avoid adding to header first round (looks bad on very small tables)
|
||||||
ci = cheights[1:].index(max(cheights[1:])) + 1
|
ci = cheights[1:].index(max(cheights[1:])) + 1
|
||||||
else:
|
else:
|
||||||
ci = cheights.index(max(cheights))
|
ci = cheights.index(max(cheights))
|
||||||
|
if ci in locked_cols:
|
||||||
|
# locked row, make sure it's not picked again
|
||||||
|
cheights[ci] -= 9999
|
||||||
|
cheights_min[ci] = locked_cols[ci]
|
||||||
|
else:
|
||||||
cheights_min[ci] += 1
|
cheights_min[ci] += 1
|
||||||
|
# change balance
|
||||||
if ci == 0 and self.header:
|
if ci == 0 and self.header:
|
||||||
# it doesn't look very good if header expands too fast
|
# it doesn't look very good if header expands too fast
|
||||||
cheights[ci] -= 2 if even else 3
|
cheights[ci] -= 2 if even else 3
|
||||||
cheights[ci] -= 2 if even else 1
|
cheights[ci] -= 2 if even else 1
|
||||||
|
correction += 1
|
||||||
cheights = cheights_min
|
cheights = cheights_min
|
||||||
|
|
||||||
# we must tell cells to crop instead of expanding
|
# we must tell cells to crop instead of expanding
|
||||||
|
|
@ -1554,6 +1609,8 @@ class EvTable(object):
|
||||||
"""
|
"""
|
||||||
if index > len(self.table):
|
if index > len(self.table):
|
||||||
raise Exception("Not a valid column index")
|
raise Exception("Not a valid column index")
|
||||||
|
# we update the columns' options which means eventual width/height
|
||||||
|
# will be 'locked in' and withstand auto-balancing width/height from the table later
|
||||||
self.table[index].options.update(kwargs)
|
self.table[index].options.update(kwargs)
|
||||||
self.table[index].reformat(**kwargs)
|
self.table[index].reformat(**kwargs)
|
||||||
|
|
||||||
|
|
@ -1569,6 +1626,7 @@ class EvTable(object):
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""print table (this also balances it)"""
|
"""print table (this also balances it)"""
|
||||||
|
# h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||||
return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()])))
|
return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()])))
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ from evennia.utils.create import create_script
|
||||||
# to real time.
|
# to real time.
|
||||||
|
|
||||||
TIMEFACTOR = settings.TIME_FACTOR
|
TIMEFACTOR = settings.TIME_FACTOR
|
||||||
|
IGNORE_DOWNTIMES = settings.TIME_IGNORE_DOWNTIMES
|
||||||
|
|
||||||
|
|
||||||
# Only set if gametime_reset was called at some point.
|
# Only set if gametime_reset was called at some point.
|
||||||
GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=0)
|
GAME_TIME_OFFSET = ServerConfig.objects.conf("gametime_offset", default=0)
|
||||||
|
|
@ -133,6 +135,9 @@ def gametime(absolute=False):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
epoch = game_epoch() if absolute else 0
|
epoch = game_epoch() if absolute else 0
|
||||||
|
if IGNORE_DOWNTIMES:
|
||||||
|
gtime = epoch + (time.time() - server_epoch()) * TIMEFACTOR
|
||||||
|
else:
|
||||||
gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR
|
gtime = epoch + (runtime() - GAME_TIME_OFFSET) * TIMEFACTOR
|
||||||
return gtime
|
return gtime
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from traceback import format_exc
|
from traceback import format_exc
|
||||||
from twisted.python import log, logfile
|
from twisted.python import log, logfile
|
||||||
|
from twisted.python import util as twisted_util
|
||||||
from twisted.internet.threads import deferToThread
|
from twisted.internet.threads import deferToThread
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -29,10 +30,15 @@ _TIMEZONE = None
|
||||||
_CHANNEL_LOG_NUM_TAIL_LINES = None
|
_CHANNEL_LOG_NUM_TAIL_LINES = None
|
||||||
|
|
||||||
|
|
||||||
|
# logging overrides
|
||||||
|
|
||||||
|
|
||||||
def timeformat(when=None):
|
def timeformat(when=None):
|
||||||
"""
|
"""
|
||||||
This helper function will format the current time in the same
|
This helper function will format the current time in the same
|
||||||
way as twisted's logger does, including time zone info.
|
way as the twisted logger does, including time zone info. Only
|
||||||
|
difference from official logger is that we only use two digits
|
||||||
|
for the year and don't show timezone for CET times.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
when (int, optional): This is a time in POSIX seconds on the form
|
when (int, optional): This is a time in POSIX seconds on the form
|
||||||
|
|
@ -49,14 +55,86 @@ def timeformat(when=None):
|
||||||
tz_offset = tz_offset.days * 86400 + tz_offset.seconds
|
tz_offset = tz_offset.days * 86400 + tz_offset.seconds
|
||||||
# correct given time to utc
|
# correct given time to utc
|
||||||
when = datetime.utcfromtimestamp(when - tz_offset)
|
when = datetime.utcfromtimestamp(when - tz_offset)
|
||||||
|
|
||||||
|
if tz_offset == 0:
|
||||||
|
tz = ""
|
||||||
|
else:
|
||||||
tz_hour = abs(int(tz_offset // 3600))
|
tz_hour = abs(int(tz_offset // 3600))
|
||||||
tz_mins = abs(int(tz_offset // 60 % 60))
|
tz_mins = abs(int(tz_offset // 60 % 60))
|
||||||
tz_sign = "-" if tz_offset >= 0 else "+"
|
tz_sign = "-" if tz_offset >= 0 else "+"
|
||||||
|
tz = "%s%02d%s" % (tz_sign, tz_hour,
|
||||||
|
(":%02d" % tz_mins if tz_mins else ""))
|
||||||
|
|
||||||
return '%d-%02d-%02d %02d:%02d:%02d%s%02d%02d' % (
|
return '%d-%02d-%02d %02d:%02d:%02d%s' % (
|
||||||
when.year, when.month, when.day,
|
when.year - 2000, when.month, when.day,
|
||||||
when.hour, when.minute, when.second,
|
when.hour, when.minute, when.second, tz)
|
||||||
tz_sign, tz_hour, tz_mins)
|
|
||||||
|
|
||||||
|
class WeeklyLogFile(logfile.DailyLogFile):
|
||||||
|
"""
|
||||||
|
Log file that rotates once per week
|
||||||
|
|
||||||
|
"""
|
||||||
|
day_rotation = 7
|
||||||
|
|
||||||
|
def shouldRotate(self):
|
||||||
|
"""Rotate when the date has changed since last write"""
|
||||||
|
# all dates here are tuples (year, month, day)
|
||||||
|
now = self.toDate()
|
||||||
|
then = self.lastDate
|
||||||
|
return now[0] > then[0] or now[1] > then[1] or now[2] > (then[2] + self.day_rotation)
|
||||||
|
|
||||||
|
def write(self, data):
|
||||||
|
"Write data to log file"
|
||||||
|
logfile.BaseLogFile.write(self, data)
|
||||||
|
self.lastDate = max(self.lastDate, self.toDate())
|
||||||
|
|
||||||
|
|
||||||
|
class PortalLogObserver(log.FileLogObserver):
|
||||||
|
"""
|
||||||
|
Reformat logging
|
||||||
|
"""
|
||||||
|
timeFormat = None
|
||||||
|
prefix = " |Portal| "
|
||||||
|
|
||||||
|
def emit(self, eventDict):
|
||||||
|
"""
|
||||||
|
Copied from Twisted parent, to change logging output
|
||||||
|
|
||||||
|
"""
|
||||||
|
text = log.textFromEventDict(eventDict)
|
||||||
|
if text is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
# timeStr = self.formatTime(eventDict["time"])
|
||||||
|
timeStr = timeformat(eventDict["time"])
|
||||||
|
fmtDict = {
|
||||||
|
"text": text.replace("\n", "\n\t")}
|
||||||
|
|
||||||
|
msgStr = log._safeFormat("%(text)s\n", fmtDict)
|
||||||
|
|
||||||
|
twisted_util.untilConcludes(self.write, timeStr + "%s" % self.prefix + msgStr)
|
||||||
|
twisted_util.untilConcludes(self.flush)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerLogObserver(PortalLogObserver):
|
||||||
|
prefix = " "
|
||||||
|
|
||||||
|
|
||||||
|
def log_msg(msg):
|
||||||
|
"""
|
||||||
|
Wrapper around log.msg call to catch any exceptions that might
|
||||||
|
occur in logging. If an exception is raised, we'll print to
|
||||||
|
stdout instead.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg: The message that was passed to log.msg
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
log.msg(msg)
|
||||||
|
except Exception:
|
||||||
|
print("Exception raised while writing message to log. Original message: %s" % msg)
|
||||||
|
|
||||||
|
|
||||||
def log_trace(errmsg=None):
|
def log_trace(errmsg=None):
|
||||||
|
|
@ -80,9 +158,9 @@ def log_trace(errmsg=None):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errmsg = str(e)
|
errmsg = str(e)
|
||||||
for line in errmsg.splitlines():
|
for line in errmsg.splitlines():
|
||||||
log.msg('[EE] %s' % line)
|
log_msg('[EE] %s' % line)
|
||||||
except Exception:
|
except Exception:
|
||||||
log.msg('[EE] %s' % errmsg)
|
log_msg('[EE] %s' % errmsg)
|
||||||
|
|
||||||
|
|
||||||
log_tracemsg = log_trace
|
log_tracemsg = log_trace
|
||||||
|
|
@ -101,13 +179,27 @@ def log_err(errmsg):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errmsg = str(e)
|
errmsg = str(e)
|
||||||
for line in errmsg.splitlines():
|
for line in errmsg.splitlines():
|
||||||
log.msg('[EE] %s' % line)
|
log_msg('[EE] %s' % line)
|
||||||
|
|
||||||
|
|
||||||
# log.err('ERROR: %s' % (errmsg,))
|
# log.err('ERROR: %s' % (errmsg,))
|
||||||
log_errmsg = log_err
|
log_errmsg = log_err
|
||||||
|
|
||||||
|
|
||||||
|
def log_server(servermsg):
|
||||||
|
"""
|
||||||
|
This is for the Portal to log captured Server stdout messages (it's
|
||||||
|
usually only used during startup, before Server log is open)
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
servermsg = str(servermsg)
|
||||||
|
except Exception as e:
|
||||||
|
servermsg = str(e)
|
||||||
|
for line in servermsg.splitlines():
|
||||||
|
log_msg('[Server] %s' % line)
|
||||||
|
|
||||||
|
|
||||||
def log_warn(warnmsg):
|
def log_warn(warnmsg):
|
||||||
"""
|
"""
|
||||||
Prints/logs any warnings that aren't critical but should be noted.
|
Prints/logs any warnings that aren't critical but should be noted.
|
||||||
|
|
@ -121,7 +213,7 @@ def log_warn(warnmsg):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
warnmsg = str(e)
|
warnmsg = str(e)
|
||||||
for line in warnmsg.splitlines():
|
for line in warnmsg.splitlines():
|
||||||
log.msg('[WW] %s' % line)
|
log_msg('[WW] %s' % line)
|
||||||
|
|
||||||
|
|
||||||
# log.msg('WARNING: %s' % (warnmsg,))
|
# log.msg('WARNING: %s' % (warnmsg,))
|
||||||
|
|
@ -139,7 +231,7 @@ def log_info(infomsg):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
infomsg = str(e)
|
infomsg = str(e)
|
||||||
for line in infomsg.splitlines():
|
for line in infomsg.splitlines():
|
||||||
log.msg('[..] %s' % line)
|
log_msg('[..] %s' % line)
|
||||||
|
|
||||||
|
|
||||||
log_infomsg = log_info
|
log_infomsg = log_info
|
||||||
|
|
@ -157,7 +249,7 @@ def log_dep(depmsg):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
depmsg = str(e)
|
depmsg = str(e)
|
||||||
for line in depmsg.splitlines():
|
for line in depmsg.splitlines():
|
||||||
log.msg('[DP] %s' % line)
|
log_msg('[DP] %s' % line)
|
||||||
|
|
||||||
|
|
||||||
log_depmsg = log_dep
|
log_depmsg = log_dep
|
||||||
|
|
@ -219,6 +311,8 @@ class EvenniaLogFile(logfile.LogFile):
|
||||||
|
|
||||||
|
|
||||||
_LOG_FILE_HANDLES = {} # holds open log handles
|
_LOG_FILE_HANDLES = {} # holds open log handles
|
||||||
|
_LOG_FILE_HANDLE_COUNTS = {}
|
||||||
|
_LOG_FILE_HANDLE_RESET = 500
|
||||||
|
|
||||||
|
|
||||||
def _open_log_file(filename):
|
def _open_log_file(filename):
|
||||||
|
|
@ -226,10 +320,15 @@ def _open_log_file(filename):
|
||||||
Helper to open the log file (always in the log dir) and cache its
|
Helper to open the log file (always in the log dir) and cache its
|
||||||
handle. Will create a new file in the log dir if one didn't
|
handle. Will create a new file in the log dir if one didn't
|
||||||
exist.
|
exist.
|
||||||
|
|
||||||
|
To avoid keeping the filehandle open indefinitely we reset it every
|
||||||
|
_LOG_FILE_HANDLE_RESET accesses. This may help resolve issues for very
|
||||||
|
long uptimes and heavy log use.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# we delay import of settings to keep logger module as free
|
# we delay import of settings to keep logger module as free
|
||||||
# from django as possible.
|
# from django as possible.
|
||||||
global _LOG_FILE_HANDLES, _LOGDIR, _LOG_ROTATE_SIZE
|
global _LOG_FILE_HANDLES, _LOG_FILE_HANDLE_COUNTS, _LOGDIR, _LOG_ROTATE_SIZE
|
||||||
if not _LOGDIR:
|
if not _LOGDIR:
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
_LOGDIR = settings.LOG_DIR
|
_LOGDIR = settings.LOG_DIR
|
||||||
|
|
@ -237,13 +336,19 @@ def _open_log_file(filename):
|
||||||
|
|
||||||
filename = os.path.join(_LOGDIR, filename)
|
filename = os.path.join(_LOGDIR, filename)
|
||||||
if filename in _LOG_FILE_HANDLES:
|
if filename in _LOG_FILE_HANDLES:
|
||||||
# cache the handle
|
_LOG_FILE_HANDLE_COUNTS[filename] += 1
|
||||||
return _LOG_FILE_HANDLES[filename]
|
if _LOG_FILE_HANDLE_COUNTS[filename] > _LOG_FILE_HANDLE_RESET:
|
||||||
|
# close/refresh handle
|
||||||
|
_LOG_FILE_HANDLES[filename].close()
|
||||||
|
del _LOG_FILE_HANDLES[filename]
|
||||||
else:
|
else:
|
||||||
|
# return cached handle
|
||||||
|
return _LOG_FILE_HANDLES[filename]
|
||||||
try:
|
try:
|
||||||
filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE)
|
filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE)
|
||||||
# filehandle = open(filename, "a+") # append mode + reading
|
# filehandle = open(filename, "a+") # append mode + reading
|
||||||
_LOG_FILE_HANDLES[filename] = filehandle
|
_LOG_FILE_HANDLES[filename] = filehandle
|
||||||
|
_LOG_FILE_HANDLE_COUNTS[filename] = 0
|
||||||
return filehandle
|
return filehandle
|
||||||
except IOError:
|
except IOError:
|
||||||
log_trace()
|
log_trace()
|
||||||
|
|
|
||||||
|
|
@ -120,9 +120,11 @@ def dbsafe_decode(value, compress_object=False):
|
||||||
|
|
||||||
class PickledWidget(Textarea):
|
class PickledWidget(Textarea):
|
||||||
def render(self, name, value, attrs=None):
|
def render(self, name, value, attrs=None):
|
||||||
|
"""Display of the PickledField in django admin"""
|
||||||
value = repr(value)
|
value = repr(value)
|
||||||
try:
|
try:
|
||||||
literal_eval(value)
|
# necessary to convert it back after repr(), otherwise validation errors will mutate it
|
||||||
|
value = literal_eval(value)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -948,7 +948,7 @@ def delay(timedelay, callback, *args, **kwargs):
|
||||||
specified here.
|
specified here.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
The task handler (`evennia.scripts.taskhandler.TASK_HANDLEr`) will
|
The task handler (`evennia.scripts.taskhandler.TASK_HANDLER`) will
|
||||||
be called for persistent or non-persistent tasks.
|
be called for persistent or non-persistent tasks.
|
||||||
If persistent is set to True, the callback, its arguments
|
If persistent is set to True, the callback, its arguments
|
||||||
and other keyword arguments will be saved in the database,
|
and other keyword arguments will be saved in the database,
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,11 @@
|
||||||
--- */
|
--- */
|
||||||
|
|
||||||
/* Overall element look */
|
/* Overall element look */
|
||||||
html, body, #clientwrapper { height: 100% }
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-size: .9em;
|
font-size: .9em;
|
||||||
|
|
@ -19,6 +20,12 @@ body {
|
||||||
line-height: 1.6em;
|
line-height: 1.6em;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
body {
|
||||||
|
font-size: .5rem;
|
||||||
|
line-height: .7rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
a:link, a:visited { color: inherit; }
|
a:link, a:visited { color: inherit; }
|
||||||
|
|
@ -74,93 +81,109 @@ div {margin:0px;}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Style specific classes corresponding to formatted, narative text. */
|
/* Style specific classes corresponding to formatted, narative text. */
|
||||||
|
.wrapper {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/* Container surrounding entire client */
|
/* Container surrounding entire client */
|
||||||
#wrapper {
|
#clientwrapper {
|
||||||
position: relative;
|
height: 100%;
|
||||||
height: 100%
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Main scrolling message area */
|
/* Main scrolling message area */
|
||||||
|
|
||||||
#messagewindow {
|
#messagewindow {
|
||||||
position: absolute;
|
overflow-y: auto;
|
||||||
overflow: auto;
|
overflow-x: hidden;
|
||||||
padding: 1em;
|
overflow-wrap: break-word;
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 70px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input area containing input field and button */
|
#messagewindow {
|
||||||
#inputform {
|
overflow-y: auto;
|
||||||
position: absolute;
|
overflow-x: hidden;
|
||||||
width: 100%;
|
overflow-wrap: break-word;
|
||||||
padding: 0;
|
|
||||||
bottom: 0;
|
|
||||||
margin: 0;
|
|
||||||
padding-bottom: 10px;
|
|
||||||
border-top: 1px solid #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
#inputcontrol {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input field */
|
/* Input field */
|
||||||
#inputfield, #inputsend, #inputsizer {
|
#inputfield, #inputsizer {
|
||||||
display: block;
|
height: 100%;
|
||||||
-webkit-box-sizing: border-box;
|
|
||||||
-moz-box-sizing: border-box;
|
|
||||||
box-sizing: border-box;
|
|
||||||
height: 50px;
|
|
||||||
background: #000;
|
background: #000;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
padding: 0 .45em;
|
padding: 0 .45rem;
|
||||||
font-size: 1.1em;
|
font-size: 1.1rem;
|
||||||
font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace;
|
font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace;
|
||||||
}
|
|
||||||
|
|
||||||
#inputfield, #inputsizer {
|
|
||||||
float: left;
|
|
||||||
width: 95%;
|
|
||||||
border: 0;
|
|
||||||
resize: none;
|
resize: none;
|
||||||
line-height: normal;
|
}
|
||||||
|
#inputsend {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#inputcontrol {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inputfield:focus {
|
#inputfield:focus {
|
||||||
outline: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#inputsizer {
|
|
||||||
margin-left: -9999px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input 'send' button */
|
|
||||||
#inputsend {
|
|
||||||
float: right;
|
|
||||||
width: 3%;
|
|
||||||
max-width: 25px;
|
|
||||||
margin-right: 10px;
|
|
||||||
border: 0;
|
|
||||||
background: #555;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* prompt area above input field */
|
/* prompt area above input field */
|
||||||
#prompt {
|
.prompt {
|
||||||
margin-top: 10px;
|
max-height: 3rem;
|
||||||
padding: 0 .45em;
|
}
|
||||||
|
|
||||||
|
#splitbutton {
|
||||||
|
width: 2rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #a6a6a6;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#splitbutton:hover {
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panebutton {
|
||||||
|
width: 2rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #a6a6a6;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#panebutton:hover {
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undobutton {
|
||||||
|
width: 2rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #a6a6a6;
|
||||||
|
background-color: transparent;
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#undobutton:hover {
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
width: fit-content;
|
||||||
|
padding: 1em;
|
||||||
|
color: black;
|
||||||
|
border: 1px solid black;
|
||||||
|
background-color: darkgray;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splitbutton:hover {
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#optionsbutton {
|
#optionsbutton {
|
||||||
width: 40px;
|
width: 2rem;
|
||||||
font-size: 20px;
|
font-size: 2rem;
|
||||||
color: #a6a6a6;
|
color: #a6a6a6;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0px;
|
border: 0px;
|
||||||
|
|
@ -173,8 +196,8 @@ div {margin:0px;}
|
||||||
|
|
||||||
#toolbar {
|
#toolbar {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: .5rem;
|
||||||
right: 5px;
|
right: .5rem;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,6 +271,52 @@ div {margin:0px;}
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.gutter.gutter-vertical {
|
||||||
|
cursor: row-resize;
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=')
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter.gutter-horizontal {
|
||||||
|
cursor: col-resize;
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==')
|
||||||
|
}
|
||||||
|
|
||||||
|
.split {
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-sub {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
border: 1px solid #C0C0C0;
|
||||||
|
box-shadow: inset 0 1px 2px #e4e4e4;
|
||||||
|
background-color: black;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.content {
|
||||||
|
padding: .5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gutter {
|
||||||
|
background-color: grey;
|
||||||
|
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split.split-horizontal, .gutter.gutter-horizontal {
|
||||||
|
height: 100%;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
/* XTERM256 colors */
|
/* XTERM256 colors */
|
||||||
|
|
||||||
|
|
|
||||||
145
evennia/web/webclient/static/webclient/js/splithandler.js
Normal file
145
evennia/web/webclient/static/webclient/js/splithandler.js
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
// Use split.js to create a basic ui
|
||||||
|
var SplitHandler = (function () {
|
||||||
|
var split_panes = {};
|
||||||
|
var backout_list = new Array;
|
||||||
|
|
||||||
|
var set_pane_types = function(splitpane, types) {
|
||||||
|
split_panes[splitpane]['types'] = types;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var dynamic_split = function(splitpane, direction, pane_name1, pane_name2, update_method1, update_method2, sizes) {
|
||||||
|
// find the sub-div of the pane we are being asked to split
|
||||||
|
splitpanesub = splitpane + '-sub';
|
||||||
|
|
||||||
|
// create the new div stack to replace the sub-div with.
|
||||||
|
var first_div = $( '<div id="'+pane_name1+'" class="split split-'+direction+'" />' )
|
||||||
|
var first_sub = $( '<div id="'+pane_name1+'-sub" class="split-sub" />' )
|
||||||
|
var second_div = $( '<div id="'+pane_name2+'" class="split split-'+direction+'" />' )
|
||||||
|
var second_sub = $( '<div id="'+pane_name2+'-sub" class="split-sub" />' )
|
||||||
|
|
||||||
|
// check to see if this sub-pane contains anything
|
||||||
|
contents = $('#'+splitpanesub).contents();
|
||||||
|
if( contents ) {
|
||||||
|
// it does, so move it to the first new div-sub (TODO -- selectable between first/second?)
|
||||||
|
contents.appendTo(first_sub);
|
||||||
|
}
|
||||||
|
first_div.append( first_sub );
|
||||||
|
second_div.append( second_sub );
|
||||||
|
|
||||||
|
// update the split_panes array to remove this pane name, but store it for the backout stack
|
||||||
|
var backout_settings = split_panes[splitpane];
|
||||||
|
delete( split_panes[splitpane] );
|
||||||
|
|
||||||
|
// now vaporize the current split_N-sub placeholder and create two new panes.
|
||||||
|
$('#'+splitpane).append(first_div);
|
||||||
|
$('#'+splitpane).append(second_div);
|
||||||
|
$('#'+splitpane+'-sub').remove();
|
||||||
|
|
||||||
|
// And split
|
||||||
|
Split(['#'+pane_name1,'#'+pane_name2], {
|
||||||
|
direction: direction,
|
||||||
|
sizes: sizes,
|
||||||
|
gutterSize: 4,
|
||||||
|
minSize: [50,50],
|
||||||
|
});
|
||||||
|
|
||||||
|
// store our new split sub-divs for future splits/uses by the main UI.
|
||||||
|
split_panes[pane_name1] = { 'types': [], 'update_method': update_method1 };
|
||||||
|
split_panes[pane_name2] = { 'types': [], 'update_method': update_method2 };
|
||||||
|
|
||||||
|
// add our new split to the backout stack
|
||||||
|
backout_list.push( {'pane1': pane_name1, 'pane2': pane_name2, 'undo': backout_settings} );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var undo_split = function() {
|
||||||
|
// pop off the last split pair
|
||||||
|
var back = backout_list.pop();
|
||||||
|
if( !back ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect all the divs/subs in play
|
||||||
|
var pane1 = back['pane1'];
|
||||||
|
var pane2 = back['pane2'];
|
||||||
|
var pane1_sub = $('#'+pane1+'-sub');
|
||||||
|
var pane2_sub = $('#'+pane2+'-sub');
|
||||||
|
var pane1_parent = $('#'+pane1).parent();
|
||||||
|
var pane2_parent = $('#'+pane2).parent();
|
||||||
|
|
||||||
|
if( pane1_parent.attr('id') != pane2_parent.attr('id') ) {
|
||||||
|
// sanity check failed...somebody did something weird...bail out
|
||||||
|
console.log( pane1 );
|
||||||
|
console.log( pane2 );
|
||||||
|
console.log( pane1_parent );
|
||||||
|
console.log( pane2_parent );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new sub-pane in the panes parent
|
||||||
|
var parent_sub = $( '<div id="'+pane1_parent.attr('id')+'-sub" class="split-sub" />' )
|
||||||
|
|
||||||
|
// check to see if the special #messagewindow is in either of our sub-panes.
|
||||||
|
var msgwindow = pane1_sub.find('#messagewindow')
|
||||||
|
if( !msgwindow ) {
|
||||||
|
//didn't find it in pane 1, try pane 2
|
||||||
|
msgwindow = pane2_sub.find('#messagewindow')
|
||||||
|
}
|
||||||
|
if( msgwindow ) {
|
||||||
|
// It is, so collect all contents into it instead of our parent_sub div
|
||||||
|
// then move it to parent sub div, this allows future #messagewindow divs to flow properly
|
||||||
|
msgwindow.append( pane1_sub.contents() );
|
||||||
|
msgwindow.append( pane2_sub.contents() );
|
||||||
|
parent_sub.append( msgwindow );
|
||||||
|
} else {
|
||||||
|
//didn't find it, so move the contents of the two panes' sub-panes into the new sub-pane
|
||||||
|
parent_sub.append( pane1_sub.contents() );
|
||||||
|
parent_sub.append( pane2_sub.contents() );
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear the parent
|
||||||
|
pane1_parent.empty();
|
||||||
|
|
||||||
|
// add the new sub-pane back to the parent div
|
||||||
|
pane1_parent.append(parent_sub);
|
||||||
|
|
||||||
|
// pull the sub-div's from split_panes
|
||||||
|
delete split_panes[pane1];
|
||||||
|
delete split_panes[pane2];
|
||||||
|
|
||||||
|
// add our parent pane back into the split_panes list for future splitting
|
||||||
|
split_panes[pane1_parent.attr('id')] = back['undo'];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var init = function(settings) {
|
||||||
|
//change Mustache tags to ruby-style (Django gets mad otherwise)
|
||||||
|
var customTags = [ '<%', '%>' ];
|
||||||
|
Mustache.tags = customTags;
|
||||||
|
|
||||||
|
var input_template = $('#input-template').html();
|
||||||
|
Mustache.parse(input_template);
|
||||||
|
|
||||||
|
Split(['#main','#input'], {
|
||||||
|
direction: 'vertical',
|
||||||
|
sizes: [90,10],
|
||||||
|
gutterSize: 4,
|
||||||
|
minSize: [50,50],
|
||||||
|
});
|
||||||
|
|
||||||
|
split_panes['main'] = { 'types': [], 'update_method': 'append' };
|
||||||
|
|
||||||
|
var input_render = Mustache.render(input_template);
|
||||||
|
$('[data-role-input]').html(input_render);
|
||||||
|
console.log("SplitHandler initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
init: init,
|
||||||
|
set_pane_types: set_pane_types,
|
||||||
|
dynamic_split: dynamic_split,
|
||||||
|
split_panes: split_panes,
|
||||||
|
undo_split: undo_split,
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -15,8 +15,13 @@
|
||||||
(function () {
|
(function () {
|
||||||
"use strict"
|
"use strict"
|
||||||
|
|
||||||
|
var num_splits = 0; //unique id counter for default split-panel names
|
||||||
|
|
||||||
var options = {};
|
var options = {};
|
||||||
|
|
||||||
|
var known_types = new Array();
|
||||||
|
known_types.push('help');
|
||||||
|
|
||||||
//
|
//
|
||||||
// GUI Elements
|
// GUI Elements
|
||||||
//
|
//
|
||||||
|
|
@ -106,6 +111,7 @@ function togglePopup(dialogname, content) {
|
||||||
|
|
||||||
// Grab text from inputline and send to Evennia
|
// Grab text from inputline and send to Evennia
|
||||||
function doSendText() {
|
function doSendText() {
|
||||||
|
console.log("sending text");
|
||||||
if (!Evennia.isConnected()) {
|
if (!Evennia.isConnected()) {
|
||||||
var reconnect = confirm("Not currently connected. Reconnect?");
|
var reconnect = confirm("Not currently connected. Reconnect?");
|
||||||
if (reconnect) {
|
if (reconnect) {
|
||||||
|
|
@ -158,7 +164,11 @@ function onKeydown (event) {
|
||||||
var code = event.which;
|
var code = event.which;
|
||||||
var history_entry = null;
|
var history_entry = null;
|
||||||
var inputfield = $("#inputfield");
|
var inputfield = $("#inputfield");
|
||||||
inputfield.focus();
|
if (code === 9) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//inputfield.focus();
|
||||||
|
|
||||||
if (code === 13) { // Enter key sends text
|
if (code === 13) { // Enter key sends text
|
||||||
doSendText();
|
doSendText();
|
||||||
|
|
@ -175,8 +185,11 @@ function onKeydown (event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (code === 27) { // Escape key
|
if (code === 27) { // Escape key
|
||||||
closePopup("#optionsdialog");
|
if ($('#helpdialog').is(':visible')) {
|
||||||
closePopup("#helpdialog");
|
closePopup("#helpdialog");
|
||||||
|
} else {
|
||||||
|
closePopup("#optionsdialog");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (history_entry !== null) {
|
if (history_entry !== null) {
|
||||||
|
|
@ -202,74 +215,68 @@ function onKeyPress (event) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var resizeInputField = function () {
|
var resizeInputField = function () {
|
||||||
var min_height = 50;
|
return function() {
|
||||||
var max_height = 300;
|
var wrapper = $("#inputform")
|
||||||
var prev_text_len = 0;
|
var input = $("#inputcontrol")
|
||||||
|
var prompt = $("#prompt")
|
||||||
|
|
||||||
// Check to see if we should change the height of the input area
|
input.height(wrapper.height() - (input.offset().top - wrapper.offset().top));
|
||||||
return function () {
|
|
||||||
var inputfield = $("#inputfield");
|
|
||||||
var scrollh = inputfield.prop("scrollHeight");
|
|
||||||
var clienth = inputfield.prop("clientHeight");
|
|
||||||
var newh = 0;
|
|
||||||
var curr_text_len = inputfield.val().length;
|
|
||||||
|
|
||||||
if (scrollh > clienth && scrollh <= max_height) {
|
|
||||||
// Need to make it bigger
|
|
||||||
newh = scrollh;
|
|
||||||
}
|
|
||||||
else if (curr_text_len < prev_text_len) {
|
|
||||||
// There is less text in the field; try to make it smaller
|
|
||||||
// To avoid repaints, we draw the text in an offscreen element and
|
|
||||||
// determine its dimensions.
|
|
||||||
var sizer = $('#inputsizer')
|
|
||||||
.css("width", inputfield.prop("clientWidth"))
|
|
||||||
.text(inputfield.val());
|
|
||||||
newh = sizer.prop("scrollHeight");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newh != 0) {
|
|
||||||
newh = Math.min(newh, max_height);
|
|
||||||
if (clienth != newh) {
|
|
||||||
inputfield.css("height", newh + "px");
|
|
||||||
doWindowResize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prev_text_len = curr_text_len;
|
|
||||||
}
|
}
|
||||||
}();
|
}();
|
||||||
|
|
||||||
// Handle resizing of client
|
// Handle resizing of client
|
||||||
function doWindowResize() {
|
function doWindowResize() {
|
||||||
var formh = $('#inputform').outerHeight(true);
|
resizeInputField();
|
||||||
var message_scrollh = $("#messagewindow").prop("scrollHeight");
|
var resizable = $("[data-update-append]");
|
||||||
$("#messagewindow")
|
var parents = resizable.closest(".split")
|
||||||
.css({"bottom": formh}) // leave space for the input form
|
parents.animate({
|
||||||
.scrollTop(message_scrollh); // keep the output window scrolled to the bottom
|
scrollTop: parents.prop("scrollHeight")
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle text coming from the server
|
// Handle text coming from the server
|
||||||
function onText(args, kwargs) {
|
function onText(args, kwargs) {
|
||||||
// append message to previous ones, then scroll so latest is at
|
var use_default_pane = true;
|
||||||
// the bottom. Send 'cls' kwarg to modify the output class.
|
|
||||||
var renderto = "main";
|
if ( kwargs && 'type' in kwargs ) {
|
||||||
if (kwargs["type"] == "help") {
|
var msgtype = kwargs['type'];
|
||||||
if (("helppopup" in options) && (options["helppopup"])) {
|
if ( ! known_types.includes(msgtype) ) {
|
||||||
renderto = "#helpdialog";
|
// this is a new output type that can be mapped to panes
|
||||||
|
console.log('detected new output type: ' + msgtype)
|
||||||
|
known_types.push(msgtype);
|
||||||
|
}
|
||||||
|
|
||||||
|
// pass this message to each pane that has this msgtype mapped
|
||||||
|
if( SplitHandler ) {
|
||||||
|
for ( var key in SplitHandler.split_panes) {
|
||||||
|
var pane = SplitHandler.split_panes[key];
|
||||||
|
// is this message type mapped to this pane?
|
||||||
|
if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) {
|
||||||
|
// yes, so append/replace this pane's inner div with this message
|
||||||
|
var text_div = $('#'+key+'-sub');
|
||||||
|
if ( pane['update_method'] == 'replace' ) {
|
||||||
|
text_div.html(args[0])
|
||||||
|
} else {
|
||||||
|
text_div.append(args[0]);
|
||||||
|
var scrollHeight = text_div.parent().prop("scrollHeight");
|
||||||
|
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
|
||||||
|
}
|
||||||
|
// record sending this message to a pane, no need to update the default div
|
||||||
|
use_default_pane = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (renderto == "main") {
|
// append message to default pane, then scroll so latest is at the bottom.
|
||||||
|
if(use_default_pane) {
|
||||||
var mwin = $("#messagewindow");
|
var mwin = $("#messagewindow");
|
||||||
var cls = kwargs == null ? 'out' : kwargs['cls'];
|
var cls = kwargs == null ? 'out' : kwargs['cls'];
|
||||||
mwin.append("<div class='" + cls + "'>" + args[0] + "</div>");
|
mwin.append("<div class='" + cls + "'>" + args[0] + "</div>");
|
||||||
mwin.animate({
|
var scrollHeight = mwin.parent().parent().prop("scrollHeight");
|
||||||
scrollTop: document.getElementById("messagewindow").scrollHeight
|
mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0);
|
||||||
}, 0);
|
|
||||||
|
|
||||||
onNewLine(args[0], null);
|
onNewLine(args[0], null);
|
||||||
} else {
|
|
||||||
openPopup(renderto, args[0]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -427,6 +434,105 @@ function doStartDragDialog(event) {
|
||||||
$(document).bind("mouseup", undrag);
|
$(document).bind("mouseup", undrag);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onSplitDialogClose() {
|
||||||
|
var pane = $("input[name=pane]:checked").attr("value");
|
||||||
|
var direction = $("input[name=direction]:checked").attr("value");
|
||||||
|
var new_pane1 = $("input[name=new_pane1]").val();
|
||||||
|
var new_pane2 = $("input[name=new_pane2]").val();
|
||||||
|
var flow1 = $("input[name=flow1]:checked").attr("value");
|
||||||
|
var flow2 = $("input[name=flow2]:checked").attr("value");
|
||||||
|
|
||||||
|
if( new_pane1 == "" ) {
|
||||||
|
new_pane1 = 'pane_'+num_splits;
|
||||||
|
num_splits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( new_pane2 == "" ) {
|
||||||
|
new_pane2 = 'pane_'+num_splits;
|
||||||
|
num_splits++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( document.getElementById(new_pane1) ) {
|
||||||
|
alert('An element: "' + new_pane1 + '" already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if( document.getElementById(new_pane2) ) {
|
||||||
|
alert('An element: "' + new_pane2 + '" already exists');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SplitHandler.dynamic_split( pane, direction, new_pane1, new_pane2, flow1, flow2, [50,50] );
|
||||||
|
|
||||||
|
closePopup("#splitdialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSplitDialog() {
|
||||||
|
var dialog = $("#splitdialogcontent");
|
||||||
|
dialog.empty();
|
||||||
|
|
||||||
|
dialog.append("<h3>Split?</h3>");
|
||||||
|
dialog.append('<input type="radio" name="direction" value="vertical" checked> top/bottom<br />');
|
||||||
|
dialog.append('<input type="radio" name="direction" value="horizontal"> side-by-side<br />');
|
||||||
|
|
||||||
|
dialog.append("<h3>Split Which Pane?</h3>");
|
||||||
|
for ( var pane in SplitHandler.split_panes ) {
|
||||||
|
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.append("<h3>New Pane Names</h3>");
|
||||||
|
dialog.append('<input type="text" name="new_pane1" value="" />');
|
||||||
|
dialog.append('<input type="text" name="new_pane2" value="" />');
|
||||||
|
|
||||||
|
dialog.append("<h3>New First Pane</h3>");
|
||||||
|
dialog.append('<input type="radio" name="flow1" value="append" checked>append new incoming messages<br />');
|
||||||
|
dialog.append('<input type="radio" name="flow1" value="replace">replace old messages with new ones<br />');
|
||||||
|
|
||||||
|
dialog.append("<h3>New Second Pane</h3>");
|
||||||
|
dialog.append('<input type="radio" name="flow2" value="append" checked>append new incoming messages<br />');
|
||||||
|
dialog.append('<input type="radio" name="flow2" value="replace">replace old messages with new ones<br />');
|
||||||
|
|
||||||
|
dialog.append('<div id="splitclose" class="button">Split It</div>');
|
||||||
|
|
||||||
|
$("#splitclose").bind("click", onSplitDialogClose);
|
||||||
|
|
||||||
|
togglePopup("#splitdialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPaneControlDialogClose() {
|
||||||
|
var pane = $("input[name=pane]:checked").attr("value");
|
||||||
|
|
||||||
|
var types = new Array;
|
||||||
|
$('#splitdialogcontent input[type=checkbox]:checked').each(function() {
|
||||||
|
types.push( $(this).attr('value') );
|
||||||
|
});
|
||||||
|
|
||||||
|
SplitHandler.set_pane_types( pane, types );
|
||||||
|
|
||||||
|
closePopup("#splitdialog");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPaneControlDialog() {
|
||||||
|
var dialog = $("#splitdialogcontent");
|
||||||
|
dialog.empty();
|
||||||
|
|
||||||
|
dialog.append("<h3>Set Which Pane?</h3>");
|
||||||
|
for ( var pane in SplitHandler.split_panes ) {
|
||||||
|
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.append("<h3>Which content types?</h3>");
|
||||||
|
for ( var type in known_types ) {
|
||||||
|
dialog.append('<input type="checkbox" value="'+ known_types[type] +'">'+ known_types[type] +'<br />');
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.append('<div id="paneclose" class="button">Make It So</div>');
|
||||||
|
|
||||||
|
$("#paneclose").bind("click", onPaneControlDialogClose);
|
||||||
|
|
||||||
|
togglePopup("#splitdialog");
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Register Events
|
// Register Events
|
||||||
//
|
//
|
||||||
|
|
@ -434,6 +540,18 @@ function doStartDragDialog(event) {
|
||||||
// Event when client finishes loading
|
// Event when client finishes loading
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
|
|
||||||
|
if( SplitHandler ) {
|
||||||
|
SplitHandler.init();
|
||||||
|
$("#splitbutton").bind("click", onSplitDialog);
|
||||||
|
$("#panebutton").bind("click", onPaneControlDialog);
|
||||||
|
$("#undobutton").bind("click", SplitHandler.undo_split);
|
||||||
|
$("#optionsbutton").hide();
|
||||||
|
} else {
|
||||||
|
$("#splitbutton").hide();
|
||||||
|
$("#panebutton").hide();
|
||||||
|
$("#undobutton").hide();
|
||||||
|
}
|
||||||
|
|
||||||
if ("Notification" in window) {
|
if ("Notification" in window) {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
}
|
}
|
||||||
|
|
@ -450,7 +568,7 @@ $(document).ready(function() {
|
||||||
|
|
||||||
//$(document).on("visibilitychange", onVisibilityChange);
|
//$(document).on("visibilitychange", onVisibilityChange);
|
||||||
|
|
||||||
$("#inputfield").bind("resize", doWindowResize)
|
$("[data-role-input]").bind("resize", doWindowResize)
|
||||||
.keypress(onKeyPress)
|
.keypress(onKeyPress)
|
||||||
.bind("paste", resizeInputField)
|
.bind("paste", resizeInputField)
|
||||||
.bind("cut", resizeInputField);
|
.bind("cut", resizeInputField);
|
||||||
|
|
@ -503,6 +621,7 @@ $(document).ready(function() {
|
||||||
},
|
},
|
||||||
60000*3
|
60000*3
|
||||||
);
|
);
|
||||||
|
console.log("Completed GUI setup");
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ JQuery available.
|
||||||
<meta http-equiv="content-type", content="application/xhtml+xml; charset=UTF-8" />
|
<meta http-equiv="content-type", content="application/xhtml+xml; charset=UTF-8" />
|
||||||
<meta name="author" content="Evennia" />
|
<meta name="author" content="Evennia" />
|
||||||
<meta name="generator" content="Evennia" />
|
<meta name="generator" content="Evennia" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||||
|
|
||||||
|
<!-- Bootstrap CSS -->
|
||||||
|
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
|
||||||
|
|
||||||
<link rel='stylesheet' type="text/css" media="screen" href={% static "webclient/css/webclient.css" %}>
|
<link rel='stylesheet' type="text/css" media="screen" href={% static "webclient/css/webclient.css" %}>
|
||||||
|
|
||||||
|
|
@ -20,15 +24,23 @@ JQuery available.
|
||||||
|
|
||||||
<!-- Import JQuery and warn if there is a problem -->
|
<!-- Import JQuery and warn if there is a problem -->
|
||||||
{% block jquery_import %}
|
{% block jquery_import %}
|
||||||
<script src="https://code.jquery.com/jquery-2.1.1.min.js" type="text/javascript" charset="utf-8"></script>
|
<script src="https://code.jquery.com/jquery-3.2.1.min.js" type="text/javascript" charset="utf-8"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<script type="text/javascript" charset="utf-8">
|
<script type="text/javascript" charset="utf-8">
|
||||||
if(!window.jQuery) {
|
if(!window.jQuery) {
|
||||||
document.write("<div class='err'>jQuery library not found or the online version could not be reached.</div>");
|
document.write("<div class='err'>jQuery library not found or the online version could not be reached. Check so Javascript is not blocked in your browser.</div>");
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<!-- This is will only fire if javascript is actually active -->
|
||||||
|
<script language="javascript" type="text/javascript">
|
||||||
|
$(document).ready(function() {
|
||||||
|
$('#noscript').remove();
|
||||||
|
$('#clientwrapper').removeClass('d-none');
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Set up Websocket url and load the evennia.js library-->
|
<!-- Set up Websocket url and load the evennia.js library-->
|
||||||
<script language="javascript" type="text/javascript">
|
<script language="javascript" type="text/javascript">
|
||||||
{% if websocket_enabled %}
|
{% if websocket_enabled %}
|
||||||
|
|
@ -51,13 +63,29 @@ JQuery available.
|
||||||
</script>
|
</script>
|
||||||
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
|
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- set up splits before loading the GUI -->
|
||||||
|
<script src="https://unpkg.com/split.js/split.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
|
||||||
|
<script src={% static "webclient/js/splithandler.js" %} language="javascript"></script>
|
||||||
|
|
||||||
<!-- Load gui library -->
|
<!-- Load gui library -->
|
||||||
{% block guilib_import %}
|
{% block guilib_import %}
|
||||||
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
|
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
<script src="https://cdn.rawgit.com/ejci/favico.js/master/favico-0.3.10.min.js" language="javascript" type="text/javascript" charset="utf-8"></script>
|
<script src="https://cdn.rawgit.com/ejci/favico.js/master/favico-0.3.10.min.js" language="javascript" type="text/javascript" charset="utf-8"></script>
|
||||||
|
<script type="text/javascript" charset="utf-8">
|
||||||
|
if(!window.Favico) {
|
||||||
|
document.write("<div class='err'>Favico.js library not found or the online version could not be reached. Check so Javascript is not blocked in your browser.</div>");
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- jQuery first, then Tether, then Bootstrap JS. -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
|
||||||
|
{% block scripts %}
|
||||||
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
|
|
@ -80,10 +108,9 @@ JQuery available.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- main client -->
|
<!-- main client -->
|
||||||
<div id=clientwrapper>
|
<div id=clientwrapper class="d-none">
|
||||||
{% block client %}
|
{% block client %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,29 @@
|
||||||
|
|
||||||
|
|
||||||
{% block client %}
|
{% block client %}
|
||||||
|
<div id="toolbar">
|
||||||
|
<button id="optionsbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Settings</span></button>
|
||||||
|
<button id="splitbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⇹<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||||
|
<button id="panebutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||||
|
<button id="undobutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">↶<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="wrapper">
|
<!-- The "Main" Content -->
|
||||||
<div id="toolbar">
|
<div id="main" class="split split-vertical" data-role-default>
|
||||||
<button id="optionsbutton" type="button" class="hidden">⚙</button>
|
<div id="main-sub" class="split-sub">
|
||||||
|
<div id="messagewindow"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="messagewindow" role="log"></div>
|
</div>
|
||||||
<div id="inputform">
|
<!-- The "Input" Pane -->
|
||||||
<div id="prompt"></div>
|
<div id="input" class="split split-vertical" data-role-input data-update-append></div>
|
||||||
<div id="inputcontrol">
|
|
||||||
<textarea id="inputfield" type="text"></textarea>
|
<!-- Basic UI Components -->
|
||||||
<input id="inputsend" type="button" value=">"/>
|
<div id="splitdialog" class="dialog">
|
||||||
|
<div class="dialogtitle">Split Pane<span class="dialogclose">×</span></div>
|
||||||
|
<div class="dialogcontentparent">
|
||||||
|
<div id="splitdialogcontent" class="dialogcontent">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="inputsizer"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="optionsdialog" class="dialog">
|
<div id="optionsdialog" class="dialog">
|
||||||
|
|
@ -47,4 +56,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script type="text/html" id="split-template">
|
||||||
|
<div class="split content<%#horizontal%> split-horizontal<%/horizontal%>" id='<%id%>'>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" id="output-template">
|
||||||
|
<div id="<%id%>" role="log" data-role-output data-update-append data-tags='[<%#tags%>"<%.%>", <%/tags%>]'></div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" id="input-template">
|
||||||
|
<div id="inputform" class="wrapper">
|
||||||
|
<div id="prompt" class="prompt">
|
||||||
|
</div>
|
||||||
|
<div id="inputcontrol" class="input-group">
|
||||||
|
<textarea id="inputfield" type="text" class="form-control"></textarea>
|
||||||
|
<span class="input-group-btn">
|
||||||
|
<button class="btn btn-large btn-outline-primary" id="inputsend" type="button" value="">></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,13 @@ def _shared_login(request):
|
||||||
if webclient_uid:
|
if webclient_uid:
|
||||||
# The webclient has previously registered a login to this browser_session
|
# The webclient has previously registered a login to this browser_session
|
||||||
if not account.is_authenticated() and not website_uid:
|
if not account.is_authenticated() and not website_uid:
|
||||||
|
try:
|
||||||
account = AccountDB.objects.get(id=webclient_uid)
|
account = AccountDB.objects.get(id=webclient_uid)
|
||||||
|
except AccountDB.DoesNotExist:
|
||||||
|
# this can happen e.g. for guest accounts or deletions
|
||||||
|
csession["website_authenticated_uid"] = False
|
||||||
|
csession["webclient_authenticated_uid"] = False
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
# calls our custom authenticate in web/utils/backends.py
|
# calls our custom authenticate in web/utils/backends.py
|
||||||
account = authenticate(autologin=account)
|
account = authenticate(autologin=account)
|
||||||
|
|
|
||||||
|
|
@ -7,3 +7,4 @@ pillow == 2.9.0
|
||||||
pytz
|
pytz
|
||||||
future >= 0.15.2
|
future >= 0.15.2
|
||||||
django-sekizai
|
django-sekizai
|
||||||
|
inflect
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,4 @@ pillow == 2.9.0
|
||||||
pytz
|
pytz
|
||||||
future >= 0.15.2
|
future >= 0.15.2
|
||||||
django-sekizai
|
django-sekizai
|
||||||
|
inflect
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue