PEP8 cleanup of the entire codebase. Unchanged are many cases of too-long lines, partly because of the rewrite they would require but also because splitting many lines up would make the code harder to read. Also the third-party libraries (idmapper, prettytable etc) were not cleaned.

This commit is contained in:
Griatch 2013-11-14 19:31:17 +01:00
parent 30b7d2a405
commit 1ae17bcbe4
154 changed files with 5613 additions and 4054 deletions

View file

@ -1,2 +1,3 @@
# experimental central dictionary for models in subprocesses to report they have been changed.
# experimental central dictionary for models in
# subprocesses to report they have been changed.
PROC_MODIFIED_OBJS = []

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -72,20 +72,25 @@ CMD_LOGINSTART = "__unloggedin_look_command"
# custom Exceptions
class NoCmdSets(Exception):
"No cmdsets found. Critical error."
pass
class ExecSystemCommand(Exception):
"Run a system command"
def __init__(self, syscmd, sysarg):
self.args = (syscmd, sysarg) # needed by exception error handling
self.args = (syscmd, sysarg) # needed by exception error handling
self.syscmd = syscmd
self.sysarg = sysarg
# Helper function
@inlineCallbacks
def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None):
def get_and_merge_cmdsets(caller, session, player, obj,
callertype, sessid=None):
"""
Gather all relevant cmdsets and merge them.
@ -124,20 +129,25 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None)
if location and not obj_cmdset.no_objs:
# Gather all cmdsets stored on objects in the room and
# also in the caller's inventory and the location itself
local_objlist = yield location.contents_get(exclude=obj.dbobj) + obj.contents + [location]
local_objlist = yield (location.contents_get(exclude=obj.dbobj) +
obj.contents +
[location])
for lobj in local_objlist:
try:
# call hook in case we need to do dynamic changing to cmdset
_GA(lobj, "at_cmdset_get")()
except Exception:
logger.log_trace()
# the call-type lock is checked here, it makes sure a player is not seeing e.g. the commands
# on a fellow player (which is why the no_superuser_bypass must be True)
local_obj_cmdsets = yield [lobj.cmdset.current for lobj in local_objlist
if (lobj.cmdset.current and lobj.locks.check(caller, 'call', no_superuser_bypass=True))]
# the call-type lock is checked here, it makes sure a player
# is not seeing e.g. the commands on a fellow player (which is why
# the no_superuser_bypass must be True)
local_obj_cmdsets = \
yield [lobj.cmdset.current for lobj in local_objlist
if (lobj.cmdset.current and
lobj.locks.check(caller, 'call', no_superuser_bypass=True))]
for cset in local_obj_cmdsets:
#This is necessary for object sets, or we won't be able to separate
#the command sets from each other in a busy room.
#This is necessary for object sets, or we won't be able to
# separate the command sets from each other in a busy room.
cset.old_duplicates = cset.duplicates
cset.duplicates = True
returnValue(local_obj_cmdsets)
@ -159,8 +169,8 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None)
report_to = session
session_cmdset = yield _get_cmdset(session)
cmdsets = [session_cmdset]
if player: # this automatically implies logged-in
player_cmdset = yield _get_cmdset(player)
if player: # this automatically implies logged-in
player_cmdset = yield _get_cmdset(player)
channel_cmdset = yield _get_channel_cmdsets(player, player_cmdset)
cmdsets.extend([player_cmdset, channel_cmdset])
if obj:
@ -185,21 +195,26 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None)
cmdsets = [obj_cmdset] + local_obj_cmdsets
else:
raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype)
#cmdsets = yield [caller_cmdset] + [player_cmdset] + [channel_cmdset] + local_obj_cmdsets
#cmdsets = yield [caller_cmdset] + [player_cmdset] +
# [channel_cmdset] + local_obj_cmdsets
# weed out all non-found sets
cmdsets = yield [cmdset for cmdset in cmdsets if cmdset and cmdset.key!="Empty"]
cmdsets = yield [cmdset for cmdset in cmdsets
if cmdset and cmdset.key != "Empty"]
# report cmdset errors to user (these should already have been logged)
yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"]
yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets
if cmdset.key == "_CMDSET_ERROR"]
if cmdsets:
mergehash = tuple([id(cmdset) for cmdset in cmdsets]) # faster to do tuple on list than to build tuple directly
# faster to do tuple on list than to build tuple directly
mergehash = tuple([id(cmdset) for cmdset in cmdsets])
if mergehash in _CMDSET_MERGE_CACHE:
# cached merge exist; use that
cmdset = _CMDSET_MERGE_CACHE[mergehash]
else:
# we group and merge all same-prio cmdsets separately (this avoids order-dependent
# clashes in certain cases, such as when duplicates=True)
# we group and merge all same-prio cmdsets separately (this avoids
# order-dependent clashes in certain cases, such as
# when duplicates=True)
tempmergers = {}
for cmdset in cmdsets:
prio = cmdset.priority
@ -241,13 +256,13 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi
if True, the command instance will be returned instead.
callertype - this is one of "session", "player" or "object", in decending
order. So when the Session is the caller, it will merge its
own cmdset into cmdsets from both Player and eventual puppeted Object (and
cmdsets in its room etc). A Player will only include its
own cmdset and the Objects and so on. Merge order is the
same order, so that Object cmdsets are merged in last, giving
them precendence for same-name and same-prio commands.
sessid - Relevant if callertype is "player" - the session id will help retrieve the
correct cmdsets from puppeted objects.
own cmdset into cmdsets from both Player and eventual puppeted
Object (and cmdsets in its room etc). A Player will only
include its own cmdset and the Objects and so on. Merge order
is the same order, so that Object cmdsets are merged in last,
giving them precendence for same-name and same-prio commands.
sessid - Relevant if callertype is "player" - the session id will help
retrieve the correct cmdsets from puppeted objects.
Note that this function returns a deferred!
"""
@ -270,10 +285,11 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi
# we assign the caller with preference 'bottom up'
caller = obj or player or session
try: # catch bugs in cmdhandler itself
try: # catch special-type commands
try: # catch bugs in cmdhandler itself
try: # catch special-type commands
cmdset = yield get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid)
cmdset = yield get_and_merge_cmdsets(caller, session, player, obj,
callertype, sessid)
if not cmdset:
# this is bad and shouldn't happen.
raise NoCmdSets
@ -323,14 +339,15 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi
else:
# fallback to default error text
sysarg = _("Command '%s' is not available.") % raw_string
suggestions = string_suggestions(raw_string, cmdset.get_all_cmd_keys_and_aliases(caller), cutoff=0.7, maxnum=3)
suggestions = string_suggestions(raw_string,
cmdset.get_all_cmd_keys_and_aliases(caller),
cutoff=0.7, maxnum=3)
if suggestions:
sysarg += _(" Maybe you meant %s?") % utils.list_to_string(suggestions, _('or'), addquote=True)
else:
sysarg += _(" Type \"help\" for help.")
raise ExecSystemCommand(syscmd, sysarg)
# Check if this is a Channel-cmd match.
if hasattr(cmd, 'is_channel') and cmd.is_channel:
# even if a user-defined syscmd is not defined, the
@ -380,7 +397,8 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi
for func_part in make_iter(cmd.func_parts):
err = yield func_part()
# returning anything but a deferred/None will kill the chain
if err: break
if err:
break
# post-command hook
yield cmd.at_post_cmd()

View file

@ -51,10 +51,10 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
for cmd in cmdset:
try:
matches.extend([create_match(cmdname, raw_string, cmd)
for cmdname in [cmd.key] + cmd.aliases
if cmdname and l_raw_string.startswith(cmdname.lower())
and (not cmd.arg_regex or
cmd.arg_regex.match(l_raw_string[len(cmdname):]))])
for cmdname in [cmd.key] + cmd.aliases
if cmdname and l_raw_string.startswith(cmdname.lower())
and (not cmd.arg_regex or
cmd.arg_regex.match(l_raw_string[len(cmdname):]))])
except Exception:
log_trace("cmdhandler error. raw_input:%s" % raw_string)
@ -67,7 +67,8 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
if mindex.isdigit():
mindex = int(mindex) - 1
# feed result back to parser iteratively
return cmdparser(new_raw_string, cmdset, caller, match_index=mindex)
return cmdparser(new_raw_string, cmdset,
caller, match_index=mindex)
# only select command matches we are actually allowed to call.
matches = [match for match in matches if match[2].access(caller, 'cmd')]
@ -75,7 +76,8 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
if len(matches) > 1:
# See if it helps to analyze the match with preserved case but only if
# it leaves at least one match.
trimmed = [match for match in matches if raw_string.startswith(match[0])]
trimmed = [match for match in matches
if raw_string.startswith(match[0])]
if trimmed:
matches = trimmed
@ -94,15 +96,13 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
matches = matches[-quality.count(quality[-1]):]
if len(matches) > 1 and match_index != None and 0 <= match_index < len(matches):
# We couldn't separate match by quality, but we have an index argument to
# tell us which match to use.
# We couldn't separate match by quality, but we have an
# index argument to tell us which match to use.
matches = [matches[match_index]]
# no matter what we have at this point, we have to return it.
return matches
#------------------------------------------------------------
# Search parsers and support methods
#------------------------------------------------------------
@ -118,7 +118,6 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
# The the replacing modules must have the same inputs and outputs as
# those in this module.
#
def at_search_result(msg_obj, ostring, results, global_search=False,
nofound_string=None, multimatch_string=None):
"""
@ -176,7 +175,7 @@ def at_search_result(msg_obj, ostring, results, global_search=False,
invtext = _(" (carried)")
if show_dbref:
dbreftext = "(#%i)" % result.dbid
string += "\n %i-%s%s%s" % (num+1, result.name,
string += "\n %i-%s%s%s" % (num + 1, result.name,
dbreftext, invtext)
results = None
else:
@ -187,6 +186,7 @@ def at_search_result(msg_obj, ostring, results, global_search=False,
msg_obj.msg(string.strip())
return results
def at_multimatch_input(ostring):
"""
Parse number-identifiers.
@ -231,9 +231,9 @@ def at_multimatch_input(ostring):
if not '-' in ostring:
return (None, ostring)
try:
index = ostring.find('-')
number = int(ostring[:index])-1
return (number, ostring[index+1:])
index = ostring.find('-')
number = int(ostring[:index]) - 1
return (number, ostring[index + 1:])
except ValueError:
#not a number; this is not an identifier.
return (None, ostring)
@ -256,13 +256,15 @@ def at_multimatch_cmd(caller, matches):
else:
is_channel = ""
if cmd.is_exit and cmd.destination:
is_exit = _(" (exit to %s)") % cmd.destination
is_exit = (" (exit to %s)") % cmd.destination
else:
is_exit = ""
id1 = ""
id2 = ""
if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller) and hasattr(cmd.obj, "key"):
if (not (is_channel or is_exit) and
(hasattr(cmd, 'obj') and cmd.obj != caller) and
hasattr(cmd.obj, "key")):
# the command is defined on some other object
id1 = "%s-%s" % (num + 1, cmdname)
id2 = " (%s)" % (cmd.obj.key)

View file

@ -18,6 +18,7 @@ from django.utils.translation import ugettext as _
from src.utils.utils import inherits_from, is_iter
__all__ = ("CmdSet",)
class _CmdSetMeta(type):
"""
This metaclass makes some minor on-the-fly convenience fixes to
@ -38,15 +39,18 @@ class _CmdSetMeta(type):
super(_CmdSetMeta, mcs).__init__(*args, **kwargs)
class CmdSet(object):
"""
This class describes a unique cmdset that understands priorities. CmdSets
can be merged and made to perform various set operations on each other.
CmdSets have priorities that affect which of their ingoing commands gets used.
CmdSets have priorities that affect which of their ingoing commands
gets used.
In the examples, cmdset A always have higher priority than cmdset B.
key - the name of the cmdset. This can be used on its own for game operations
key - the name of the cmdset. This can be used on its own for game
operations
mergetype (partly from Set theory):
@ -80,8 +84,9 @@ class CmdSet(object):
priority- All cmdsets are always merged in pairs of two so that
the higher set's mergetype is applied to the
lower-priority cmdset. Default commands have priority 0,
high-priority ones like Exits and Channels have 10 and 9. Priorities
can be negative as well to give default commands preference.
high-priority ones like Exits and Channels have 10 and 9.
Priorities can be negative as well to give default
commands preference.
duplicates - determines what happens when two sets of equal
priority merge. Default has the first of them in the
@ -130,16 +135,17 @@ class CmdSet(object):
permanent = False
errmessage = ""
# pre-store properties to duplicate straight off
to_duplicate = ("key", "cmdsetobj", "no_exits", "no_objs", "no_channels", "permanent",
"mergetype", "priority", "duplicates", "errmessage")
to_duplicate = ("key", "cmdsetobj", "no_exits", "no_objs",
"no_channels", "permanent", "mergetype",
"priority", "duplicates", "errmessage")
def __init__(self, cmdsetobj=None, key=None):
"""
Creates a new CmdSet instance.
cmdsetobj - this is the database object to which this particular
instance of cmdset is related. It is often a player but may also be a
regular object.
instance of cmdset is related. It is often a character but
may also be a regular object.
"""
if key:
self.key = key
@ -161,7 +167,8 @@ class CmdSet(object):
if cmdset_a.duplicates and cmdset_a.priority == cmdset_b.priority:
cmdset_c.commands.extend(cmdset_b.commands)
else:
cmdset_c.commands.extend([cmd for cmd in cmdset_b if not cmd in cmdset_a])
cmdset_c.commands.extend([cmd for cmd in cmdset_b
if not cmd in cmdset_a])
return cmdset_c
def _intersect(self, cmdset_a, cmdset_b):
@ -206,7 +213,8 @@ class CmdSet(object):
cmdset = CmdSet()
for key, val in ((key, getattr(self, key)) for key in self.to_duplicate):
if val != getattr(cmdset, key):
# only copy if different from default; avoid turning class-vars into instance vars
# only copy if different from default; avoid turning
# class-vars into instance vars
setattr(cmdset, key, val)
cmdset.key_mergetypes = self.key_mergetypes.copy()
return cmdset
@ -230,10 +238,11 @@ class CmdSet(object):
def __contains__(self, othercmd):
"""
Returns True if this cmdset contains the given command (as defined
by command name and aliases). This allows for things like 'if cmd in cmdset'
by command name and aliases). This allows for things
like 'if cmd in cmdset'
"""
ret = self._contains_cache.get(othercmd)
if ret == None:
if ret is None:
ret = othercmd in self.commands
self._contains_cache[othercmd] = ret
return ret
@ -264,7 +273,8 @@ class CmdSet(object):
# A higher or equal priority than B
# preserve system __commands
sys_commands = sys_commands_a + [cmd for cmd in sys_commands_b if cmd not in sys_commands_a]
sys_commands = sys_commands_a + [cmd for cmd in sys_commands_b
if cmd not in sys_commands_a]
mergetype = self.key_mergetypes.get(cmdset_b.key, self.mergetype)
if mergetype == "Intersect":
@ -286,7 +296,8 @@ class CmdSet(object):
# B higher priority than A
# preserver system __commands
sys_commands = sys_commands_b + [cmd for cmd in sys_commands_a if cmd not in sys_commands_b]
sys_commands = sys_commands_b + [cmd for cmd in sys_commands_a
if cmd not in sys_commands_b]
mergetype = cmdset_b.key_mergetypes.get(self.key, cmdset_b.mergetype)
if mergetype == "Intersect":
@ -295,7 +306,7 @@ class CmdSet(object):
cmdset_c = self._replace(cmdset_b, self)
elif mergetype == "Remove":
cmdset_c = self._remove(self, cmdset_b)
else: # Union
else: # Union
cmdset_c = self._union(cmdset_b, self)
cmdset_c.no_channels = cmdset_b.no_channels
cmdset_c.no_exits = cmdset_b.no_exits
@ -311,10 +322,8 @@ class CmdSet(object):
# return the system commands to the cmdset
cmdset_c.add(sys_commands)
return cmdset_c
def add(self, cmd):
"""
Add a command, a list of commands or a cmdset to this cmdset.
@ -338,9 +347,12 @@ class CmdSet(object):
try:
cmd = self._instantiate(cmd)
except RuntimeError:
string = "Adding cmdset %(cmd)s to %(class)s lead to an infinite loop. When adding a cmdset to another, "
string += "make sure they are not themself cyclically added to the new cmdset somewhere in the chain."
raise RuntimeError(_(string) % {"cmd":cmd, "class":self.__class__})
string = "Adding cmdset %(cmd)s to %(class)s lead to an "
string += "infinite loop. When adding a cmdset to another, "
string += "make sure they are not themself cyclically added to "
string += "the new cmdset somewhere in the chain."
raise RuntimeError(_(string) % {"cmd": cmd,
"class": self.__class__})
cmds = cmd.commands
elif is_iter(cmd):
cmds = [self._instantiate(c) for c in cmd]
@ -354,7 +366,7 @@ class CmdSet(object):
cmd.obj = self.cmdsetobj
try:
ic = commands.index(cmd)
commands[ic] = cmd # replace
commands[ic] = cmd # replace
except ValueError:
commands.append(cmd)
# extra run to make sure to avoid doublets
@ -365,11 +377,10 @@ class CmdSet(object):
if cmd.key.startswith("__"):
try:
ic = system_commands.index(cmd)
system_commands[ic] = cmd # replace
system_commands[ic] = cmd # replace
except ValueError:
system_commands.append(cmd)
def remove(self, cmd):
"""
Remove a command instance from the cmdset.
@ -431,7 +442,8 @@ class CmdSet(object):
"""
names = []
if caller:
[names.extend(cmd._keyaliases) for cmd in self.commands if cmd.access(caller)]
[names.extend(cmd._keyaliases) for cmd in self.commands
if cmd.access(caller)]
else:
[names.extend(cmd._keyaliases) for cmd in self.commands]
return names

View file

@ -63,7 +63,6 @@ can then implement separate sets for different situations. For
example, you can have a 'On a boat' set, onto which you then tack on
the 'Fishing' set. Fishing from a boat? No problem!
"""
import traceback
from src.utils import logger, utils
from src.commands.cmdset import CmdSet
from src.server.models import ServerConfig
@ -73,23 +72,27 @@ __all__ = ("import_cmdset", "CmdSetHandler")
_CACHED_CMDSETS = {}
class _ErrorCmdSet(CmdSet):
"This is a special cmdset used to report errors"
key = "_CMDSET_ERROR"
errmessage = "Error when loading cmdset."
def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False):
"""
This helper function is used by the cmdsethandler to load a cmdset
instance from a python module, given a python_path. It's usually accessed
through the cmdsethandler's add() and add_default() methods.
python_path - This is the full path to the cmdset object.
cmdsetobj - the database object/typeclass on which this cmdset is to be assigned
(this can be also channels and exits, as well as players but there will
always be such an object)
emit_to_obj - if given, error is emitted to this object (in addition to logging)
no_logging - don't log/send error messages. This can be useful if import_cmdset is just
used to check if this is a valid python path or not.
cmdsetobj - the database object/typeclass on which this cmdset is to be
assigned (this can be also channels and exits, as well as players
but there will always be such an object)
emit_to_obj - if given, error is emitted to this object (in addition
to logging)
no_logging - don't log/send error messages. This can be useful
if import_cmdset is just used to check if this is a
valid python path or not.
function returns None if an error was encountered or path not found.
"""
@ -117,7 +120,8 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False):
raise
except KeyError:
errstring = _("Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s.")
errstring = errstring % {"classname":classname, "modulepath":modulepath}
errstring = errstring % {"classname": classname,
"modulepath": modulepath}
raise
except Exception:
errstring = _("Compile/Run error when loading cmdset '%s'. Error was logged.")
@ -135,15 +139,17 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False):
# classes
class CmdSetHandler(object):
"""
The CmdSetHandler is always stored on an object, this object is supplied as an argument.
The CmdSetHandler is always stored on an object, this object is supplied
as an argument.
The 'current' cmdset is the merged set currently active for this object.
This is the set the game engine will retrieve when determining which
commands are available to the object. The cmdset_stack holds a history of all CmdSets
to allow the handler to remove/add cmdsets at will. Doing so will re-calculate
the 'current' cmdset.
commands are available to the object. The cmdset_stack holds a history of
all CmdSets to allow the handler to remove/add cmdsets at will. Doing so
will re-calculate the 'current' cmdset.
"""
def __init__(self, obj):
@ -176,10 +182,8 @@ class CmdSetHandler(object):
mergelist = []
if len(self.cmdset_stack) > 1:
# We have more than one cmdset in stack; list them all
num = 0
#print self.cmdset_stack, self.mergetype_stack
for snum, cmdset in enumerate(self.cmdset_stack):
num = snum
mergetype = self.mergetype_stack[snum]
permstring = "non-perm"
if cmdset.permanent:
@ -196,17 +200,21 @@ class CmdSetHandler(object):
mergetype = self.mergetype_stack[-1]
if mergetype != self.current.mergetype:
merged_on = self.cmdset_stack[-2].key
mergetype = _("custom %(mergetype)s on cmdset '%(merged_on)s'") % {"mergetype":mergetype, "merged_on":merged_on}
mergetype = _("custom %(mergetype)s on cmdset '%(merged_on)s'") % \
{"mergetype": mergetype, "merged_on":merged_on}
if mergelist:
string += _(" <Merged %(mergelist)s (%(mergetype)s, prio %(prio)i)>: %(current)s") % \
{"mergelist": "+".join(mergelist), "mergetype":mergetype, "prio":self.current.priority, "current":self.current}
{"mergelist": "+".join(mergelist),
"mergetype": mergetype, "prio": self.current.priority,
"current":self.current}
else:
permstring = "non-perm"
if self.current.permanent:
permstring = "perm"
string += _(" <%(key)s (%(mergetype)s, prio %(prio)i, %(permstring)s)>: %(keylist)s") % \
{"key":self.current.key, "mergetype":mergetype, "prio":self.current.priority, "permstring":permstring,
"keylist":", ".join(cmd.key for cmd in sorted(self.current, key=lambda o:o.key))}
{"key": self.current.key, "mergetype": mergetype,
"prio": self.current.priority, "permstring": permstring,
"keylist": ", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key))}
return string.strip()
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
@ -362,10 +370,12 @@ class CmdSetHandler(object):
else:
# try it as a callable
if callable(cmdset) and hasattr(cmdset, 'path'):
delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset.path]
delcmdsets = [cset for cset in self.cmdset_stack[1:]
if cset.path == cmdset.path]
else:
# try it as a path or key
delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset or cset.key == cmdset]
delcmdsets = [cset for cset in self.cmdset_stack[1:]
if cset.path == cmdset or cset.key == cmdset]
storage = []
if any(cset.permanent for cset in delcmdsets):
@ -387,7 +397,10 @@ class CmdSetHandler(object):
self.update()
def delete_default(self):
"This explicitly deletes the default cmdset. It's the only command that can."
"""
This explicitly deletes the default cmdset. It's the
only command that can.
"""
if self.cmdset_stack:
cmdset = self.cmdset_stack[0]
if cmdset.permanent:
@ -404,7 +417,8 @@ class CmdSetHandler(object):
def all(self):
"""
Returns the list of cmdsets. Mostly useful to check if stack if empty or not.
Returns the list of cmdsets. Mostly useful to check
if stack if empty or not.
"""
return self.cmdset_stack
@ -431,13 +445,6 @@ class CmdSetHandler(object):
else:
return any([cmdset.key == cmdset_key for cmdset in self.cmdset_stack])
def all(self):
"""
Returns all cmdsets.
"""
return self.cmdset_stack
def reset(self):
"""
Force reload of all cmdsets in handler. This should be called

View file

@ -9,6 +9,7 @@ import re
from src.locks.lockhandler import LockHandler
from src.utils.utils import is_iter, fill
def _init_command(mcs, **kwargs):
"""
Helper command.
@ -17,20 +18,23 @@ def _init_command(mcs, **kwargs):
Sets up locks to be more forgiving. This is used both by the metaclass
and (optionally) at instantiation time.
If kwargs are given, these are set as instance-specific properties on the command.
If kwargs are given, these are set as instance-specific properties
on the command.
"""
for i in range(len(kwargs)):
# used for dynamic creation of commands
key, value = kwargs.popitem()
setattr(mcs, key, value)
# used for dynamic creation of commands
key, value = kwargs.popitem()
setattr(mcs, key, value)
mcs.key = mcs.key.lower()
if mcs.aliases and not is_iter(mcs.aliases):
try:
mcs.aliases = [str(alias).strip().lower() for alias in mcs.aliases.split(',')]
mcs.aliases = [str(alias).strip().lower()
for alias in mcs.aliases.split(',')]
except Exception:
mcs.aliases = []
mcs.aliases = list(set(alias for alias in mcs.aliases if alias and alias != mcs.key))
mcs.aliases = list(set(alias for alias in mcs.aliases
if alias and alias != mcs.key))
# optimization - a set is much faster to match against than a list
mcs._matchset = set([mcs.key] + mcs.aliases)
@ -84,6 +88,7 @@ class CommandMeta(type):
# structure can parse the input string the same way, minimizing
# parsing errors.
class Command(object):
"""
Base command
@ -112,13 +117,16 @@ class Command(object):
key - identifier for command (e.g. "look")
aliases - (optional) list of aliases (e.g. ["l", "loo"])
locks - lock string (default is "cmd:all()")
help_category - how to organize this help entry in help system (default is "General")
help_category - how to organize this help entry in help system
(default is "General")
auto_help - defaults to True. Allows for turning off auto-help generation
arg_regex - (optional) raw string regex defining how the argument part of the command should look
in order to match for this command (e.g. must it be a space between cmdname and arg?)
arg_regex - (optional) raw string regex defining how the argument part of
the command should look in order to match for this command
(e.g. must it be a space between cmdname and arg?)
(Note that if auto_help is on, this initial string is also used by the system
to create the help entry for the command, so it's a good idea to format it similar to this one)
(Note that if auto_help is on, this initial string is also used by the
system to create the help entry for the command, so it's a good idea to
format it similar to this one)
"""
# Tie our metaclass, for some convenience cleanup
__metaclass__ = CommandMeta
@ -127,7 +135,8 @@ class Command(object):
key = "command"
# alternative ways to call the command (e.g. 'l', 'glance', 'examine')
aliases = []
# a list of lock definitions on the form cmd:[NOT] func(args) [ AND|OR][ NOT] func2(args)
# a list of lock definitions on the form
# cmd:[NOT] func(args) [ AND|OR][ NOT] func2(args)
locks = ""
# used by the help system to group commands in lists.
help_category = "general"
@ -136,7 +145,8 @@ class Command(object):
auto_help = True
# auto-set (by Evennia on command instantiation) are:
# obj - which object this command is defined on
# sessid - which session-id (if any) is responsible for triggering this command
# sessid - which session-id (if any) is responsible for
# triggering this command
#
def __init__(self, **kwargs):
@ -206,20 +216,22 @@ class Command(object):
"""
return self.lockhandler.check(srcobj, access_type, default=default)
def msg(self, msg="", to_obj=None, from_obj=None, sessid=None, all_sessions=False, **kwargs):
def msg(self, msg="", to_obj=None, from_obj=None,
sessid=None, all_sessions=False, **kwargs):
"""
This is a shortcut instad of calling msg() directly on an object - it will
detect if caller is an Object or a Player and also appends self.sessid
automatically.
This is a shortcut instad of calling msg() directly on an object - it
will detect if caller is an Object or a Player and also appends
self.sessid automatically.
msg - text string of message to send
to_obj - target object of message. Defaults to self.caller
from_obj - source of message. Defaults to to_obj
data - optional dictionary of data
sessid - supply data only to a unique sessid (normally not used - this is only potentially useful if
to_obj is a Player object different from self.caller or self.caller.player)
all_sessions (bool) - default is to send only to the session connected to
the target object
sessid - supply data only to a unique sessid (normally not used -
this is only potentially useful if to_obj is a Player object
different from self.caller or self.caller.player)
all_sessions (bool) - default is to send only to the session
connected to the target object
"""
from_obj = from_obj or self.caller
to_obj = to_obj or from_obj

View file

@ -4,7 +4,8 @@ Admin commands
"""
import time, re
import time
import re
from django.conf import settings
from django.contrib.auth.models import User
from src.server.sessionhandler import SESSIONS
@ -15,8 +16,8 @@ from src.commands.default.muxcommand import MuxCommand
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
# limit members for API inclusion
__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelPlayer", "CmdEmit", "CmdNewPassword",
"CmdPerm", "CmdWall")
__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelPlayer",
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall")
class CmdBoot(MuxCommand):
@ -98,6 +99,7 @@ class CmdBoot(MuxCommand):
# regex matching IP addresses with wildcards, eg. 233.122.4.*
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
def list_bans(banlist):
"""
Helper function to display a list of active bans. Input argument
@ -108,12 +110,13 @@ def list_bans(banlist):
table = prettytable.PrettyTable(["{wid", "{wname/ip", "{wdate", "{wreason"])
for inum, ban in enumerate(banlist):
table.add_row([str(inum+1),
table.add_row([str(inum + 1),
ban[0] and ban[0] or ban[1],
ban[3], ban[4]])
string = "{wActive bans:{n\n%s" % table
return string
class CmdBan(MuxCommand):
"""
ban a player from the server
@ -152,7 +155,7 @@ class CmdBan(MuxCommand):
key = "@ban"
aliases = ["@bans"]
locks = "cmd:perm(ban) or perm(Immortals)"
help_category="Admin"
help_category = "Admin"
def func(self):
"""
@ -172,14 +175,15 @@ class CmdBan(MuxCommand):
banlist = []
if not self.args or (self.switches
and not any(switch in ('ip', 'name') for switch in self.switches)):
and not any(switch in ('ip', 'name')
for switch in self.switches)):
self.caller.msg(list_bans(banlist))
return
now = time.ctime()
reason = ""
if ':' in self.args:
ban, reason = self.args.rsplit(':',1)
ban, reason = self.args.rsplit(':', 1)
else:
ban = self.args
ban = ban.lower()
@ -193,7 +197,7 @@ class CmdBan(MuxCommand):
typ = "IP"
ban = ipban[0]
# replace * with regex form and compile it
ipregex = ban.replace('.','\.')
ipregex = ban.replace('.', '\.')
ipregex = ipregex.replace('*', '[0-9]{1,3}')
#print "regex:",ipregex
ipregex = re.compile(r"%s" % ipregex)
@ -203,6 +207,7 @@ class CmdBan(MuxCommand):
ServerConfig.objects.conf('server_bans', banlist)
self.caller.msg("%s-Ban {w%s{x was added." % (typ, ban))
class CmdUnban(MuxCommand):
"""
remove a ban
@ -218,7 +223,7 @@ class CmdUnban(MuxCommand):
"""
key = "@unban"
locks = "cmd:perm(unban) or perm(Immortals)"
help_category="Admin"
help_category = "Admin"
def func(self):
"Implement unbanning"
@ -241,10 +246,11 @@ class CmdUnban(MuxCommand):
self.caller.msg("Ban id {w%s{x was not found." % self.args)
else:
# all is ok, clear ban
ban = banlist[num-1]
del banlist[num-1]
ban = banlist[num - 1]
del banlist[num - 1]
ServerConfig.objects.conf('server_bans', banlist)
self.caller.msg("Cleared ban %s: %s" % (num, " ".join([s for s in ban[:2]])))
self.caller.msg("Cleared ban %s: %s" %
(num, " ".join([s for s in ban[:2]])))
class CmdDelPlayer(MuxCommand):
@ -408,7 +414,7 @@ class CmdEmit(MuxCommand):
obj = caller.search(objname, global_search=True)
if not obj:
return
if rooms_only and not obj.location == None:
if rooms_only and not obj.location is None:
caller.msg("%s is not a room. Ignored." % objname)
continue
if players_only and not obj.has_player:
@ -425,7 +431,6 @@ class CmdEmit(MuxCommand):
caller.msg("You are not allowed to emit to %s." % objname)
class CmdNewPassword(MuxCommand):
"""
@userpassword
@ -457,7 +462,8 @@ class CmdNewPassword(MuxCommand):
player.user.save()
self.msg("%s - new password set to '%s'." % (player.name, self.rhs))
if player.character != caller:
player.msg("%s has changed your password to '%s'." % (caller.name, self.rhs))
player.msg("%s has changed your password to '%s'." % (caller.name,
self.rhs))
class CmdPerm(MuxCommand):
@ -497,7 +503,7 @@ class CmdPerm(MuxCommand):
if playermode:
obj = caller.search_player(lhs)
else:
obj = caller.search(lhs, global_search=True)
obj = caller.search(lhs, global_search=True)
if not obj:
return
@ -511,7 +517,9 @@ class CmdPerm(MuxCommand):
string += "<None>"
else:
string += ", ".join(obj.permissions.all())
if hasattr(obj, 'player') and hasattr(obj.player, 'is_superuser') and obj.player.is_superuser:
if (hasattr(obj, 'player') and
hasattr(obj.player, 'is_superuser') and
obj.player.is_superuser):
string += "\n(... but this object is currently controlled by a SUPERUSER! "
string += "All access checks are passed automatically.)"
caller.msg(string)
@ -539,9 +547,10 @@ class CmdPerm(MuxCommand):
for perm in self.rhslist:
# don't allow to set a permission higher in the hierarchy than the one the
# caller has (to prevent self-escalation)
if perm.lower() in PERMISSION_HIERARCHY and not obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm):
# don't allow to set a permission higher in the hierarchy than
# the one the caller has (to prevent self-escalation)
if (perm.lower() in PERMISSION_HIERARCHY and not
obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm)):
caller.msg("You cannot assign a permission higher than the one you have yourself.")
return

View file

@ -95,11 +95,12 @@ def format_header(caller, entry):
stacklen = len(caller.ndb.batch_stack)
header = "{w%02i/%02i{G: %s{n" % (ptr, stacklen, header)
# add extra space to the side for padding.
header = "%s%s" % (header, " "*(width - len(header)))
header = "%s%s" % (header, " " * (width - len(header)))
header = header.replace('\n', '\\n')
return header
def format_code(entry):
"""
Formats the viewing of code and errors
@ -109,6 +110,7 @@ def format_code(entry):
code += "\n{G>>>{n %s" % line
return code.strip()
def batch_cmd_exec(caller):
"""
Helper function for executing a single batch-command entry
@ -124,6 +126,7 @@ def batch_cmd_exec(caller):
return False
return True
def batch_code_exec(caller):
"""
Helper function for executing a single batch-code entry
@ -135,12 +138,13 @@ def batch_code_exec(caller):
caller.msg(format_header(caller, codedict['code']))
err = BATCHCODE.code_exec(codedict,
extra_environ={"caller":caller}, debug=debug)
extra_environ={"caller": caller}, debug=debug)
if err:
caller.msg(format_code(err))
return False
return True
def step_pointer(caller, step=1):
"""
Step in stack, returning the item located.
@ -156,7 +160,8 @@ def step_pointer(caller, step=1):
caller.msg("{RBeginning of batch file.")
if ptr + step >= nstack:
caller.msg("{REnd of batch file.")
caller.ndb.batch_stackptr = max(0, min(nstack-1, ptr + step))
caller.ndb.batch_stackptr = max(0, min(nstack - 1, ptr + step))
def show_curr(caller, showall=False):
"""
@ -186,6 +191,7 @@ def show_curr(caller, showall=False):
string += "\n{G|{n %s" % line
caller.msg(string)
def purge_processor(caller):
"""
This purges all effects running
@ -201,12 +207,13 @@ def purge_processor(caller):
# clear everything but the default cmdset.
caller.cmdset.delete(BatchSafeCmdSet)
caller.cmdset.clear()
caller.scripts.validate() # this will purge interactive mode
caller.scripts.validate() # this will purge interactive mode
#------------------------------------------------------------
# main access commands
#------------------------------------------------------------
class CmdBatchCommands(MuxCommand):
"""
Build from batch-command file
@ -275,19 +282,25 @@ class CmdBatchCommands(MuxCommand):
procpool = False
if "PythonProcPool" in utils.server_services():
if utils.uses_database("sqlite3"):
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
else:
procpool=True
procpool = True
if procpool:
# run in parallel process
def callback(r):
caller.msg(" {GBatchfile '%s' applied." % python_path)
purge_processor(caller)
def errback(e):
caller.msg(" {RError from processor: '%s'" % e)
purge_processor(caller)
utils.run_async(_PROCPOOL_BATCHCMD_SOURCE, commands=commands, caller=caller, at_return=callback, at_err=errback)
utils.run_async(_PROCPOOL_BATCHCMD_SOURCE,
commands=commands,
caller=caller,
at_return=callback,
at_err=errback)
else:
# run in-process (might block)
for inum in range(len(commands)):
@ -295,11 +308,13 @@ class CmdBatchCommands(MuxCommand):
if not batch_cmd_exec(caller):
return
step_pointer(caller, 1)
# clean out the safety cmdset and clean out all other temporary attrs.
# clean out the safety cmdset and clean out all other
# temporary attrs.
string = " Batchfile '%s' applied." % python_path
caller.msg("{G%s" % string)
purge_processor(caller)
class CmdBatchCode(MuxCommand):
"""
Build from batch-code file
@ -352,7 +367,7 @@ class CmdBatchCode(MuxCommand):
debug = False
if 'debug' in switches:
debug = True
debug = True
# Store work data in cache
caller.ndb.batch_stack = codes
@ -376,18 +391,23 @@ class CmdBatchCode(MuxCommand):
procpool = False
if "PythonProcPool" in utils.server_services():
if utils.uses_database("sqlite3"):
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
else:
procpool=True
procpool = True
if procpool:
# run in parallel process
def callback(r):
caller.msg(" {GBatchfile '%s' applied." % python_path)
purge_processor(caller)
def errback(e):
caller.msg(" {RError from processor: '%s'" % e)
purge_processor(caller)
utils.run_async(_PROCPOOL_BATCHCODE_SOURCE, codes=codes, caller=caller, at_return=callback, at_err=errback)
utils.run_async(_PROCPOOL_BATCHCODE_SOURCE,
codes=codes,
caller=caller,
at_return=callback,
at_err=errback)
else:
# un in-process (will block)
for inum in range(len(codes)):
@ -395,7 +415,8 @@ class CmdBatchCode(MuxCommand):
if not batch_code_exec(caller):
return
step_pointer(caller, 1)
# clean out the safety cmdset and clean out all other temporary attrs.
# clean out the safety cmdset and clean out all other
# temporary attrs.
string = " Batchfile '%s' applied." % python_path
caller.msg("{G%s" % string)
purge_processor(caller)
@ -423,6 +444,7 @@ class CmdStateAbort(MuxCommand):
purge_processor(self.caller)
self.caller.msg("Exited processor and reset out active cmdset back to the default one.")
class CmdStateLL(MuxCommand):
"""
ll
@ -479,6 +501,7 @@ class CmdStateRR(MuxCommand):
caller.msg(format_code("File reloaded. Staying on same command."))
show_curr(caller)
class CmdStateRRR(MuxCommand):
"""
rrr
@ -500,6 +523,7 @@ class CmdStateRRR(MuxCommand):
caller.msg(format_code("File reloaded. Restarting from top."))
show_curr(caller)
class CmdStateNN(MuxCommand):
"""
nn
@ -520,6 +544,7 @@ class CmdStateNN(MuxCommand):
step_pointer(caller, step)
show_curr(caller)
class CmdStateNL(MuxCommand):
"""
nl
@ -541,6 +566,7 @@ class CmdStateNL(MuxCommand):
step_pointer(caller, step)
show_curr(caller, showall=True)
class CmdStateBB(MuxCommand):
"""
bb
@ -562,6 +588,7 @@ class CmdStateBB(MuxCommand):
step_pointer(caller, step)
show_curr(caller)
class CmdStateBL(MuxCommand):
"""
bl
@ -583,6 +610,7 @@ class CmdStateBL(MuxCommand):
step_pointer(caller, step)
show_curr(caller, showall=True)
class CmdStateSS(MuxCommand):
"""
ss [steps]
@ -611,6 +639,7 @@ class CmdStateSS(MuxCommand):
step_pointer(caller, 1)
show_curr(caller)
class CmdStateSL(MuxCommand):
"""
sl [steps]
@ -639,6 +668,7 @@ class CmdStateSL(MuxCommand):
step_pointer(caller, 1)
show_curr(caller)
class CmdStateCC(MuxCommand):
"""
cc
@ -670,6 +700,7 @@ class CmdStateCC(MuxCommand):
del caller.ndb.batch_batchmode
caller.msg(format_code("Finished processing batch file."))
class CmdStateJJ(MuxCommand):
"""
j <command number>
@ -684,7 +715,7 @@ class CmdStateJJ(MuxCommand):
caller = self.caller
arg = self.args
if arg and arg.isdigit():
number = int(self.args)-1
number = int(self.args) - 1
else:
caller.msg(format_code("You must give a number index."))
return
@ -693,6 +724,7 @@ class CmdStateJJ(MuxCommand):
step_pointer(caller, step)
show_curr(caller)
class CmdStateJL(MuxCommand):
"""
jl <command number>
@ -707,7 +739,7 @@ class CmdStateJL(MuxCommand):
caller = self.caller
arg = self.args
if arg and arg.isdigit():
number = int(self.args)-1
number = int(self.args) - 1
else:
caller.msg(format_code("You must give a number index."))
return
@ -716,6 +748,7 @@ class CmdStateJL(MuxCommand):
step_pointer(caller, step)
show_curr(caller, showall=True)
class CmdStateQQ(MuxCommand):
"""
qq
@ -730,6 +763,7 @@ class CmdStateQQ(MuxCommand):
purge_processor(self.caller)
self.caller.msg("Aborted interactive batch mode.")
class CmdStateHH(MuxCommand):
"Help command"
@ -766,7 +800,6 @@ class CmdStateHH(MuxCommand):
self.caller.msg(string)
#------------------------------------------------------------
#
# Defining the cmdsets for the interactive batchprocessor
@ -781,12 +814,13 @@ class BatchSafeCmdSet(CmdSet):
always be available to get out of everything.
"""
key = "Batch_default"
priority = 104 # override other cmdsets.
priority = 104 # override other cmdsets.
def at_cmdset_creation(self):
"Init the cmdset"
self.add(CmdStateAbort())
class BatchInteractiveCmdSet(CmdSet):
"""
The cmdset for the interactive batch processor mode.

View file

@ -29,6 +29,7 @@ except ImportError:
# used by @find
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
class ObjManipCommand(MuxCommand):
"""
This is a parent class for some of the defining objmanip commands
@ -60,7 +61,7 @@ class ObjManipCommand(MuxCommand):
# get all the normal parsing done (switches etc)
super(ObjManipCommand, self).parse()
obj_defs = ([],[]) # stores left- and right-hand side of '='
obj_defs = ([], []) # stores left- and right-hand side of '='
obj_attrs = ([], []) # "
for iside, arglist in enumerate((self.lhslist, self.rhslist)):
@ -101,7 +102,7 @@ class CmdSetObjAlias(MuxCommand):
by everyone.
"""
key = "@alias"
key = "@alias"
aliases = "@setobjalias"
locks = "cmd:perm(setobjalias) or perm(Builders)"
help_category = "Building"
@ -121,7 +122,7 @@ class CmdSetObjAlias(MuxCommand):
obj = caller.search(objname)
if not obj:
return
if self.rhs == None:
if self.rhs is None:
# no =, so we just list aliases on object.
aliases = obj.aliases.all()
if aliases:
@ -146,15 +147,18 @@ class CmdSetObjAlias(MuxCommand):
# merge the old and new aliases (if any)
old_aliases = obj.aliases.all()
new_aliases = [alias.strip().lower() for alias in self.rhs.split(',') if alias.strip()]
new_aliases = [alias.strip().lower() for alias in self.rhs.split(',')
if alias.strip()]
# make the aliases only appear once
old_aliases.extend(new_aliases)
aliases = list(set(old_aliases))
# save back to object.
obj.aliases.add(aliases)
# we treat this as a re-caching (relevant for exits to re-build their exit commands with the correct aliases)
# we treat this as a re-caching (relevant for exits to re-build their
# exit commands with the correct aliases)
caller.msg("Alias(es) for '%s' set to %s." % (obj.key, str(obj.aliases)))
class CmdCopy(ObjManipCommand):
"""
@copy - copy objects
@ -167,8 +171,8 @@ class CmdCopy(ObjManipCommand):
removing any changes that might have been made to the original
since it was first created.
Create one or more copies of an object. If you don't supply any targets, one exact copy
of the original object will be created with the name *_copy.
Create one or more copies of an object. If you don't supply any targets,
one exact copt of the original object will be created with the name *_copy.
"""
key = "@copy"
@ -210,12 +214,15 @@ class CmdCopy(ObjManipCommand):
to_obj_aliases = objdef['aliases']
to_obj_location = objdef['option']
if to_obj_location:
to_obj_location = caller.search(to_obj_location, global_search=True)
to_obj_location = caller.search(to_obj_location,
global_search=True)
if not to_obj_location:
return
copiedobj = ObjectDB.objects.copy_object(from_obj, new_key=to_obj_name,
new_location=to_obj_location, new_aliases=to_obj_aliases)
copiedobj = ObjectDB.objects.copy_object(from_obj,
new_key=to_obj_name,
new_location=to_obj_location,
new_aliases=to_obj_aliases)
if copiedobj:
string = "Copied %s to '%s' (aliases: %s)." % (from_obj_name, to_obj_name,
to_obj_aliases)
@ -225,6 +232,7 @@ class CmdCopy(ObjManipCommand):
# we are done, echo to user
caller.msg(string)
class CmdCpAttr(ObjManipCommand):
"""
@cpattr - copy attributes
@ -244,8 +252,8 @@ class CmdCpAttr(ObjManipCommand):
copies the coolness attribute (defined on yourself), to attributes
on Anna and Tom.
Copy the attribute one object to one or more attributes on another object. If
you don't supply a source object, yourself is used.
Copy the attribute one object to one or more attributes on another object.
If you don't supply a source object, yourself is used.
"""
key = "@cpattr"
locks = "cmd:perm(cpattr) or perm(Builders)"
@ -272,7 +280,8 @@ class CmdCpAttr(ObjManipCommand):
from_obj_attrs = lhs_objattr[0]['attrs']
if not from_obj_attrs:
# this means the from_obj_name is actually an attribute name on self.
# this means the from_obj_name is actually an attribute
# name on self.
from_obj_attrs = [from_obj_name]
from_obj = self.caller
from_obj_name = self.caller.name
@ -282,7 +291,8 @@ class CmdCpAttr(ObjManipCommand):
caller.msg("You have to supply both source object and target(s).")
return
if not from_obj.attributes.has(from_obj_attrs[0]):
caller.msg("%s doesn't have an attribute %s." % (from_obj_name, from_obj_attrs[0]))
caller.msg("%s doesn't have an attribute %s." % (from_obj_name,
from_obj_attrs[0]))
return
srcvalue = from_obj.attributes.get(from_obj_attrs[0])
@ -291,7 +301,8 @@ class CmdCpAttr(ObjManipCommand):
string = "Moving "
else:
string = "Copying "
string += "%s/%s (with value %s) ..." % (from_obj_name, from_obj_attrs[0], srcvalue)
string += "%s/%s (with value %s) ..." % (from_obj_name,
from_obj_attrs[0], srcvalue)
for to_obj in to_objs:
to_obj_name = to_obj['name']
@ -308,15 +319,20 @@ class CmdCpAttr(ObjManipCommand):
# on the to_obj, we copy the original name instead.
to_attr = from_attr
to_obj.attributes.add(to_attr, srcvalue)
if "move" in self.switches and not (from_obj == to_obj and from_attr == to_attr):
if ("move" in self.switches and not (from_obj == to_obj and
from_attr == to_attr)):
from_obj.del_attribute(from_attr)
string += "\nMoved %s.%s -> %s.%s." % (from_obj.name, from_attr,
string += "\nMoved %s.%s -> %s.%s." % (from_obj.name,
from_attr,
to_obj_name, to_attr)
else:
string += "\nCopied %s.%s -> %s.%s." % (from_obj.name, from_attr,
to_obj_name, to_attr)
string += "\nCopied %s.%s -> %s.%s." % (from_obj.name,
from_attr,
to_obj_name,
to_attr)
caller.msg(string)
class CmdMvAttr(ObjManipCommand):
"""
@mvattr - move attributes
@ -330,8 +346,8 @@ class CmdMvAttr(ObjManipCommand):
Switches:
copy - Don't delete the original after moving.
Move an attribute from one object to one or more attributes on another object. If
you don't supply a source object, yourself is used.
Move an attribute from one object to one or more attributes on another
object. If you don't supply a source object, yourself is used.
"""
key = "@mvattr"
locks = "cmd:perm(mvattr) or perm(Builders)"
@ -356,6 +372,7 @@ class CmdMvAttr(ObjManipCommand):
else:
self.caller.execute_cmd("@cpattr/move %s" % self.args)
class CmdCreate(ObjManipCommand):
"""
@create - create new objects
@ -364,8 +381,9 @@ class CmdCreate(ObjManipCommand):
@create[/drop] objname[;alias;alias...][:typeclass], objname...
switch:
drop - automatically drop the new object into your current location (this is not echoed)
this also sets the new object's home to the current location rather than to you.
drop - automatically drop the new object into your current
location (this is not echoed). This also sets the new
object's home to the current location rather than to you.
Creates one or more new objects. If typeclass is given, the object
is created as a child of this typeclass. The typeclass script is
@ -406,7 +424,8 @@ class CmdCreate(ObjManipCommand):
# object typeclass will automatically be used)
lockstring = "control:id(%s);examine:perm(Builders);delete:id(%s) or perm(Wizards);get:all()" % (caller.id, caller.id)
obj = create.create_object(typeclass, name, caller,
home=caller, aliases=aliases, locks=lockstring, report_to=caller)
home=caller, aliases=aliases,
locks=lockstring, report_to=caller)
if not obj:
continue
if aliases:
@ -423,7 +442,8 @@ class CmdCreate(ObjManipCommand):
obj.home = caller.location
obj.move_to(caller.location, quiet=True)
if string:
caller.msg(string)
caller.msg(string)
class CmdDesc(MuxCommand):
"""
@ -471,8 +491,8 @@ class CmdDestroy(MuxCommand):
@destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...]
switches:
override - The @destroy command will usually avoid accidentally destroying
player objects. This switch overrides this safety.
override - The @destroy command will usually avoid accidentally
destroying player objects. This switch overrides this safety.
examples:
@destroy house, roof, door, 44-78
@destroy 5-10, flower, 45
@ -502,7 +522,8 @@ class CmdDestroy(MuxCommand):
if not obj:
self.caller.msg(" (Objects to destroy must either be local or specified with a unique #dbref.)")
return ""
if not "override" in self.switches and obj.dbid == int(settings.CHARACTER_DEFAULT_HOME.lstrip("#")):
if (not "override" in self.switches and
obj.dbid == int(settings.CHARACTER_DEFAULT_HOME.lstrip("#"))):
return "\nYou are trying to delete CHARACTER_DEFAULT_HOME. If you want to do this, use the /override switch."
objname = obj.name
if not obj.access(caller, 'delete'):
@ -529,9 +550,10 @@ class CmdDestroy(MuxCommand):
for objname in self.lhslist:
if '-' in objname:
# might be a range of dbrefs
dmin, dmax = [utils.dbref(part, reqhash=False) for part in objname.split('-', 1)]
dmin, dmax = [utils.dbref(part, reqhash=False)
for part in objname.split('-', 1)]
if dmin and dmax:
for dbref in range(int(dmin),int(dmax+1)):
for dbref in range(int(dmin), int(dmax + 1)):
string += delobj("#" + str(dbref), True)
else:
string += delobj(objname)
@ -558,9 +580,11 @@ class CmdDig(ObjManipCommand):
@dig house:myrooms.MyHouseTypeclass
@dig sheer cliff;cliff;sheer = climb up, climb down
This command is a convenient way to build rooms quickly; it creates the new room and you can optionally
set up exits back and forth between your current room and the new one. You can add as many aliases as you
like to the name of the room and the exits in question; an example would be 'north;no;n'.
This command is a convenient way to build rooms quickly; it creates the
new room and you can optionally set up exits back and forth between your
current room and the new one. You can add as many aliases as you
like to the name of the room and the exits in question; an example
would be 'north;no;n'.
"""
key = "@dig"
locks = "cmd:perm(dig) or perm(Builders)"
@ -595,13 +619,14 @@ class CmdDig(ObjManipCommand):
lockstring = lockstring % (caller.dbref, caller.dbref, caller.dbref)
new_room = create.create_object(typeclass, room["name"],
aliases=room["aliases"], report_to=caller)
aliases=room["aliases"],
report_to=caller)
new_room.locks.add(lockstring)
alias_string = ""
if new_room.aliases.all():
alias_string = " (%s)" % ", ".join(new_room.aliases.all())
room_string = "Created room %s(%s)%s of type %s." % (new_room, new_room.dbref, alias_string, typeclass)
room_string = "Created room %s(%s)%s of type %s." % (new_room,
new_room.dbref, alias_string, typeclass)
# create exit to room
@ -622,15 +647,21 @@ class CmdDig(ObjManipCommand):
if not typeclass:
typeclass = settings.BASE_EXIT_TYPECLASS
new_to_exit = create.create_object(typeclass, to_exit["name"], location,
new_to_exit = create.create_object(typeclass, to_exit["name"],
location,
aliases=to_exit["aliases"],
locks=lockstring, destination=new_room, report_to=caller)
locks=lockstring,
destination=new_room,
report_to=caller)
alias_string = ""
if new_to_exit.aliases.all():
alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all())
exit_to_string = "\nCreated Exit from %s to %s: %s(%s)%s."
exit_to_string = exit_to_string % (location.name, new_room.name, new_to_exit,
new_to_exit.dbref, alias_string)
exit_to_string = exit_to_string % (location.name,
new_room.name,
new_to_exit,
new_to_exit.dbref,
alias_string)
# Create exit back from new room
@ -647,15 +678,22 @@ class CmdDig(ObjManipCommand):
typeclass = back_exit["option"]
if not typeclass:
typeclass = settings.BASE_EXIT_TYPECLASS
new_back_exit = create.create_object(typeclass, back_exit["name"],
new_room, aliases=back_exit["aliases"],
locks=lockstring, destination=location, report_to=caller)
new_back_exit = create.create_object(typeclass,
back_exit["name"],
new_room,
aliases=back_exit["aliases"],
locks=lockstring,
destination=location,
report_to=caller)
alias_string = ""
if new_back_exit.aliases.all():
alias_string = " (%s)" % ", ".join(new_back_exit.aliases.all())
exit_back_string = "\nCreated Exit back from %s to %s: %s(%s)%s."
exit_back_string = exit_back_string % (new_room.name, location.name,
new_back_exit, new_back_exit.dbref, alias_string)
exit_back_string = exit_back_string % (new_room.name,
location.name,
new_back_exit,
new_back_exit.dbref,
alias_string)
caller.msg("%s%s%s" % (room_string, exit_to_string, exit_back_string))
if new_room and ('teleport' in self.switches or "tel" in self.switches):
caller.move_to(new_room)
@ -693,18 +731,18 @@ class CmdTunnel(MuxCommand):
help_category = "Building"
# store the direction, full name and its opposite
directions = {"n" : ("north", "s"),
directions = {"n": ("north", "s"),
"ne": ("northeast", "sw"),
"e" : ("east", "w"),
"e": ("east", "w"),
"se": ("southeast", "nw"),
"s" : ("south", "n"),
"s": ("south", "n"),
"sw": ("southwest", "ne"),
"w" : ("west", "e"),
"w": ("west", "e"),
"nw": ("northwest", "se"),
"u" : ("up", "d"),
"d" : ("down", "u"),
"i" : ("in", "o"),
"o" : ("out", "i")}
"u": ("up", "d"),
"d": ("down", "u"),
"i": ("in", "o"),
"o": ("out", "i")}
def func(self):
"Implements the tunnel command"
@ -725,7 +763,7 @@ class CmdTunnel(MuxCommand):
roomname = "Some place"
if self.rhs:
roomname = self.rhs # this may include aliases; that's fine.
roomname = self.rhs # this may include aliases; that's fine.
telswitch = ""
if "tel" in self.switches:
@ -735,9 +773,11 @@ class CmdTunnel(MuxCommand):
backstring = ", %s;%s" % (backname, backshort)
# build the string we will use to call @dig
digstring = "@dig%s %s = %s;%s%s" % (telswitch, roomname, exitname, exitshort, backstring)
digstring = "@dig%s %s = %s;%s%s" % (telswitch, roomname,
exitname, exitshort, backstring)
self.caller.execute_cmd(digstring)
class CmdLink(MuxCommand):
"""
@link - connect objects
@ -754,8 +794,9 @@ class CmdLink(MuxCommand):
If <object> is an exit, set its destination to <target>. Two-way operation
instead sets the destination to the *locations* of the respective given
arguments.
The second form (a lone =) sets the destination to None (same as the @unlink command)
and the third form (without =) just shows the currently set destination.
The second form (a lone =) sets the destination to None (same as
the @unlink command) and the third form (without =) just shows the
currently set destination.
"""
key = "@link"
@ -802,7 +843,7 @@ class CmdLink(MuxCommand):
obj.destination = target
string += "\nLink created %s -> %s (one way)." % (obj.name, target)
elif self.rhs == None:
elif self.rhs is None:
# this means that no = was given (otherwise rhs
# would have been an empty string). So we inspect
# the home/destination on object
@ -823,6 +864,7 @@ class CmdLink(MuxCommand):
# give feedback
caller.msg(string.strip())
class CmdUnLink(CmdLink):
"""
@unlink - unconnect objects
@ -857,6 +899,7 @@ class CmdUnLink(CmdLink):
# call the @link functionality
super(CmdUnLink, self).func()
class CmdSetHome(CmdLink):
"""
@home - control an object's home location
@ -893,7 +936,8 @@ class CmdSetHome(CmdLink):
if not home:
string = "This object has no home location set!"
else:
string = "%s's current home is %s(%s)." % (obj, home, home.dbref)
string = "%s's current home is %s(%s)." % (obj, home,
home.dbref)
else:
# set a home location
new_home = self.caller.search(self.rhs, global_search=True)
@ -907,6 +951,7 @@ class CmdSetHome(CmdLink):
string = "%s' home location was set to %s(%s)." % (obj, new_home, new_home.dbref)
self.caller.msg(string)
class CmdListCmdSets(MuxCommand):
"""
list command sets on an object
@ -935,6 +980,7 @@ class CmdListCmdSets(MuxCommand):
string = "%s" % obj.cmdset
caller.msg(string)
class CmdName(ObjManipCommand):
"""
cname - change the name and/or aliases of an object
@ -1006,7 +1052,8 @@ class CmdOpen(ObjManipCommand):
help_category = "Building"
# a custom member method to chug out exits and do checks
def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None):
def create_exit(self, exit_name, location, destination,
exit_aliases=None, typeclass=None):
"""
Helper function to avoid code duplication.
At this point we know destination is a valid location
@ -1047,9 +1094,11 @@ class CmdOpen(ObjManipCommand):
# exit does not exist before. Create a new one.
if not typeclass:
typeclass = settings.BASE_EXIT_TYPECLASS
exit_obj = create.create_object(typeclass, key=exit_name,
exit_obj = create.create_object(typeclass,
key=exit_name,
location=location,
aliases=exit_aliases, report_to=caller)
aliases=exit_aliases,
report_to=caller)
if exit_obj:
# storing a destination is what makes it an exit!
exit_obj.destination = destination
@ -1095,7 +1144,11 @@ class CmdOpen(ObjManipCommand):
return
# Create exit
ok = self.create_exit(exit_name, location, destination, exit_aliases, exit_typeclass)
ok = self.create_exit(exit_name,
location,
destination,
exit_aliases,
exit_typeclass)
if not ok:
# an error; the exit was not created, so we quit.
return
@ -1104,7 +1157,11 @@ class CmdOpen(ObjManipCommand):
back_exit_name = self.lhs_objs[1]['name']
back_exit_aliases = self.lhs_objs[1]['aliases']
back_exit_typeclass = self.lhs_objs[1]['option']
ok = self.create_exit(back_exit_name, destination, location, back_exit_aliases, back_exit_typeclass)
ok = self.create_exit(back_exit_name,
destination,
location,
back_exit_aliases,
back_exit_typeclass)
class CmdSetAttribute(ObjManipCommand):
@ -1126,7 +1183,8 @@ class CmdSetAttribute(ObjManipCommand):
numbers. You can however also set Python primities such as lists,
dictionaries and tuples on objects (this might be important for
the functionality of certain custom objects). This is indicated
by you starting your value with one of {c'{n, {c"{n, {c({n, {c[{n or {c{ {n.
by you starting your value with one of {c'{n, {c"{n, {c({n, {c[{n
or {c{ {n.
Note that you should leave a space after starting a dictionary ('{ ')
so as to not confuse the dictionary start with a colour code like \{g.
Remember that if you use Python primitives like this, you must
@ -1169,10 +1227,14 @@ class CmdSetAttribute(ObjManipCommand):
used for Python <=2.5. After that literal_eval is available.
"""
# simple types
try: return int(obj)
except ValueError: pass
try: return float(obj)
except ValueError: pass
try:
return int(obj)
except ValueError:
pass
try:
return float(obj)
except ValueError:
pass
# iterables
if obj.startswith('[') and obj.endswith(']'):
"A list. Traverse recursively."
@ -1182,7 +1244,8 @@ class CmdSetAttribute(ObjManipCommand):
return tuple([rec_convert(val) for val in obj[1:-1].split(',')])
if obj.startswith('{') and obj.endswith('}') and ':' in obj:
"A dict. Traverse recursively."
return dict([(rec_convert(pair.split(":",1)[0]), rec_convert(pair.split(":",1)[1]))
return dict([(rec_convert(pair.split(":", 1)[0]),
rec_convert(pair.split(":", 1)[1]))
for pair in obj[1:-1].split(',') if ":" in pair])
# if nothing matches, return as-is
return obj
@ -1198,7 +1261,8 @@ class CmdSetAttribute(ObjManipCommand):
self.caller.msg(string)
return utils.to_str(strobj)
else:
# fall back to old recursive solution (does not support nested lists/dicts)
# fall back to old recursive solution (does not support
# nested lists/dicts)
return rec_convert(strobj.strip())
def func(self):
@ -1223,17 +1287,18 @@ class CmdSetAttribute(ObjManipCommand):
string = ""
if not value:
if self.rhs == None:
if self.rhs is None:
# no = means we inspect the attribute(s)
if not attrs:
attrs = [attr.key for attr in obj.get_all_attributes()]
for attr in attrs:
if obj.attributes.has(attr):
string += "\nAttribute %s/%s = %s" % (obj.name, attr, obj.attributes.get(attr))
string += "\nAttribute %s/%s = %s" % (obj.name, attr,
obj.attributes.get(attr))
else:
string += "\n%s has no attribute '%s'." % (obj.name, attr)
# we view it without parsing markup.
self.caller.msg(string.strip(), data={"raw":True})
self.caller.msg(string.strip(), data={"raw": True})
return
else:
# deleting the attribute(s)
@ -1252,9 +1317,11 @@ class CmdSetAttribute(ObjManipCommand):
string += "\nCreated attribute %s/%s = %s" % (obj.name, attr, value)
except SyntaxError:
# this means literal_eval tried to parse a faulty string
string = "{RCritical Python syntax error in your value. Only primitive Python structures"
string += "\nare allowed. You also need to use correct Python syntax. Remember especially"
string += "\nto put quotes around all strings inside lists and dicts.{n"
string = "{RCritical Python syntax error in your value. "
string += "Only primitive Python structures are allowed. "
string += "\nYou also need to use correct Python syntax. "
string += "Remember especially to put quotes around all "
string += "strings inside lists and dicts.{n"
# send feedback
caller.msg(string.strip('\n'))
@ -1314,7 +1381,8 @@ class CmdTypeclass(MuxCommand):
# we did not supply a new typeclass, view the
# current one instead.
if hasattr(obj, "typeclass"):
string = "%s's current typeclass is '%s' (%s)." % (obj.name, obj.typeclass.typename, obj.typeclass.path)
string = "%s's current typeclass is '%s' (%s)." % (obj.name,
obj.typeclass.typename, obj.typeclass.path)
else:
string = "%s is not a typed object." % obj.name
caller.msg(string)
@ -1343,8 +1411,8 @@ class CmdTypeclass(MuxCommand):
string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.typeclass.path)
else:
string = "%s changed typeclass from %s to %s.\n" % (obj.name,
old_typeclass_path,
obj.typeclass_path)
old_typeclass_path,
obj.typeclass_path)
string += "Creation hooks were run."
if reset:
string += " All old attributes where deleted before the swap."
@ -1354,8 +1422,8 @@ class CmdTypeclass(MuxCommand):
else:
string = obj.typeclass_last_errmsg
string += "\nCould not swap '%s' (%s) to typeclass '%s'." % (obj.name,
old_typeclass_path,
typeclass)
old_typeclass_path,
typeclass)
caller.msg(string)
@ -1410,6 +1478,7 @@ class CmdWipe(ObjManipCommand):
string = string % (",".join(attrs), obj.name)
caller.msg(string)
class CmdLock(ObjManipCommand):
"""
lock - assign a lock definition to an object
@ -1493,6 +1562,7 @@ class CmdLock(ObjManipCommand):
return
caller.msg(obj.locks)
class CmdExamine(ObjManipCommand):
"""
examine - detailed info on objects
@ -1545,7 +1615,9 @@ class CmdExamine(ObjManipCommand):
else:
db_attr = [(attr.key, attr.value) for attr in obj.db_attributes.all()]
try:
ndb_attr = [(aname, avalue) for aname, avalue in obj.ndb.__dict__.items() if not aname.startswith("_")]
ndb_attr = [(aname, avalue)
for aname, avalue in obj.ndb.__dict__.items()
if not aname.startswith("_")]
except Exception:
ndb_attr = None
string = ""
@ -1572,7 +1644,8 @@ class CmdExamine(ObjManipCommand):
if hasattr(obj, "sessid") and obj.sessid:
string += "\n{wsession{n: %s" % obj.sessid
elif hasattr(obj, "sessions") and obj.sessions:
string += "\n{wsession(s){n: %s" % (", ".join(str(sess.sessid) for sess in obj.sessions))
string += "\n{wsession(s){n: %s" % (", ".join(str(sess.sessid)
for sess in obj.sessions))
if hasattr(obj, "has_player") and obj.has_player:
string += "\n{wPlayer{n: {c%s{n" % obj.player.name
perms = obj.player.permissions.all()
@ -1581,7 +1654,8 @@ class CmdExamine(ObjManipCommand):
elif not perms:
perms = ["<None>"]
string += "\n{wPlayer Perms{n: %s" % (", ".join(perms))
string += "\n{wTypeclass{n: %s (%s)" % (obj.typeclass.typename, obj.typeclass_path)
string += "\n{wTypeclass{n: %s (%s)" % (obj.typeclass.typename,
obj.typeclass_path)
if hasattr(obj, "location"):
string += "\n{wLocation{n: %s" % obj.location
if obj.location:
@ -1610,14 +1684,20 @@ class CmdExamine(ObjManipCommand):
if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "Empty"):
# list the current cmdsets
all_cmdsets = obj.cmdset.all() + (hasattr(obj, "player") and obj.player and obj.player.cmdset.all() or [])
all_cmdsets += hasattr(obj, "sessid") and hasattr(obj, "player") and obj.player.get_session(obj.sessid).cmdset.all()
all_cmdsets.sort(key=lambda x:x.priority, reverse=True)
string += "\n{wStored Cmdset(s){n:\n %s" % ("\n ".join("%s [%s] (prio %s)" %
(cmdset.path, cmdset.key, cmdset.priority) for cmdset in all_cmdsets))
all_cmdsets = (obj.cmdset.all() +
(hasattr(obj, "player") and
obj.player and obj.player.cmdset.all() or []))
all_cmdsets += (hasattr(obj, "sessid") and
hasattr(obj, "player") and
obj.player.get_session(obj.sessid).cmdset.all())
all_cmdsets.sort(key=lambda x: x.priority, reverse=True)
string += "\n{wStored Cmdset(s){n:\n %s" % ("\n ".join("%s [%s] (prio %s)" % \
(cmdset.path, cmdset.key, cmdset.priority)
for cmdset in all_cmdsets))
# list the commands available to this object
avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")])
avail_cmdset = sorted([cmd.key for cmd in avail_cmdset
if cmd.access(obj, "cmd")])
cmdsetstr = utils.fill(", ".join(avail_cmdset), indent=2)
string += "\n{wCommands available to %s (all cmdsets + exits and external cmds){n:\n %s" % (obj.key, cmdsetstr)
@ -1644,10 +1724,10 @@ class CmdExamine(ObjManipCommand):
string += "\n{wCharacters{n: %s" % ", ".join(["{c%s{n" % pobj.name for pobj in pobjs])
if things:
string += "\n{wContents{n: %s" % ", ".join([cont.name for cont in obj.contents
if cont not in exits and cont not in pobjs])
separator = "-"*78
if cont not in exits and cont not in pobjs])
separator = "-" * 78
#output info
return '%s\n%s\n%s' % ( separator, string.strip(), separator )
return '%s\n%s\n%s' % (separator, string.strip(), separator)
def func(self):
"Process command"
@ -1686,7 +1766,7 @@ class CmdExamine(ObjManipCommand):
obj_attrs = objdef['attrs']
self.player_mode = utils.inherits_from(caller, "src.players.player.Player") or \
"player" in self.switches or obj_name.startswith('*')
"player" in self.switches or obj_name.startswith('*')
if self.player_mode:
try:
obj = caller.search_player(obj_name.lstrip('*'))
@ -1699,7 +1779,8 @@ class CmdExamine(ObjManipCommand):
continue
if not obj.access(caller, 'examine'):
#If we don't have special info access, just look at the object instead.
#If we don't have special info access, just look
# at the object instead.
caller.execute_cmd('look %s' % obj_name)
continue
@ -1727,7 +1808,8 @@ class CmdFind(MuxCommand):
Searches the database for an object of a particular name or dbref.
Use *playername to search for a player. The switches allows for
limiting object matches to certain game entities. Dbrefmin and dbrefmax
limits matches to within the given dbrefs, or above/below if only one is given.
limits matches to within the given dbrefs, or above/below if only
one is given.
"""
key = "@find"
@ -1772,11 +1854,13 @@ class CmdFind(MuxCommand):
if not low <= int(result.id) <= high:
string += "\n {RNo match found for '%s' within the given dbref limits.{n" % searchstring
else:
string += "\n{g %s(%s) - %s{n" % (result.key, result.dbref, result.typeclass.path)
string += "\n{g %s(%s) - %s{n" % (result.key, result.dbref,
result.typeclass.path)
else:
# Not a player/dbref search but a wider search; build a queryset.
results = ObjectDB.objects.filter(db_key__istartswith=searchstring, id__gte=low, id__lte=high)
results = ObjectDB.objects.filter(db_key__istartswith=searchstring,
id__gte=low, id__lte=high)
if "room" in switches:
results = results.filter(db_location__isnull=True)
if "exit" in switches:
@ -1909,12 +1993,14 @@ class CmdTeleport(MuxCommand):
use_destination = False
# try the teleport
if obj_to_teleport.move_to(destination, quiet=tel_quietly, emit_to_obj=caller,
if obj_to_teleport.move_to(destination, quiet=tel_quietly,
emit_to_obj=caller,
use_destination=use_destination):
if obj_to_teleport == caller:
caller.msg("Teleported to %s." % destination)
else:
caller.msg("Teleported %s -> %s." % (obj_to_teleport, destination))
caller.msg("Teleported %s -> %s." % (obj_to_teleport,
destination))
class CmdScript(MuxCommand):
@ -1931,9 +2017,10 @@ class CmdScript(MuxCommand):
If no script path/key is given, lists all scripts active on the given
object.
Script path can be given from the base location for scripts as given in
settings. If adding a new script, it will be started automatically (no /start
switch is needed). Using the /start or /stop switches on an object without
specifying a script key/path will start/stop ALL scripts on the object.
settings. If adding a new script, it will be started automatically
(no /start switch is needed). Using the /start or /stop switches on an
object without specifying a script key/path will start/stop ALL scripts on
the object.
"""
key = "@script"
@ -1970,7 +2057,8 @@ class CmdScript(MuxCommand):
string += "%s scripts started on %s." % (num, obj.key)
elif "stop" in self.switches:
for script in scripts:
string += "Stopping script %s on %s." % (script.key, obj.key)
string += "Stopping script %s on %s." % (script.key,
obj.key)
script.stop()
string = string.strip()
obj.scripts.validate()

View file

@ -1,14 +1,15 @@
"""
This module ties together all the commands default Character objects have
available (i.e. IC commands). Note that some commands, such as communication-commands are
instead put on the player level, in the Player cmdset. Player commands remain
available also to Characters.
available (i.e. IC commands). Note that some commands, such as
communication-commands are instead put on the player level, in the
Player cmdset. Player commands remain available also to Characters.
"""
from src.commands.cmdset import CmdSet
from src.commands.default import general, help, admin, system
from src.commands.default import building
from src.commands.default import batchprocess
class CharacterCmdSet(CmdSet):
"""
Implements the default command set.

View file

@ -13,6 +13,7 @@ from src.commands.cmdset import CmdSet
from src.commands.default import help, comms, admin, system
from src.commands.default import building, player
class PlayerCmdSet(CmdSet):
"""
Implements the player command set.

View file

@ -6,6 +6,7 @@ of the state instance in this module.
from src.commands.cmdset import CmdSet
from src.commands.default import unloggedin
class UnloggedinCmdSet(CmdSet):
"""
Sets up the unlogged cmdset.

View file

@ -22,6 +22,7 @@ __all__ = ("CmdAddCom", "CmdDelCom", "CmdAllCom",
"CmdPage", "CmdIRC2Chan", "CmdIMC2Chan", "CmdIMCInfo",
"CmdIMCTell", "CmdRSS2Chan")
def find_channel(caller, channelname, silent=False, noaliases=False):
"""
Helper function for searching for a single channel with
@ -30,7 +31,8 @@ def find_channel(caller, channelname, silent=False, noaliases=False):
channels = ChannelDB.objects.channel_search(channelname)
if not channels:
if not noaliases:
channels = [chan for chan in ChannelDB.objects.get_all_channels() if channelname in chan.aliases.all()]
channels = [chan for chan in ChannelDB.objects.get_all_channels()
if channelname in chan.aliases.all()]
if channels:
return channels[0]
if not silent:
@ -43,6 +45,7 @@ def find_channel(caller, channelname, silent=False, noaliases=False):
return None
return channels[0]
class CmdAddCom(MuxPlayerCommand):
"""
addcom - subscribe to a channel with optional alias
@ -57,7 +60,7 @@ class CmdAddCom(MuxPlayerCommand):
"""
key = "addcom"
aliases = ["aliaschan","chanalias"]
aliases = ["aliaschan", "chanalias"]
help_category = "Comms"
locks = "cmd:not pperm(channel_banned)"
@ -168,6 +171,7 @@ class CmdDelCom(MuxPlayerCommand):
else:
self.msg("You had no such alias defined for this channel.")
class CmdAllCom(MuxPlayerCommand):
"""
allcom - operate on all channels
@ -197,8 +201,10 @@ class CmdAllCom(MuxPlayerCommand):
return
if args == "on":
# get names of all channels available to listen to and activate them all
channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'listen')]
# get names of all channels available to listen to
# and activate them all
channels = [chan for chan in ChannelDB.objects.get_all_channels()
if chan.access(caller, 'listen')]
for channel in channels:
caller.execute_cmd("addcom %s" % channel.key)
elif args == "off":
@ -208,13 +214,15 @@ class CmdAllCom(MuxPlayerCommand):
caller.execute_cmd("delcom %s" % channel.key)
elif args == "destroy":
# destroy all channels you control
channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'control')]
channels = [chan for chan in ChannelDB.objects.get_all_channels()
if chan.access(caller, 'control')]
for channel in channels:
caller.execute_cmd("@cdestroy %s" % channel.key)
elif args == "who":
# run a who, listing the subscribers on visible channels.
string = "\n{CChannel subscriptions{n"
channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'listen')]
channels = [chan for chan in ChannelDB.objects.get_all_channels()
if chan.access(caller, 'listen')]
if not channels:
string += "No channels."
for channel in channels:
@ -229,6 +237,7 @@ class CmdAllCom(MuxPlayerCommand):
# wrong input
self.msg("Usage: allcom on | off | who | clear")
class CmdChannels(MuxPlayerCommand):
"""
@clist
@ -253,7 +262,8 @@ class CmdChannels(MuxPlayerCommand):
caller = self.caller
# all channels we have available to listen to
channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'listen')]
channels = [chan for chan in ChannelDB.objects.get_all_channels()
if chan.access(caller, 'listen')]
#print channels
if not channels:
self.msg("No channels available.")
@ -264,28 +274,39 @@ class CmdChannels(MuxPlayerCommand):
if self.cmdstring == "comlist":
# just display the subscribed channels with no extra info
comtable = prettytable.PrettyTable(["{wchannel","{wmy aliases", "{wdescription"])
comtable = prettytable.PrettyTable(["{wchannel",
"{wmy aliases",
"{wdescription"])
for chan in subs:
clower = chan.key.lower()
nicks = caller.nicks.get(category="channel")
comtable.add_row(["%s%s" % (chan.key, chan.aliases.all() and "(%s)" % ",".join(chan.aliases.all()) or ""),
"%s".join(nick for nick in make_iter(nicks) if nick and nick.lower()==clower),
comtable.add_row(["%s%s" % (chan.key, chan.aliases.all() and
"(%s)" % ",".join(chan.aliases.all()) or ""),
"%s".join(nick for nick in make_iter(nicks)
if nick and nick.lower() == clower),
chan.db.desc])
caller.msg("\n{wChannel subscriptions{n (use {w@channels{n to list all, {waddcom{n/{wdelcom{n to sub/unsub):{n\n%s" % comtable)
else:
# full listing (of channels caller is able to listen to)
comtable = prettytable.PrettyTable(["{wsub","{wchannel","{wmy aliases","{wlocks","{wdescription"])
comtable = prettytable.PrettyTable(["{wsub",
"{wchannel",
"{wmy aliases",
"{wlocks",
"{wdescription"])
for chan in channels:
clower = chan.key.lower()
nicks = caller.nicks.get(category="channel")
nicks = nicks or []
comtable.add_row([chan in subs and "{gYes{n" or "{rNo{n",
"%s%s" % (chan.key, chan.aliases.all() and "(%s)" % ",".join(chan.aliases.all()) or ""),
"%s".join(nick for nick in make_iter(nicks) if nick.lower()==clower),
"%s%s" % (chan.key, chan.aliases.all() and
"(%s)" % ",".join(chan.aliases.all()) or ""),
"%s".join(nick for nick in make_iter(nicks)
if nick.lower() == clower),
str(chan.locks),
chan.db.desc])
caller.msg("\n{wAvailable channels{n (use {wcomlist{n,{waddcom{n and {wdelcom{n to manage subscriptions):\n%s" % comtable)
class CmdCdestroy(MuxPlayerCommand):
"""
@cdestroy
@ -322,6 +343,7 @@ class CmdCdestroy(MuxPlayerCommand):
CHANNELHANDLER.update()
self.msg("Channel '%s' was destroyed." % channel)
class CmdCBoot(MuxPlayerCommand):
"""
@cboot
@ -382,6 +404,7 @@ class CmdCBoot(MuxPlayerCommand):
channel.disconnect_from(player)
CHANNELHANDLER.update()
class CmdCemit(MuxPlayerCommand):
"""
@cemit - send a message to channel
@ -429,6 +452,7 @@ class CmdCemit(MuxPlayerCommand):
string = "Sent to channel %s: %s" % (channel.key, message)
self.msg(string)
class CmdCWho(MuxPlayerCommand):
"""
@cwho
@ -466,6 +490,7 @@ class CmdCWho(MuxPlayerCommand):
string += " <None>"
self.msg(string.strip())
class CmdChannelCreate(MuxPlayerCommand):
"""
@ccreate
@ -508,7 +533,10 @@ class CmdChannelCreate(MuxPlayerCommand):
return
# Create and set the channel up
lockstring = "send:all();listen:all();control:id(%s)" % caller.id
new_chan = create.create_channel(channame, aliases, description, locks=lockstring)
new_chan = create.create_channel(channame,
aliases,
description,
locks=lockstring)
new_chan.connect_to(caller)
self.msg("Created channel %s and connected to it." % new_chan.key)
@ -593,7 +621,9 @@ class CmdCdesc(MuxPlayerCommand):
# set the description
channel.db.desc = self.rhs
channel.save()
self.msg("Description of channel '%s' set to '%s'." % (channel.key, self.rhs))
self.msg("Description of channel '%s' set to '%s'." % (channel.key,
self.rhs))
class CmdPage(MuxPlayerCommand):
"""
@ -624,15 +654,16 @@ class CmdPage(MuxPlayerCommand):
caller = self.caller
# get the messages we've sent (not to channels)
pages_we_sent = Msg.objects.get_messages_by_sender(caller, exclude_channel_messages=True)
pages_we_sent = Msg.objects.get_messages_by_sender(caller,
exclude_channel_messages=True)
# get last messages we've got
pages_we_got = Msg.objects.get_messages_by_receiver(caller)
if 'last' in self.switches:
if pages_we_sent:
recv = ",".join(obj.key for obj in pages_we_sent[-1].receivers)
self.msg("You last paged {c%s{n:%s" % (recv, pages_we_sent[-1].message))
self.msg("You last paged {c%s{n:%s" % (recv,
pages_we_sent[-1].message))
return
else:
self.msg("You haven't paged anyone yet.")
@ -654,12 +685,12 @@ class CmdPage(MuxPlayerCommand):
lastpages = pages[-number:]
else:
lastpages = pages
lastpages = "\n ".join("{w%s{n {c%s{n to {c%s{n: %s" % (utils.datetime_format(page.date_sent),
",".join(obj.key for obj in page.senders),
"{n,{c ".join([obj.name for obj in page.receivers]),
page.message)
for page in lastpages)
template = "{w%s{n {c%s{n to {c%s{n: %s"
lastpages = "\n ".join(template %
(utils.datetime_format(page.date_sent),
",".join(obj.key for obj in page.senders),
"{n,{c ".join([obj.name for obj in page.receivers]),
page.message) for page in lastpages)
if lastpages:
string = "Your latest pages:\n %s" % lastpages
@ -668,7 +699,6 @@ class CmdPage(MuxPlayerCommand):
self.msg(string)
return
# We are sending. Build a list of targets
if not self.lhs:
@ -722,7 +752,7 @@ class CmdPage(MuxPlayerCommand):
else:
received.append("{c%s{n" % pobj.name)
if rstrings:
self.msg(rstrings = "\n".join(rstrings))
self.msg(rstrings="\n".join(rstrings))
self.msg("You paged %s with: '%s'." % (", ".join(received), message))
@ -734,18 +764,20 @@ class CmdIRC2Chan(MuxCommand):
@irc2chan[/switches] <evennia_channel> = <ircnetwork> <port> <#irchannel> <botname>
Switches:
/disconnect - this will delete the bot and remove the irc connection to the channel.
/disconnect - this will delete the bot and remove the irc connection
to the channel.
/remove - "
/list - show all irc<->evennia mappings
Example:
@irc2chan myircchan = irc.dalnet.net 6667 myevennia-channel evennia-bot
This creates an IRC bot that connects to a given IRC network and channel. It will
relay everything said in the evennia channel to the IRC channel and vice versa. The
bot will automatically connect at server start, so this comman need only be given once.
The /disconnect switch will permanently delete the bot. To only temporarily deactivate it,
use the @services command instead.
This creates an IRC bot that connects to a given IRC network and channel.
It will relay everything said in the evennia channel to the IRC channel and
vice versa. The bot will automatically connect at server start, so this
comman need only be given once. The /disconnect switch will permanently
delete the bot. To only temporarily deactivate it, use the {w@services{n
command instead.
"""
key = "@irc2chan"
@ -780,19 +812,25 @@ class CmdIRC2Chan(MuxCommand):
channel = self.lhs
self.rhs = self.rhs.replace('#', ' ') # to avoid Python comment issues
try:
irc_network, irc_port, irc_channel, irc_botname = [part.strip() for part in self.rhs.split(None, 3)]
irc_network, irc_port, irc_channel, irc_botname = \
[part.strip() for part in self.rhs.split(None, 3)]
irc_channel = "#%s" % irc_channel
except Exception:
string = "IRC bot definition '%s' is not valid." % self.rhs
self.msg(string)
return
if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches:
if('disconnect' in self.switches or 'remove' in self.switches or
'delete' in self.switches):
chanmatch = find_channel(self.caller, channel, silent=True)
if chanmatch:
channel = chanmatch.key
ok = irc.delete_connection(channel, irc_network, irc_port, irc_channel, irc_botname)
ok = irc.delete_connection(channel,
irc_network,
irc_port,
irc_channel,
irc_botname)
if not ok:
self.msg("IRC connection/bot could not be removed, does it exist?")
else:
@ -802,12 +840,17 @@ class CmdIRC2Chan(MuxCommand):
channel = find_channel(self.caller, channel)
if not channel:
return
ok = irc.create_connection(channel, irc_network, irc_port, irc_channel, irc_botname)
ok = irc.create_connection(channel,
irc_network,
irc_port,
irc_channel,
irc_botname)
if not ok:
self.msg("This IRC connection already exists.")
return
self.msg("Connection created. Starting IRC bot.")
class CmdIMC2Chan(MuxCommand):
"""
imc2chan - link an evennia channel to imc2
@ -863,9 +906,10 @@ class CmdIMC2Chan(MuxCommand):
channel = self.lhs
imc2_channel = self.rhs
if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches:
# we don't search for channels before this since we want to clear the link
# also if the channel no longer exists.
if('disconnect' in self.switches or 'remove' in self.switches or
'delete' in self.switches):
# we don't search for channels before this since we want
# to clear the link also if the channel no longer exists.
ok = imc2.delete_connection(channel, imc2_channel)
if not ok:
self.msg("IMC2 connection could not be removed, does it exist?")
@ -932,7 +976,8 @@ class CmdIMCInfo(MuxCommand):
IMC2_CLIENT.send_packet(pck.IMC2PacketIceRefresh())
self.msg("IMC2 lists were re-synced.")
elif "games" in self.switches or "muds" in self.switches or self.cmdstring == "@imclist":
elif("games" in self.switches or "muds" in self.switches
or self.cmdstring == "@imclist"):
# list muds
from src.comms.imc2 import IMC2_MUDLIST
@ -956,9 +1001,13 @@ class CmdIMCInfo(MuxCommand):
return
from src.comms.imc2 import IMC2_CLIENT
self.msg("Sending IMC whois request. If you receive no response, no matches were found.")
IMC2_CLIENT.msg_imc2(None, from_obj=self.caller, packet_type="imcwhois", target=self.args)
IMC2_CLIENT.msg_imc2(None,
from_obj=self.caller,
packet_type="imcwhois",
target=self.args)
elif not self.switches or "channels" in self.switches or self.cmdstring == "@imcchanlist":
elif(not self.switches or "channels" in self.switches or
self.cmdstring == "@imcchanlist"):
# show channels
from src.comms.imc2 import IMC2_CHANLIST, IMC2_CLIENT
@ -968,7 +1017,8 @@ class CmdIMCInfo(MuxCommand):
table = prettytable.PrettyTable(["Full name", "Name", "Owner", "Perm", "Policy"])
for chan in channels:
nchans += 1
table.add_row([chan.name, chan.localname, chan.owner, chan.level, chan.policy])
table.add_row([chan.name, chan.localname, chan.owner,
chan.level, chan.policy])
string += "\n{wChannels on %s:{n\n%s" % (IMC2_CLIENT.factory.network, table)
string += "\n%i Channels found." % nchans
self.msg(string)
@ -977,6 +1027,7 @@ class CmdIMCInfo(MuxCommand):
string = "Usage: imcinfo|imcchanlist|imclist"
self.msg(string)
# unclear if this is working ...
class CmdIMCTell(MuxCommand):
"""
@ -1028,19 +1079,21 @@ class CmdRSS2Chan(MuxCommand):
@rss2chan[/switches] <evennia_channel> = <rss_url>
Switches:
/disconnect - this will stop the feed and remove the connection to the channel.
/disconnect - this will stop the feed and remove the connection to the
channel.
/remove - "
/list - show all rss->evennia mappings
Example:
@rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic
This creates an RSS reader that connects to a given RSS feed url. Updates will be
echoed as a title and news link to the given channel. The rate of updating is set
with the RSS_UPDATE_INTERVAL variable in settings (default is every 10 minutes).
This creates an RSS reader that connects to a given RSS feed url. Updates
will be echoed as a title and news link to the given channel. The rate of
updating is set with the RSS_UPDATE_INTERVAL variable in settings (default
is every 10 minutes).
When disconnecting you need to supply both the channel and url again so as to identify
the connection uniquely.
When disconnecting you need to supply both the channel and url again so as
to identify the connection uniquely.
"""
key = "@rss2chan"
@ -1075,7 +1128,8 @@ class CmdRSS2Chan(MuxCommand):
channel = self.lhs
url = self.rhs
if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches:
if('disconnect' in self.switches or 'remove' in self.switches or
'delete' in self.switches):
chanmatch = find_channel(self.caller, channel, silent=True)
if chanmatch:
channel = chanmatch.key

View file

@ -13,6 +13,7 @@ __all__ = ("CmdHome", "CmdLook", "CmdNick",
AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
class CmdHome(MuxCommand):
"""
home
@ -38,6 +39,7 @@ class CmdHome(MuxCommand):
caller.move_to(home)
caller.msg("There's no place like home ...")
class CmdLook(MuxCommand):
"""
look
@ -126,7 +128,9 @@ class CmdNick(MuxCommand):
nicks = caller.nicks.get(category="channel")
if 'list' in switches:
table = prettytable.PrettyTable(["{wNickType", "{wNickname", "{wTranslates-to"])
table = prettytable.PrettyTable(["{wNickType",
"{wNickname",
"{wTranslates-to"])
for nick in nicks:
table.add_row([nick.db_category, nick.db_key, nick.db_data])
string = "{wDefined Nicks:{n\n%s" % table
@ -170,6 +174,7 @@ class CmdNick(MuxCommand):
caller.nicks.add(nick, real, category=switch)
caller.msg(string)
class CmdInventory(MuxCommand):
"""
inventory
@ -198,6 +203,7 @@ class CmdInventory(MuxCommand):
string = "{wYou are carrying:\n%s" % table
self.caller.msg(string)
class CmdGet(MuxCommand):
"""
get
@ -327,7 +333,6 @@ class CmdGive(MuxCommand):
target.msg("%s gives you %s." % (caller.key, to_give.key))
class CmdSay(MuxCommand):
"""
say
@ -365,6 +370,7 @@ class CmdSay(MuxCommand):
caller.location.msg_contents(emit_string,
exclude=caller)
class CmdPose(MuxCommand):
"""
pose - strike a pose
@ -407,6 +413,7 @@ class CmdPose(MuxCommand):
msg = "%s%s" % (self.caller.name, self.args)
self.caller.location.msg_contents(msg)
class CmdAccess(MuxCommand):
"""
access - show access groups
@ -440,5 +447,4 @@ class CmdAccess(MuxCommand):
string += "\nCharacter {c%s{n: %s" % (caller.key, cperms)
if hasattr(caller, 'player'):
string += "\nPlayer {c%s{n: %s" % (caller.player.key, pperms)
caller.msg(string)
caller.msg(string)

View file

@ -18,7 +18,8 @@ from src.commands.default.muxcommand import MuxCommand
__all__ = ("CmdHelp", "CmdSetHelp")
SEP = "{C" + "-"*78 + "{n"
SEP = "{C" + "-" * 78 + "{n"
def format_help_entry(title, help_text, aliases=None, suggested=None):
"""
@ -38,6 +39,7 @@ def format_help_entry(title, help_text, aliases=None, suggested=None):
string += "\n" + SEP
return string
def format_help_list(hdict_cmds, hdict_db):
"""
Output a category-ordered list. The input are the
@ -57,6 +59,7 @@ def format_help_list(hdict_cmds, hdict_db):
string += "{G" + fill(", ".join(sorted([str(topic) for topic in hdict_db[category]]))) + "{n"
return string
class CmdHelp(Command):
"""
The main help command
@ -129,13 +132,18 @@ class CmdHelp(Command):
# try an exact command auto-help match
match = [cmd for cmd in all_cmds if cmd == query]
if len(match) == 1:
self.msg(format_help_entry(match[0].key, match[0].__doc__, aliases=match[0].aliases, suggested=suggestions))
self.msg(format_help_entry(match[0].key,
match[0].__doc__,
aliases=match[0].aliases,
suggested=suggestions))
return
# try an exact database help entry match
match = list(HelpEntry.objects.find_topicmatch(query, exact=True))
if len(match) == 1:
self.msg(format_help_entry(match[0].key, match[0].entrytext, suggested=suggestions))
self.msg(format_help_entry(match[0].key,
match[0].entrytext,
suggested=suggestions))
return
# try to see if a category name was entered
@ -147,6 +155,7 @@ class CmdHelp(Command):
# no exact matches found. Just give suggestions.
self.msg(format_help_entry("", "No help entry found for '%s'" % query, None, suggested=suggestions))
class CmdSetHelp(MuxCommand):
"""
@help - edit the help database
@ -169,9 +178,9 @@ class CmdSetHelp(MuxCommand):
@sethelp/append pickpocketing, ,attr(is_thief) = This steals ...
This command manipulates the help database. A help entry can be created,
appended/merged to and deleted. If you don't assign a category, the "General"
category will be used. If no lockstring is specified, default is to let everyone read
the help file.
appended/merged to and deleted. If you don't assign a category, the
"General" category will be used. If no lockstring is specified, default
is to let everyone read the help file.
"""
key = "@help"

View file

@ -74,8 +74,8 @@ class MuxCommand(Command):
The 'name[ with several words]' part is already dealt with by the
cmdhandler at this point, and stored in self.cmdname (we don't use
it here). The rest of the command is stored in self.args, which can start
with the switch indicator /.
it here). The rest of the command is stored in self.args, which can
start with the switch indicator /.
This parser breaks self.args into its constituents and stores them in the
following variables:

View file

@ -24,8 +24,9 @@ from src.utils import utils, create, search, prettytable
from settings import MAX_NR_CHARACTERS, MULTISESSION_MODE
# limit symbol import for API
__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit", "CmdCharCreate",
"CmdEncoding", "CmdSessions", "CmdWho", "CmdColorTest", "CmdQuell")
__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit",
"CmdCharCreate", "CmdEncoding", "CmdSessions", "CmdWho",
"CmdColorTest", "CmdQuell")
# force max nr chars to 1 if mode is 0 or 1
MAX_NR_CHARACTERS = MULTISESSION_MODE < 2 and 1 or MAX_NR_CHARACTERS
@ -58,7 +59,8 @@ class CmdOOCLook(MuxPlayerCommand):
"Hook method for when an argument is given."
player = self.caller
key = self.args.lower()
chars = dict((utils.to_str(char.key.lower()), char) for char in player.db._playable_characters)
chars = dict((utils.to_str(char.key.lower()), char)
for char in player.db._playable_characters)
looktarget = chars.get(key)
if looktarget:
self.msg(looktarget.return_appearance(player))
@ -101,7 +103,7 @@ class CmdOOCLook(MuxPlayerCommand):
string += "\n\nAvailable character%s (%i/unlimited):" % (string_s_ending, len(characters))
else:
string += "\n\nAvailable character%s%s:" % (string_s_ending,
MAX_NR_CHARACTERS > 1 and " (%i/%i)" % (len(characters), MAX_NR_CHARACTERS) or "")
MAX_NR_CHARACTERS > 1 and " (%i/%i)" % (len(characters), MAX_NR_CHARACTERS) or "")
for char in characters:
csessid = char.sessid
@ -171,8 +173,10 @@ class CmdCharCreate(MuxPlayerCommand):
typeclass = settings.BASE_CHARACTER_TYPECLASS
permissions = settings.PERMISSION_PLAYER_DEFAULT
new_character = create.create_object(typeclass, key=key, location=default_home,
home=default_home, permissions=permissions)
new_character = create.create_object(typeclass, key=key,
location=default_home,
home=default_home,
permissions=permissions)
# only allow creator (and immortals) to puppet this char
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
(new_character.id, player.id))
@ -203,7 +207,8 @@ class CmdIC(MuxPlayerCommand):
"""
key = "@ic"
locks = "cmd:all()" # must be all() or different puppeted objects won't be able to access it.
# lockmust be all() for different puppeted objects to access it.
locks = "cmd:all()"
aliases = "@puppet"
help_category = "General"
@ -235,7 +240,8 @@ class CmdIC(MuxPlayerCommand):
if new_character.player:
# may not puppet an already puppeted character
if new_character.sessid and new_character.player == player:
# as a safeguard we allow "taking over chars from your own sessions.
# as a safeguard we allow "taking over chars from
# your own sessions.
player.msg("{c%s{n{R is now acted from another of your sessions.{n" % (new_character.name), sessid=new_character.sessid)
player.unpuppet_object(new_character.sessid)
self.msg("Taking over {c%s{n from another of your sessions." % new_character.name)
@ -251,6 +257,7 @@ class CmdIC(MuxPlayerCommand):
else:
self.msg("{rYou cannot become {C%s{n." % new_character.name)
class CmdOOC(MuxPlayerCommand):
"""
go ooc
@ -264,7 +271,8 @@ class CmdOOC(MuxPlayerCommand):
"""
key = "@ooc"
locks = "cmd:all()" # this must be all(), or different puppeted objects won't be able to access it.
# lock must be all(), for different puppeted objects to access it.
locks = "cmd:all()"
aliases = "@unpuppet"
help_category = "General"
@ -311,17 +319,22 @@ class CmdSessions(MuxPlayerCommand):
player = self.caller
sessions = player.get_all_sessions()
table = prettytable.PrettyTable(["{wsessid", "{wprotocol", "{whost", "{wpuppet/character", "{wlocation"])
for sess in sorted(sessions, key=lambda x:x.sessid):
table = prettytable.PrettyTable(["{wsessid",
"{wprotocol",
"{whost",
"{wpuppet/character",
"{wlocation"])
for sess in sorted(sessions, key=lambda x: x.sessid):
sessid = sess.sessid
char = player.get_puppet(sessid)
table.add_row([str(sessid), str(sess.protocol_key),
type(sess.address)==tuple and sess.address[0] or sess.address,
type(sess.address) == tuple and sess.address[0] or sess.address,
char and str(char) or "None",
char and str(char.location) or "N/A"])
string = "{wYour current session(s):{n\n%s" % table
self.msg(string)
class CmdWho(MuxPlayerCommand):
"""
who
@ -355,7 +368,13 @@ class CmdWho(MuxPlayerCommand):
nplayers = (SESSIONS.player_count())
if show_session_data:
table = prettytable.PrettyTable(["{wPlayer Name","{wOn for", "{wIdle", "{wRoom", "{wCmds", "{wProtocol", "{wHost"])
table = prettytable.PrettyTable(["{wPlayer Name",
"{wOn for",
"{wIdle",
"{wRoom",
"{wCmds",
"{wProtocol",
"{wHost"])
for session in session_list:
if not session.logged_in: continue
delta_cmd = time.time() - session.cmd_last_visible
@ -372,7 +391,8 @@ class CmdWho(MuxPlayerCommand):
else:
table = prettytable.PrettyTable(["{wPlayer name", "{wOn for", "{wIdle"])
for session in session_list:
if not session.logged_in: continue
if not session.logged_in:
continue
delta_cmd = time.time() - session.cmd_last_visible
delta_conn = time.time() - session.conn_time
plr_pobject = session.get_puppet()
@ -397,14 +417,16 @@ class CmdEncoding(MuxPlayerCommand):
clear - clear your custom encoding
This sets the text encoding for communicating with Evennia. This is mostly an issue only if
you want to use non-ASCII characters (i.e. letters/symbols not found in English). If you see
that your characters look strange (or you get encoding errors), you should use this command
to set the server encoding to be the same used in your client program.
This sets the text encoding for communicating with Evennia. This is mostly
an issue only if you want to use non-ASCII characters (i.e. letters/symbols
not found in English). If you see that your characters look strange (or you
get encoding errors), you should use this command to set the server
encoding to be the same used in your client program.
Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc.
If you don't submit an encoding, the current encoding will be displayed instead.
If you don't submit an encoding, the current encoding will be displayed
instead.
"""
key = "@encoding"
@ -444,6 +466,7 @@ class CmdEncoding(MuxPlayerCommand):
string = "Your custom text encoding was changed from '%s' to '%s'." % (old_encoding, encoding)
self.msg(string.strip())
class CmdPassword(MuxPlayerCommand):
"""
@password - set your password
@ -463,8 +486,8 @@ class CmdPassword(MuxPlayerCommand):
if not self.rhs:
self.msg("Usage: @password <oldpass> = <newpass>")
return
oldpass = self.lhslist[0] # this is already stripped by parse()
newpass = self.rhslist[0] # ''
oldpass = self.lhslist[0] # this is already stripped by parse()
newpass = self.rhslist[0] # ''
if not player.check_password(oldpass):
self.msg("The specified old password isn't correct.")
elif len(newpass) < 3:
@ -474,6 +497,7 @@ class CmdPassword(MuxPlayerCommand):
player.save()
self.msg("Password changed.")
class CmdQuit(MuxPlayerCommand):
"""
quit
@ -518,10 +542,10 @@ class CmdColorTest(MuxPlayerCommand):
Usage:
@color ansi|xterm256
Print a color map along with in-mud color codes, while testing what is supported in your client.
Choices are 16-color ansi (supported in most muds) or the 256-color xterm256 standard.
No checking is done to determine your client supports color - if not you will
see rubbish appear.
Print a color map along with in-mud color codes, while testing what is
supported in your client. Choices are 16-color ansi (supported in most
muds) or the 256-color xterm256 standard. No checking is done to determine
your client supports color - if not you will see rubbish appear.
"""
key = "@color"
locks = "cmd:all()"
@ -552,10 +576,10 @@ class CmdColorTest(MuxPlayerCommand):
ap = ansi.ANSI_PARSER
# ansi colors
# show all ansi color-related codes
col1 = ["%s%s{n" % (code, code.replace("{","{{")) for code, _ in ap.ext_ansi_map[:-1]]
col1 = ["%s%s{n" % (code, code.replace("{", "{{")) for code, _ in ap.ext_ansi_map[:-1]]
hi = "%ch"
col2 = ["%s%s{n" % (code, code.replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]]
col3 = ["%s%s{n" % (hi+code, (hi+code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]]
col3 = ["%s%s{n" % (hi + code, (hi + code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]]
table = utils.format_table([col1, col2, col3], extra_space=1)
string = "ANSI colors:"
for row in table:
@ -566,16 +590,16 @@ class CmdColorTest(MuxPlayerCommand):
elif self.args.startswith("x"):
# show xterm256 table
table = [[],[],[],[],[],[],[],[],[],[],[],[]]
table = [[], [], [], [], [], [], [], [], [], [], [], []]
for ir in range(6):
for ig in range(6):
for ib in range(6):
# foreground table
table[ir].append("%%c%i%i%i%s{n" % (ir,ig,ib, "{{%i%i%i" % (ir,ig,ib)))
table[ir].append("%%c%i%i%i%s{n" % (ir, ig, ib, "{{%i%i%i" % (ir, ig, ib)))
# background table
table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir,ig,ib,
5-ir,5-ig,5-ib,
"{{b%i%i%i" % (ir,ig,ib)))
table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir, ig, ib,
5 - ir, 5 - ig, 5 - ib,
"{{b%i%i%i" % (ir, ig, ib)))
table = self.table_format(table)
string = "Xterm256 colors (if not all hues show, your client might not report that it can handle xterm256):"
for row in table:
@ -586,6 +610,7 @@ class CmdColorTest(MuxPlayerCommand):
# malformed input
self.msg("Usage: @color ansi|xterm256")
class CmdQuell(MuxPlayerCommand):
"""
Quelling permissions
@ -604,7 +629,7 @@ class CmdQuell(MuxPlayerCommand):
"""
key = "@quell"
aliases =["@unquell"]
aliases = ["@unquell"]
locks = "cmd:all()"
help_category = "General"
@ -613,8 +638,9 @@ class CmdQuell(MuxPlayerCommand):
if self.sessid:
char = player.get_puppet(self.sessid)
if char:
# we are already puppeting an object. We need to reset the lock caches
# (otherwise the superuser status change won't be visible until repuppet)
# we are already puppeting an object. We need to reset
# the lock caches (otherwise the superuser status change
# won't be visible until repuppet)
char.locks.reset()
player.locks.reset()

View file

@ -33,6 +33,7 @@ from src.commands.default.muxcommand import MuxCommand
# Command called when there is no input at line
# (i.e. an lone return key)
class SystemNoInput(MuxCommand):
"""
This is called when there is no input given
@ -44,11 +45,11 @@ class SystemNoInput(MuxCommand):
"Do nothing."
pass
#
# Command called when there was no match to the
# command name
#
class SystemNoMatch(MuxCommand):
"""
No command was found matching the given input.
@ -62,6 +63,7 @@ class SystemNoMatch(MuxCommand):
"""
self.caller.msg("Huh?")
#
# Command called when there were mulitple matches to the command.
#
@ -100,7 +102,7 @@ class SystemMultimatch(MuxCommand):
is_channel = ""
is_exit = hasattr(cmd, "is_exit") and cmd.is_exit
if is_exit and cmd.destination:
is_exit = " (exit to %s)" % cmd.destination
is_exit = " (exit to %s)" % cmd.destination
else:
is_exit = ""
@ -124,6 +126,7 @@ class SystemMultimatch(MuxCommand):
string = self.format_multimatches(self.caller, self.matches)
self.caller.msg(string)
# Command called when the command given at the command line
# was identified as a channel name, like there existing a
# channel named 'ooc' and the user wrote
@ -167,4 +170,4 @@ class SystemSendToChannel(MuxCommand):
return
msg = "[%s] %s: %s" % (channel.key, caller.name, msg)
msgobj = create.create_message(caller, msg, channels=[channel])
channel.msg(msgobj)
channel.msg(msgobj)

View file

@ -5,10 +5,13 @@ System commands
"""
import traceback
import os, datetime, time
from time import time as timemeasure
import os
import datetime
import time
import sys
import django, twisted
import django
import twisted
from time import time as timemeasure
from django.conf import settings
from src.server.caches import get_cache_sizes
@ -30,6 +33,7 @@ __all__ = ("CmdReload", "CmdReset", "CmdShutdown", "CmdPy",
"CmdScripts", "CmdObjects", "CmdService", "CmdAbout",
"CmdTime", "CmdServerLoad")
class CmdReload(MuxCommand):
"""
Reload the system
@ -55,6 +59,7 @@ class CmdReload(MuxCommand):
SESSIONS.announce_all(" Server restarting %s..." % reason)
SESSIONS.server.shutdown(mode='reload')
class CmdReset(MuxCommand):
"""
Reset and reboot the system
@ -110,6 +115,7 @@ class CmdShutdown(MuxCommand):
SESSIONS.portal_shutdown()
SESSIONS.server.shutdown(mode='shutdown')
class CmdPy(MuxCommand):
"""
Execute a snippet of python code
@ -157,18 +163,17 @@ class CmdPy(MuxCommand):
# import useful variables
import ev
available_vars = {'self':caller,
'me':caller,
'here':hasattr(caller, "location") and caller.location or None,
'ev':ev,
'inherits_from':utils.inherits_from}
available_vars = {'self': caller,
'me': caller,
'here': hasattr(caller, "location") and caller.location or None,
'ev': ev,
'inherits_from': utils.inherits_from}
try:
self.msg(">>> %s" % pycode, raw=True, sessid=self.sessid)
except TypeError:
self.msg(">>> %s" % pycode, raw=True)
mode = "eval"
try:
try:
@ -195,7 +200,7 @@ class CmdPy(MuxCommand):
errlist = errlist[4:]
ret = "\n".join("{n<<< %s" % line for line in errlist if line)
if ret != None:
if ret is not None:
try:
self.msg(ret, sessid=self.sessid)
except TypeError:
@ -210,7 +215,16 @@ def format_script_list(scripts):
if not scripts:
return "<No scripts>"
table = prettytable.PrettyTable(["{wid","{wobj","{wkey","{wintval","{wnext","{wrept","{wdb"," {wtypeclass","{wdesc"],align='r')
table = prettytable.PrettyTable(["{wid",
"{wobj",
"{wkey",
"{wintval",
"{wnext",
"{wrept",
"{wdb",
"{wtypeclass",
"{wdesc"],
align='r')
table.align = 'r'
for script in scripts:
nextrep = script.time_until_next_repeat()
@ -322,7 +336,6 @@ class CmdScripts(MuxCommand):
caller.msg(string)
class CmdObjects(MuxCommand):
"""
@objects - Give a summary of object types in database
@ -357,32 +370,37 @@ class CmdObjects(MuxCommand):
nother = nobjs - nchars - nrooms - nexits
# total object sum table
totaltable = prettytable.PrettyTable(["{wtype","{wcomment","{wcount", "{w%%"])
totaltable = prettytable.PrettyTable(["{wtype", "{wcomment", "{wcount", "{w%%"])
totaltable.align = 'l'
totaltable.add_row(["Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars)/nobjs)*100)])
totaltable.add_row(["Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms)/nobjs)*100)])
totaltable.add_row(["Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits)/nobjs)*100)])
totaltable.add_row(["Other", "", nother, "%.2f" % ((float(nother)/nobjs)*100)])
totaltable.add_row(["Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars) / nobjs) * 100)])
totaltable.add_row(["Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100)])
totaltable.add_row(["Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits) / nobjs) * 100)])
totaltable.add_row(["Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100)])
# typeclass table
typetable = prettytable.PrettyTable(["{wtypeclass","{wcount", "{w%%"])
typetable = prettytable.PrettyTable(["{wtypeclass", "{wcount", "{w%%"])
typetable.align = 'l'
dbtotals = ObjectDB.objects.object_totals()
for path, count in dbtotals.items():
typetable.add_row([path, count, "%.2f" % ((float(count)/nobjs)*100)])
typetable.add_row([path, count, "%.2f" % ((float(count) / nobjs) * 100)])
# last N table
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):]
latesttable = prettytable.PrettyTable(["{wcreated","{wdbref","{wname","{wtypeclass"])
latesttable = prettytable.PrettyTable(["{wcreated",
"{wdbref",
"{wname",
"{wtypeclass"])
latesttable.align = 'l'
for obj in objs:
latesttable.add_row([utils.datetime_format(obj.date_created), obj.dbref, obj.key, obj.typeclass.path])
latesttable.add_row([utils.datetime_format(obj.date_created),
obj.dbref, obj.key, obj.typeclass.path])
string = "\n{wObject subtype totals (out of %i Objects):{n\n%s" % (nobjs, totaltable)
string += "\n{wObject typeclass distribution:{n\n%s" % typetable
string += "\n{wLast %s Objects created:{n\n%s" % (min(nobjs, nlim), latesttable)
caller.msg(string)
class CmdPlayers(MuxCommand):
"""
@players - give a summary of all registed Players
@ -397,6 +415,7 @@ class CmdPlayers(MuxCommand):
key = "@players"
aliases = ["@listplayers"]
locks = "cmd:perm(listplayers) or perm(Wizards)"
def func(self):
"List the players"
@ -413,10 +432,10 @@ class CmdPlayers(MuxCommand):
typetable = prettytable.PrettyTable(["{wtypeclass", "{wcount", "{w%%"])
typetable.align = 'l'
for path, count in dbtotals.items():
typetable.add_row([path, count, "%.2f" % ((float(count)/nplayers)*100)])
typetable.add_row([path, count, "%.2f" % ((float(count) / nplayers) * 100)])
# last N table
plyrs = PlayerDB.objects.all().order_by("db_date_created")[max(0, nplayers - nlim):]
latesttable = prettytable.PrettyTable(["{wcreated", "{wdbref","{wname","{wtypeclass"])
latesttable = prettytable.PrettyTable(["{wcreated", "{wdbref", "{wname", "{wtypeclass"])
latesttable.align = 'l'
for ply in plyrs:
latesttable.add_row([utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.typeclass.path])
@ -425,6 +444,7 @@ class CmdPlayers(MuxCommand):
string += "\n{wLast %s Players created:{n\n%s" % (min(nplayers, nlim), latesttable)
caller.msg(string)
class CmdService(MuxCommand):
"""
@service - manage services
@ -441,7 +461,8 @@ class CmdService(MuxCommand):
Service management system. Allows for the listing,
starting, and stopping of services. If no switches
are given, services will be listed. Note that to operate on the
service you have to supply the full (green or red) name as given in the list.
service you have to supply the full (green or red) name as given
in the list.
"""
key = "@service"
@ -520,6 +541,7 @@ class CmdService(MuxCommand):
caller.msg("Starting service '%s'." % self.args)
service.startService()
class CmdAbout(MuxCommand):
"""
@about - game engine info
@ -567,6 +589,7 @@ class CmdAbout(MuxCommand):
sversion)
self.caller.msg(string)
class CmdTime(MuxCommand):
"""
@time
@ -591,6 +614,7 @@ class CmdTime(MuxCommand):
table.add_row(["Server time stamp", datetime.datetime.now()])
self.caller.msg(str(table))
class CmdServerLoad(MuxCommand):
"""
server load and memory statistics
@ -648,20 +672,20 @@ class CmdServerLoad(MuxCommand):
pid = os.getpid()
rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1024.0 # resident memory
vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1024.0 # virtual memory
pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # percent of resident memory to total
pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # percent of resident memory to total
rusage = resource.getrusage(resource.RUSAGE_SELF)
# load table
loadtable = prettytable.PrettyTable(["property", "statistic"])
loadtable.align = 'l'
loadtable.add_row(["Server load (1 min)","%g" % loadavg[0]])
loadtable.add_row(["Process ID","%g" % pid]),
loadtable.add_row(["Bytes per page","%g " % psize])
loadtable.add_row(["Server load (1 min)", "%g" % loadavg[0]])
loadtable.add_row(["Process ID", "%g" % pid]),
loadtable.add_row(["Bytes per page", "%g " % psize])
loadtable.add_row(["CPU time used (total)", "%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime)])
loadtable.add_row(["CPU time used (user)", "%s (%gs)" % (utils.time_format(rusage.ru_stime), rusage.ru_stime)])
loadtable.add_row(["Memory usage","%g MB (%g%%)" % (rmem, pmem)])
loadtable.add_row(["Virtual address space\n {x(resident+swap+caching){n", "%g MB" % vmem])
loadtable.add_row(["Page faults","%g hard, %g soft, %g swapouts" % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap)])
loadtable.add_row(["Page faults", "%g hard, %g soft, %g swapouts" % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap)])
loadtable.add_row(["Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock)])
loadtable.add_row(["Network I/O", "%g in, %g out" % (rusage.ru_msgrcv, rusage.ru_msgsnd)])
loadtable.add_row(["Context switching", "%g vol, %g forced, %g signals" % (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals)])
@ -669,17 +693,24 @@ class CmdServerLoad(MuxCommand):
string = "{wServer CPU and Memory load:{n\n%s" % loadtable
if not is_pypy:
# Cache size measurements are not available on PyPy because it lacks sys.getsizeof
# Cache size measurements are not available on PyPy
# because it lacks sys.getsizeof
# object cache size
cachedict = _idmapper.cache_size()
totcache = cachedict["_total"]
sorted_cache = sorted([(key, tup[0], tup[1]) for key, tup in cachedict.items() if key !="_total" and tup[0] > 0],
key=lambda tup: tup[2], reverse=True)
memtable = prettytable.PrettyTable(["entity name", "number", "cache (MB)", "idmapper %%"])
memtable = prettytable.PrettyTable(["entity name",
"number",
"cache (MB)",
"idmapper %%"])
memtable.align = 'l'
for tup in sorted_cache:
memtable.add_row([tup[0], "%i" % tup [1], "%5.2f" % tup[2], "%.2f" % (float(tup[2]/totcache[1])*100)])
memtable.add_row([tup[0],
"%i" % tup[1],
"%5.2f" % tup[2],
"%.2f" % (float(tup[2] / totcache[1]) * 100)])
# get sizes of other caches
attr_cache_info, field_cache_info, prop_cache_info = get_cache_sizes()

View file

@ -18,7 +18,7 @@ from django.utils.unittest import TestCase
from src.server.serversession import ServerSession
from src.objects.objects import Object, Character
from src.players.player import Player
from src.utils import create, utils, ansi
from src.utils import create, ansi
from src.server.sessionhandler import SESSIONS
from django.db.models.signals import pre_save
@ -33,16 +33,20 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--*|\|", re.MULTILINE)
# Command testing
# ------------------------------------------------------------
def dummy(self, *args, **kwargs):
pass
SESSIONS.data_out = dummy
SESSIONS.disconnect = dummy
class TestObjectClass(Object):
def msg(self, text="", **kwargs):
"test message"
pass
class TestCharacterClass(Character):
def msg(self, text="", **kwargs):
"test message"
@ -52,17 +56,21 @@ class TestCharacterClass(Character):
if not self.ndb.stored_msg:
self.ndb.stored_msg = []
self.ndb.stored_msg.append(text)
class TestPlayerClass(Player):
def msg(self, text="", **kwargs):
"test message"
if not self.ndb.stored_msg:
self.ndb.stored_msg = []
self.ndb.stored_msg.append(text)
def _get_superuser(self):
"test with superuser flag"
return self.ndb.is_superuser
is_superuser = property(_get_superuser)
class CommandTest(TestCase):
"""
Tests a command
@ -93,6 +101,7 @@ class CommandTest(TestCase):
SESSIONS.portal_connect(session.get_sync_data())
SESSIONS.login(SESSIONS.session_from_sessid(self.CID), self.player, testmode=True)
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None):
"""
Test a command by assigning all the needed
@ -141,6 +150,7 @@ class CommandTest(TestCase):
from src.commands.default import general
class TestGeneral(CommandTest):
CID = 1
def test_cmds(self):
self.call(general.CmdLook(), "here", "Room1\n room_desc")
self.call(general.CmdHome(), "", "You are already home")
@ -158,6 +168,7 @@ class TestGeneral(CommandTest):
self.call(general.CmdSay(), "Testing", "You say, \"Testing\"")
self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):")
from src.commands.default import help
from src.commands.default.cmdset_character import CharacterCmdSet
class TestHelp(CommandTest):
@ -167,6 +178,7 @@ class TestHelp(CommandTest):
self.call(help.CmdSetHelp(), "testhelp, General = This is a test", "Topic 'testhelp' was successfully created.")
self.call(help.CmdHelp(), "testhelp", "Help topic for testhelp", cmdset=CharacterCmdSet())
from src.commands.default import system
class TestSystem(CommandTest):
CID = 3
@ -179,6 +191,7 @@ class TestSystem(CommandTest):
self.call(system.CmdAbout(), "", None)
self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:")
from src.commands.default import admin
class TestAdmin(CommandTest):
CID = 4
@ -190,6 +203,7 @@ class TestAdmin(CommandTest):
self.call(admin.CmdPerm(), "Char4b = Builders","Permission 'Builders' given to Char4b.")
self.call(admin.CmdBan(), "Char4", "NameBan char4 was added.")
from src.commands.default import player
class TestPlayer(CommandTest):
CID = 5
@ -209,6 +223,7 @@ class TestPlayer(CommandTest):
self.call(player.CmdCharCreate(), "Test1=Test char","Created new character Test1. Use @ic Test1 to enter the game", caller=self.player)
self.call(player.CmdQuell(), "", "Quelling Player permissions (immortals). Use @unquell to get them back.", caller=self.player)
from src.commands.default import building
class TestBuilding(CommandTest):
CID = 6
@ -239,6 +254,7 @@ class TestBuilding(CommandTest):
self.call(building.CmdScript(), "Obj6 = src.scripts.scripts.Script", "Script src.scripts.scripts.Script successfully added")
self.call(building.CmdTeleport(), "TestRoom1", "TestRoom1\nExits: back|Teleported to TestRoom1.")
from src.commands.default import comms
class TestComms(CommandTest):
CID = 7
@ -257,6 +273,7 @@ class TestComms(CommandTest):
self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] <channel> = <player> [:reason]") # noone else connected to boot
self.call(comms.CmdCdestroy(), "testchan" ,"Channel 'testchan' was destroyed.")
from src.commands.default import batchprocess
class TestBatchProcess(CommandTest):
CID = 8

View file

@ -14,7 +14,8 @@ from src.commands.default.muxcommand import MuxCommand
from src.commands.cmdhandler import CMD_LOGINSTART
# limit symbol import for API
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate", "CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
"CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
MULTISESSION_MODE = settings.MULTISESSION_MODE
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
@ -26,6 +27,7 @@ except Exception:
if not CONNECTION_SCREEN:
CONNECTION_SCREEN = "\nEvennia: Error in CONNECTION_SCREEN MODULE (randomly picked connection screen variable is not a string). \nEnter 'help' for aid."
class CmdUnconnectedConnect(MuxCommand):
"""
Connect to the game.
@ -40,7 +42,7 @@ class CmdUnconnectedConnect(MuxCommand):
"""
key = "connect"
aliases = ["conn", "con", "co"]
locks = "cmd:all()" # not really needed
locks = "cmd:all()" # not really needed
def func(self):
"""
@ -92,12 +94,13 @@ class CmdUnconnectedConnect(MuxCommand):
# actually do the login. This will call all other hooks:
# session.at_login()
# player.at_init() # always called when object is loaded from disk
# player.at_init() # always called when object is loaded from disk
# player.at_pre_login()
# player.at_first_login() # only once
# player.at_post_login(sessid=sessid)
session.sessionhandler.login(session, player)
class CmdUnconnectedCreate(MuxCommand):
"""
Create a new account.
@ -134,8 +137,9 @@ class CmdUnconnectedCreate(MuxCommand):
# sanity checks
if not re.findall('^[\w. @+-]+$', playername) or not (0 < len(playername) <= 30):
# this echoes the restrictions made by django's auth module (except not
# allowing spaces, for convenience of logging in).
# this echoes the restrictions made by django's auth
# module (except not allowing spaces, for convenience of
# logging in).
string = "\n\r Playername can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only."
session.msg(string)
return
@ -163,14 +167,14 @@ class CmdUnconnectedCreate(MuxCommand):
new_player = create.create_player(playername, None, password,
permissions=permissions)
except Exception, e:
session.msg("There was an error creating the default Player/Character:\n%s\n If this problem persists, contact an admin." % e)
logger.log_trace()
return
# This needs to be called so the engine knows this player is logging in for the first time.
# (so it knows to call the right hooks during login later)
# This needs to be called so the engine knows this player is
# logging in for the first time. (so it knows to call the right
# hooks during login later)
utils.init_new_player(new_player)
# join the new player to the public channel
@ -181,7 +185,6 @@ class CmdUnconnectedCreate(MuxCommand):
string = "New player '%s' could not connect to public channel!" % new_player.key
logger.log_errmsg(string)
if MULTISESSION_MODE < 2:
# if we only allow one character, create one with the same name as Player
# (in mode 2, the character must be created manually once logging in)
@ -210,12 +213,14 @@ class CmdUnconnectedCreate(MuxCommand):
session.msg(string % (playername, playername))
except Exception:
# We are in the middle between logged in and -not, so we have to handle tracebacks
# ourselves at this point. If we don't, we won't see any errors at all.
# We are in the middle between logged in and -not, so we have
# to handle tracebacks ourselves at this point. If we don't,
# we won't see any errors at all.
string = "%s\nThis is a bug. Please e-mail an admin if the problem persists."
session.msg(string % (traceback.format_exc()))
logger.log_errmsg(traceback.format_exc())
class CmdUnconnectedQuit(MuxCommand):
"""
We maintain a different version of the quit command
@ -230,7 +235,8 @@ class CmdUnconnectedQuit(MuxCommand):
"Simply close the connection."
session = self.caller
#session.msg("Good bye! Disconnecting ...")
session.sessionhandler.disconnect(session, "Good bye! Disconnecting ...")
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
class CmdUnconnectedLook(MuxCommand):
"""
@ -247,6 +253,7 @@ class CmdUnconnectedLook(MuxCommand):
"Show the connect screen."
self.caller.msg(CONNECTION_SCREEN)
class CmdUnconnectedHelp(MuxCommand):
"""
This is an unconnected version of the help command,

View file

@ -1,12 +1,14 @@
"""
Makes it easier to import by grouping all relevant things already at this level.
Makes it easier to import by grouping all relevant things already at this
level.
You can henceforth import most things directly from src.comms
Also, the initiated object manager is available as src.comms.msgmanager and src.comms.channelmanager.
Also, the initiated object manager is available as src.comms.msgmanager and
src.comms.channelmanager.
"""
from src.comms.models import *
from src.comms.models import *
msgmanager = Msg.objects
channelmanager = ChannelDB.objects

View file

@ -1,51 +1,57 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from django.contrib import admin
from src.comms.models import ChannelDB, Msg, PlayerChannelConnection, ExternalChannelConnection
class MsgAdmin(admin.ModelAdmin):
list_display = ('id', 'db_date_sent', 'db_sender', 'db_receivers', 'db_channels', 'db_message', 'db_lock_storage')
list_display_links = ("id",)
ordering = ["db_date_sent", 'db_sender', 'db_receivers', 'db_channels']
#readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels']
search_fields = ['id', '^db_date_sent', '^db_message']
save_as = True
save_on_top = True
list_select_related = True
#admin.site.register(Msg, MsgAdmin)
class PlayerChannelConnectionInline(admin.TabularInline):
model = PlayerChannelConnection
fieldsets = (
(None, {
'fields':(('db_player', 'db_channel')),
'classes':('collapse',)}),)
extra = 1
class ExternalChannelConnectionInline(admin.StackedInline):
model = ExternalChannelConnection
fieldsets = (
(None, {
'fields':(('db_is_enabled','db_external_key', 'db_channel'), 'db_external_send_code', 'db_external_config'),
'classes':('collapse',)
}),)
extra = 1
class ChannelAdmin(admin.ModelAdmin):
inlines = (PlayerChannelConnectionInline, ExternalChannelConnectionInline)
list_display = ('id', 'db_key', 'db_lock_storage')
list_display_links = ("id", 'db_key')
ordering = ["db_key"]
search_fields = ['id', 'db_key', 'db_aliases']
save_as = True
save_on_top = True
list_select_related = True
fieldsets = (
(None, {'fields':(('db_key',),'db_lock_storage',)}),
)
#
# This sets up how models are displayed
# in the web admin interface.
#
from django.contrib import admin
from src.comms.models import ChannelDB, Msg, PlayerChannelConnection, ExternalChannelConnection
class MsgAdmin(admin.ModelAdmin):
list_display = ('id', 'db_date_sent', 'db_sender', 'db_receivers',
'db_channels', 'db_message', 'db_lock_storage')
list_display_links = ("id",)
ordering = ["db_date_sent", 'db_sender', 'db_receivers', 'db_channels']
#readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels']
search_fields = ['id', '^db_date_sent', '^db_message']
save_as = True
save_on_top = True
list_select_related = True
#admin.site.register(Msg, MsgAdmin)
class PlayerChannelConnectionInline(admin.TabularInline):
model = PlayerChannelConnection
fieldsets = (
(None, {
'fields':(('db_player', 'db_channel')),
'classes':('collapse',)}),)
extra = 1
class ExternalChannelConnectionInline(admin.StackedInline):
model = ExternalChannelConnection
fieldsets = (
(None, {
'fields': (('db_is_enabled','db_external_key', 'db_channel'),
'db_external_send_code', 'db_external_config'),
'classes': ('collapse',)
}),)
extra = 1
class ChannelAdmin(admin.ModelAdmin):
inlines = (PlayerChannelConnectionInline, ExternalChannelConnectionInline)
list_display = ('id', 'db_key', 'db_lock_storage')
list_display_links = ("id", 'db_key')
ordering = ["db_key"]
search_fields = ['id', 'db_key', 'db_aliases']
save_as = True
save_on_top = True
list_select_related = True
fieldsets = (
(None, {'fields': (('db_key',), 'db_lock_storage',)}),
)
admin.site.register(ChannelDB, ChannelAdmin)

View file

@ -23,9 +23,9 @@ update() on the channelhandler. Or use Channel.objects.delete() which
does this for you.
"""
from src.comms.models import ChannelDB, Msg
from src.comms.models import ChannelDB
from src.commands import cmdset, command
from src.utils import utils
class ChannelCommand(command.Command):
"""
@ -51,7 +51,8 @@ class ChannelCommand(command.Command):
"""
Simple parser
"""
channelname, msg = self.args.split(":", 1) # cmdhandler sends channame:msg here.
# cmdhandler sends channame:msg here.
channelname, msg = self.args.split(":", 1)
self.args = (channelname.strip(), msg.strip())
def func(self):
@ -128,7 +129,7 @@ class ChannelHandler(object):
help_category="Channel names",
obj=channel,
is_channel=True)
cmd.__doc__= self._format_help(channel)
cmd.__doc__ = self._format_help(channel)
self.cached_channel_cmds.append(cmd)
self.cached_cmdsets = {}

View file

@ -53,7 +53,6 @@ class Comm(TypeClass):
else:
return '%s: %s' % (sender_string, message)
def format_external(self, msg, senders, emit=False):
"""
Used for formatting external messages. This is needed as a separate
@ -71,7 +70,6 @@ class Comm(TypeClass):
senders = ', '.join(senders)
return self.pose_transform(msg, senders)
def format_message(self, msg, emit=False):
"""
Formats a message body for display.
@ -169,30 +167,39 @@ class Comm(TypeClass):
conn.player.msg(msg.message, from_obj=msg.senders)
except AttributeError:
try:
conn.to_external(msg.message, senders=msg.senders, from_channel=self)
conn.to_external(msg.message,
senders=msg.senders, from_channel=self)
except Exception:
logger.log_trace("Cannot send msg to connection '%s'" % conn)
def msg(self, msgobj, header=None, senders=None, sender_strings=None,
persistent=True, online=False, emit=False, external=False):
"""
Send the given message to all players connected to channel. Note that
no permission-checking is done here; it is assumed to have been
done before calling this method. The optional keywords are not used if persistent is False.
done before calling this method. The optional keywords are not used if
persistent is False.
msgobj - a Msg/TempMsg instance or a message string. If one of the former, the remaining
keywords will be ignored. If a string, this will either be sent as-is (if persistent=False) or
it will be used together with header and senders keywords to create a Msg instance on the fly.
senders - an object, player or a list of objects or players. Optional if persistent=False.
sender_strings - Name strings of senders. Used for external connections where the sender
is not a player or object. When this is defined, external will be assumed.
msgobj - a Msg/TempMsg instance or a message string. If one of the
former, the remaining keywords will be ignored. If a string,
this will either be sent as-is (if persistent=False) or it
will be used together with header and senders keywords to
create a Msg instance on the fly.
senders - an object, player or a list of objects or players.
Optional if persistent=False.
sender_strings - Name strings of senders. Used for external
connections where the sender is not a player or object. When
this is defined, external will be assumed.
external - Treat this message agnostic of its sender.
persistent (bool) - ignored if msgobj is a Msg or TempMsg. If True, a Msg will be created, using
header and senders keywords. If False, other keywords will be ignored.
online (bool) - If this is set true, only messages people who are online. Otherwise, messages all players
connected. This can make things faster, but may not trigger listeners on players that are offline.
emit (bool) - Signals to the message formatter that this message is not to be directly associated with a name.
persistent (bool) - ignored if msgobj is a Msg or TempMsg. If True,
a Msg will be created, using header and senders keywords. If
False, other keywords will be ignored.
online (bool) - If this is set true, only messages people who are
online. Otherwise, messages all players connected. This can
make things faster, but may not trigger listeners on players
that are offline.
emit (bool) - Signals to the message formatter that this message is
not to be directly associated with a name.
"""
if senders:
senders = make_iter(senders)
@ -209,7 +216,7 @@ class Comm(TypeClass):
msgobj = TempMsg()
msgobj.header = header
msgobj.message = msg
msgobj.channels = [self.dbobj] # add this channel
msgobj.channels = [self.dbobj] # add this channel
if not msgobj.senders:
msgobj.senders = senders

View file

@ -65,11 +65,14 @@ class Send_IsAlive(Script):
self.interval = 900
self.desc = _("Send an IMC2 is-alive packet")
self.persistent = True
def at_repeat(self):
IMC2_CLIENT.send_packet(pck.IMC2PacketIsAlive())
def is_valid(self):
"Is only valid as long as there are channels to update"
return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_"))
return any(service for service in SESSIONS.server.services
if service.name.startswith("imc2_"))
class Send_Keepalive_Request(Script):
"""
@ -81,11 +84,14 @@ class Send_Keepalive_Request(Script):
self.interval = 3500
self.desc = _("Send an IMC2 keepalive-request packet")
self.persistent = True
def at_repeat(self):
IMC2_CLIENT.channel.send_packet(pck.IMC2PacketKeepAliveRequest())
def is_valid(self):
"Is only valid as long as there are channels to update"
return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_"))
return any(service for service in SESSIONS.server.services
if service.name.startswith("imc2_"))
class Prune_Inactive_Muds(Script):
"""
@ -99,13 +105,17 @@ class Prune_Inactive_Muds(Script):
self.desc = _("Check IMC2 list for inactive games")
self.persistent = True
self.inactive_threshold = 3599
def at_repeat(self):
for name, mudinfo in IMC2_MUDLIST.mud_list.items():
if time() - mudinfo.last_updated > self.inactive_threshold:
del IMC2_MUDLIST.mud_list[name]
def is_valid(self):
"Is only valid as long as there are channels to update"
return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_"))
return any(service for service in SESSIONS.server.services
if service.name.startswith("imc2_"))
class Sync_Server_Channel_List(Script):
"""
@ -116,21 +126,25 @@ class Sync_Server_Channel_List(Script):
"""
def at_script_creation(self):
self.key = "IMC2_Sync_Server_Channel_List"
self.interval = 24 * 3600 # once every day
self.interval = 24 * 3600 # once every day
self.desc = _("Re-sync IMC2 network channel list")
self.persistent = True
def at_repeat(self):
checked_networks = []
network = IMC2_CLIENT.factory.network
if not network in checked_networks:
channel.send_packet(pkg.IMC2PacketIceRefresh())
checked_networks.append(network)
def is_valid(self):
return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_"))
return any(service for service in SESSIONS.server.services
if service.name.startswith("imc2_"))
#
# IMC2 protocol
#
class IMC2Protocol(telnet.StatefulTelnetProtocol):
"""
Provides the abstraction for the IMC2 protocol. Handles connection,
@ -145,7 +159,6 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol):
self.network_name = None
self.sequence = None
def connectionMade(self):
"""
Triggered after connecting to the IMC2 network.
@ -179,8 +192,10 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol):
# This gets incremented with every command.
self.sequence += 1
packet.imc2_protocol = self
packet_str = utils.to_str(packet.assemble(self.factory.mudname, self.factory.client_pwd, self.factory.server_pwd))
if IMC2_DEBUG and not (hasattr(packet, 'packet_type') and packet.packet_type == "is-alive"):
packet_str = utils.to_str(packet.assemble(self.factory.mudname,
self.factory.client_pwd, self.factory.server_pwd))
if IMC2_DEBUG and not (hasattr(packet, 'packet_type') and
packet.packet_type == "is-alive"):
logger.log_infomsg("IMC2: SENT> %s" % packet_str)
logger.log_infomsg(str(packet))
self.sendLine(packet_str)
@ -257,9 +272,9 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol):
"""
Handle tells over IMC2 by formatting the text properly
"""
return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender":packet.sender,
"origin":packet.origin,
"msg":packet.optional_data.get('text', 'ERROR: No text provided.')}
return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender": packet.sender,
"origin": packet.origin,
"msg": packet.optional_data.get('text', 'ERROR: No text provided.')}
def lineReceived(self, line):
"""
@ -349,6 +364,7 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol):
target = data.get("target", "Unknown")
self.send_packet(pck.IMC2PacketWhois(from_obj.id, target))
class IMC2Factory(protocol.ClientFactory):
"""
Creates instances of the IMC2Protocol. Should really only ever
@ -382,7 +398,9 @@ def build_connection_key(channel, imc2_channel):
"Build an id hash for the connection"
if hasattr(channel, "key"):
channel = channel.key
return "imc2_%s:%s(%s)%s<>%s" % (IMC2_NETWORK, IMC2_PORT, IMC2_MUDNAME, imc2_channel, channel)
return "imc2_%s:%s(%s)%s<>%s" % (IMC2_NETWORK, IMC2_PORT,
IMC2_MUDNAME, imc2_channel, channel)
def start_scripts(validate=False):
"""
@ -402,6 +420,7 @@ def start_scripts(validate=False):
if not search.scripts("IMC2_Sync_Server_Channel_List"):
create.create_script(Sync_Server_Channel_List)
def create_connection(channel, imc2_channel):
"""
This will create a new IMC2<->channel connection.
@ -417,7 +436,8 @@ def create_connection(channel, imc2_channel):
old_conns = ExternalChannelConnection.objects.filter(db_external_key=key)
if old_conns:
# this evennia channel is already connected to imc. Check if imc2_channel is different.
# this evennia channel is already connected to imc. Check
# if imc2_channel is different.
# connection already exists. We try to only connect a new channel
old_config = old_conns[0].db_external_config.split(",")
if imc2_channel in old_config:
@ -432,9 +452,9 @@ def create_connection(channel, imc2_channel):
# no old connection found; create a new one.
config = imc2_channel
# how the evennia channel will be able to contact this protocol in reverse
send_code = "from src.comms.imc2 import IMC2_CLIENT\n"
send_code += "data={'channel':from_channel}\n"
send_code += "IMC2_CLIENT.msg_imc2(message, senders=[self])\n"
send_code = "from src.comms.imc2 import IMC2_CLIENT\n"
send_code += "data={'channel':from_channel}\n"
send_code += "IMC2_CLIENT.msg_imc2(message, senders=[self])\n"
conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_send_code=send_code,
db_external_config=config)
conn.save()
@ -453,15 +473,22 @@ def delete_connection(channel, imc2_channel):
conn.delete()
return True
def connect_to_imc2():
"Create the imc instance and connect to the IMC2 network."
# connect
imc = internet.TCPClient(IMC2_NETWORK, int(IMC2_PORT), IMC2Factory(IMC2_NETWORK, IMC2_PORT, IMC2_MUDNAME,
IMC2_CLIENT_PWD, IMC2_SERVER_PWD))
imc = internet.TCPClient(IMC2_NETWORK,
int(IMC2_PORT),
IMC2Factory(IMC2_NETWORK,
IMC2_PORT,
IMC2_MUDNAME,
IMC2_CLIENT_PWD,
IMC2_SERVER_PWD))
imc.setName("imc2_%s:%s(%s)" % (IMC2_NETWORK, IMC2_PORT, IMC2_MUDNAME))
SESSIONS.server.services.addService(imc)
def connect_all():
"""
Activates the imc2 system. Called by the server if IMC2_ENABLED=True.

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -1,59 +1,60 @@
"""
ANSI parser - this adds colour to text according to
special markup strings.
This is a IMC2 complacent version.
"""
import re
from src.utils import ansi
class IMCANSIParser(ansi.ANSIParser):
"""
This parser is per the IMC2 specification.
"""
def __init__(self):
normal = ansi.ANSI_NORMAL
hilite = ansi.ANSI_HILITE
self.ansi_map = [
(r'~Z', normal), # Random
(r'~x', normal + ansi.ANSI_BLACK), # Black
(r'~D', hilite + ansi.ANSI_BLACK), # Dark Grey
(r'~z', hilite + ansi.ANSI_BLACK),
(r'~w', normal + ansi.ANSI_WHITE), # Grey
(r'~W', hilite + ansi.ANSI_WHITE), # White
(r'~g', normal + ansi.ANSI_GREEN), # Dark Green
(r'~G', hilite + ansi.ANSI_GREEN), # Green
(r'~p', normal + ansi.ANSI_MAGENTA), # Dark magenta
(r'~m', normal + ansi.ANSI_MAGENTA),
(r'~M', hilite + ansi.ANSI_MAGENTA), # Magenta
(r'~P', hilite + ansi.ANSI_MAGENTA),
(r'~c', normal + ansi.ANSI_CYAN), # Cyan
(r'~y', normal + ansi.ANSI_YELLOW), # Dark Yellow (brown)
(r'~Y', hilite + ansi.ANSI_YELLOW), # Yellow
(r'~b', normal + ansi.ANSI_BLUE), # Dark Blue
(r'~B', hilite + ansi.ANSI_BLUE), # Blue
(r'~C', hilite + ansi.ANSI_BLUE),
(r'~r', normal + ansi.ANSI_RED), # Dark Red
(r'~R', hilite + ansi.ANSI_RED), # Red
## Formatting
(r'~L', hilite), # Bold/hilite
(r'~!', normal), # reset
(r'\\r', normal),
(r'\\n', ansi.ANSI_RETURN),
]
# prepare regex matching
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
for sub in self.ansi_map]
# prepare matching ansi codes overall
self.ansi_regex = re.compile("\033\[[0-9;]+m")
ANSI_PARSER = IMCANSIParser()
def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER):
"""
Shortcut to use the IMC2 ANSI parser.
"""
return parser.parse_ansi(string, strip_ansi=strip_ansi)
"""
ANSI parser - this adds colour to text according to
special markup strings.
This is a IMC2 complacent version.
"""
import re
from src.utils import ansi
class IMCANSIParser(ansi.ANSIParser):
"""
This parser is per the IMC2 specification.
"""
def __init__(self):
normal = ansi.ANSI_NORMAL
hilite = ansi.ANSI_HILITE
self.ansi_map = [
(r'~Z', normal), # Random
(r'~x', normal + ansi.ANSI_BLACK), # Black
(r'~D', hilite + ansi.ANSI_BLACK), # Dark Grey
(r'~z', hilite + ansi.ANSI_BLACK),
(r'~w', normal + ansi.ANSI_WHITE), # Grey
(r'~W', hilite + ansi.ANSI_WHITE), # White
(r'~g', normal + ansi.ANSI_GREEN), # Dark Green
(r'~G', hilite + ansi.ANSI_GREEN), # Green
(r'~p', normal + ansi.ANSI_MAGENTA), # Dark magenta
(r'~m', normal + ansi.ANSI_MAGENTA),
(r'~M', hilite + ansi.ANSI_MAGENTA), # Magenta
(r'~P', hilite + ansi.ANSI_MAGENTA),
(r'~c', normal + ansi.ANSI_CYAN), # Cyan
(r'~y', normal + ansi.ANSI_YELLOW), # Dark Yellow (brown)
(r'~Y', hilite + ansi.ANSI_YELLOW), # Yellow
(r'~b', normal + ansi.ANSI_BLUE), # Dark Blue
(r'~B', hilite + ansi.ANSI_BLUE), # Blue
(r'~C', hilite + ansi.ANSI_BLUE),
(r'~r', normal + ansi.ANSI_RED), # Dark Red
(r'~R', hilite + ansi.ANSI_RED), # Red
## Formatting
(r'~L', hilite), # Bold/hilite
(r'~!', normal), # reset
(r'\\r', normal),
(r'\\n', ansi.ANSI_RETURN),
]
# prepare regex matching
self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1])
for sub in self.ansi_map]
# prepare matching ansi codes overall
self.ansi_regex = re.compile("\033\[[0-9;]+m")
ANSI_PARSER = IMCANSIParser()
def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER):
"""
Shortcut to use the IMC2 ANSI parser.
"""
return parser.parse_ansi(string, strip_ansi=strip_ansi)

View file

@ -1,24 +1,24 @@
"""
This module handles some of the -reply packets like whois-reply.
"""
from src.objects.models import ObjectDB
from src.comms.imc2lib import imc2_ansi
from django.utils.translation import ugettext as _
def handle_whois_reply(packet):
"""
When the player sends an imcwhois <playername> request, the outgoing
packet contains the id of the one asking. This handler catches the
(possible) reply from the server, parses the id back to the
original asker and tells them the result.
"""
try:
pobject = ObjectDB.objects.get(id=packet.target)
response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown'))
string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text}
pobject.msg(string.strip())
except ObjectDB.DoesNotExist:
# No match found for whois sender. Ignore it.
pass
"""
This module handles some of the -reply packets like whois-reply.
"""
from src.objects.models import ObjectDB
from src.comms.imc2lib import imc2_ansi
from django.utils.translation import ugettext as _
def handle_whois_reply(packet):
"""
When the player sends an imcwhois <playername> request, the outgoing
packet contains the id of the one asking. This handler catches the
(possible) reply from the server, parses the id back to the
original asker and tells them the result.
"""
try:
pobject = ObjectDB.objects.get(id=packet.target)
response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown'))
string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text}
pobject.msg(string.strip())
except ObjectDB.DoesNotExist:
# No match found for whois sender. Ignore it.
pass

File diff suppressed because it is too large Load diff

View file

@ -1,104 +1,108 @@
"""
Certain periodic packets are sent by connected MUDs (is-alive, user-cache,
etc). The IMC2 protocol assumes that each connected MUD will capture these and
populate/maintain their own lists of other servers connected. This module
contains stuff like this.
"""
from time import time
class IMC2Mud(object):
"""
Stores information about other games connected to our current IMC2 network.
"""
def __init__(self, packet):
self.name = packet.origin
self.versionid = packet.optional_data.get('versionid', None)
self.networkname = packet.optional_data.get('networkname', None)
self.url = packet.optional_data.get('url', None)
self.host = packet.optional_data.get('host', None)
self.port = packet.optional_data.get('port', None)
self.sha256 = packet.optional_data.get('sha256', None)
# This is used to determine when a Mud has fallen into inactive status.
self.last_updated = time()
class IMC2MudList(object):
"""
Keeps track of other MUDs connected to the IMC network.
"""
def __init__(self):
# Mud list is stored in a dict, key being the IMC Mud name.
self.mud_list = {}
def get_mud_list(self):
"""
Returns a sorted list of connected Muds.
"""
muds = self.mud_list.items()
muds.sort()
return [value for key, value in muds]
def update_mud_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
Mud list for later retrieval.
"""
mud = IMC2Mud(packet)
self.mud_list[mud.name] = mud
def remove_mud_from_packet(self, packet):
"""
Removes a mud from the Mud list when given a packet.
"""
mud = IMC2Mud(packet)
try:
del self.mud_list[mud.name]
except KeyError:
# No matching entry, no big deal.
pass
class IMC2Channel(object):
"""
Stores information about channels available on the network.
"""
def __init__(self, packet):
self.localname = packet.optional_data.get('localname', None)
self.name = packet.optional_data.get('channel', None)
self.level = packet.optional_data.get('level', None)
self.owner = packet.optional_data.get('owner', None)
self.policy = packet.optional_data.get('policy', None)
self.last_updated = time()
class IMC2ChanList(object):
"""
Keeps track of other MUDs connected to the IMC network.
"""
def __init__(self):
# Chan list is stored in a dict, key being the IMC Mud name.
self.chan_list = {}
def get_channel_list(self):
"""
Returns a sorted list of cached channels.
"""
channels = self.chan_list.items()
channels.sort()
return [value for key, value in channels]
def update_channel_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
channel list for later retrieval.
"""
channel = IMC2Channel(packet)
self.chan_list[channel.name] = channel
def remove_channel_from_packet(self, packet):
"""
Removes a channel from the Channel list when given a packet.
"""
channel = IMC2Channel(packet)
try:
del self.chan_list[channel.name]
except KeyError:
# No matching entry, no big deal.
pass
"""
Certain periodic packets are sent by connected MUDs (is-alive, user-cache,
etc). The IMC2 protocol assumes that each connected MUD will capture these and
populate/maintain their own lists of other servers connected. This module
contains stuff like this.
"""
from time import time
class IMC2Mud(object):
"""
Stores information about other games connected to our current IMC2 network.
"""
def __init__(self, packet):
self.name = packet.origin
self.versionid = packet.optional_data.get('versionid', None)
self.networkname = packet.optional_data.get('networkname', None)
self.url = packet.optional_data.get('url', None)
self.host = packet.optional_data.get('host', None)
self.port = packet.optional_data.get('port', None)
self.sha256 = packet.optional_data.get('sha256', None)
# This is used to determine when a Mud has fallen into inactive status.
self.last_updated = time()
class IMC2MudList(object):
"""
Keeps track of other MUDs connected to the IMC network.
"""
def __init__(self):
# Mud list is stored in a dict, key being the IMC Mud name.
self.mud_list = {}
def get_mud_list(self):
"""
Returns a sorted list of connected Muds.
"""
muds = self.mud_list.items()
muds.sort()
return [value for key, value in muds]
def update_mud_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
Mud list for later retrieval.
"""
mud = IMC2Mud(packet)
self.mud_list[mud.name] = mud
def remove_mud_from_packet(self, packet):
"""
Removes a mud from the Mud list when given a packet.
"""
mud = IMC2Mud(packet)
try:
del self.mud_list[mud.name]
except KeyError:
# No matching entry, no big deal.
pass
class IMC2Channel(object):
"""
Stores information about channels available on the network.
"""
def __init__(self, packet):
self.localname = packet.optional_data.get('localname', None)
self.name = packet.optional_data.get('channel', None)
self.level = packet.optional_data.get('level', None)
self.owner = packet.optional_data.get('owner', None)
self.policy = packet.optional_data.get('policy', None)
self.last_updated = time()
class IMC2ChanList(object):
"""
Keeps track of other MUDs connected to the IMC network.
"""
def __init__(self):
# Chan list is stored in a dict, key being the IMC Mud name.
self.chan_list = {}
def get_channel_list(self):
"""
Returns a sorted list of cached channels.
"""
channels = self.chan_list.items()
channels.sort()
return [value for key, value in channels]
def update_channel_from_packet(self, packet):
"""
This grabs relevant info from the packet and stuffs it in the
channel list for later retrieval.
"""
channel = IMC2Channel(packet)
self.chan_list[channel.name] = channel
def remove_channel_from_packet(self, packet):
"""
Removes a channel from the Channel list when given a packet.
"""
channel = IMC2Channel(packet)
try:
del self.chan_list[channel.name]
except KeyError:
# No matching entry, no big deal.
pass

View file

@ -1,205 +1,218 @@
"""
This connects to an IRC network/channel and launches an 'bot' onto it.
The bot then pipes what is being said between the IRC channel and one or
more Evennia channels.
"""
from twisted.application import internet
from twisted.words.protocols import irc
from twisted.internet import protocol
from django.conf import settings
from src.comms.models import ExternalChannelConnection, ChannelDB
from src.utils import logger, utils
from src.server.sessionhandler import SESSIONS
from django.utils.translation import ugettext as _
INFOCHANNEL = ChannelDB.objects.channel_search(settings.CHANNEL_MUDINFO[0])
IRC_CHANNELS = []
def msg_info(message):
"""
Send info to default info channel
"""
message = '[%s][IRC]: %s' % (INFOCHANNEL[0].key, message)
try:
INFOCHANNEL[0].msg(message)
except AttributeError:
logger.log_infomsg("MUDinfo (irc): %s" % message)
class IRC_Bot(irc.IRCClient):
"""
This defines an IRC bot that connects to an IRC channel
and relays data to and from an evennia game.
"""
def _get_nickname(self):
"required for correct nickname setting"
return self.factory.nickname
nickname = property(_get_nickname)
def signedOn(self):
# This is the first point the protocol is instantiated.
# add this protocol instance to the global list so we
# can access it later to send data.
global IRC_CHANNELS
self.join(self.factory.channel)
IRC_CHANNELS.append(self)
#msg_info("Client connecting to %s.'" % (self.factory.channel))
def joined(self, channel):
msg = _("joined %s.") % self.factory.pretty_key
msg_info(msg)
logger.log_infomsg(msg)
def get_mesg_info(self, user, irc_channel, msg):
"""
Get basic information about a message posted in IRC.
"""
#find irc->evennia channel mappings
conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key)
if not conns:
return
#format message:
user = user.split("!")[0]
if user:
user.strip()
else:
user = _("Unknown")
msg = msg.strip()
sender_strings = ["%s@%s" % (user, irc_channel)]
return conns, msg, sender_strings
def privmsg(self, user, irc_channel, msg):
"Someone has written something in irc channel. Echo it to the evennia channel"
conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg)
#logger.log_infomsg("<IRC: " + msg)
for conn in conns:
if conn.channel:
conn.to_channel(msg, sender_strings=sender_strings)
def action(self, user, irc_channel, msg):
"Someone has performed an action, e.g. using /me <pose>"
#find irc->evennia channel mappings
conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key)
if not conns:
return
conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg)
# Transform this into a pose.
msg = ':' + msg
#logger.log_infomsg("<IRC: " + msg)
for conn in conns:
if conn.channel:
conn.to_channel(msg, sender_strings=sender_strings)
def msg_irc(self, msg, senders=None):
"""
Called by evennia when sending something to mapped IRC channel.
Note that this cannot simply be called msg() since that's the
name of of the twisted irc hook as well, this leads to some
initialization messages to be sent without checks, causing loops.
"""
self.msg(utils.to_str(self.factory.channel), utils.to_str(msg))
class IRCbotFactory(protocol.ClientFactory):
protocol = IRC_Bot
def __init__(self, key, channel, network, port, nickname, evennia_channel):
self.key = key
self.pretty_key = "%s:%s%s ('%s')" % (network, port, channel, nickname)
self.network = network
self.port = port
self.channel = channel
self.nickname = nickname
self.evennia_channel = evennia_channel
def clientConnectionLost(self, connector, reason):
from twisted.internet.error import ConnectionDone
if type(reason.type) == type(ConnectionDone):
msg_info(_("Connection closed."))
else:
msg_info(_("Lost connection %(key)s. Reason: '%(reason)s'. Reconnecting.") % {"key":self.pretty_key, "reason":reason})
connector.connect()
def clientConnectionFailed(self, connector, reason):
msg = _("Could not connect %(key)s Reason: '%(reason)s'") % {"key":self.pretty_key, "reason":reason}
msg_info(msg)
logger.log_errmsg(msg)
def build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick):
"Build an id hash for the connection"
if hasattr(channel, 'key'):
channel = channel.key
return "irc_%s:%s%s(%s)<>%s" % (irc_network, irc_port, irc_channel, irc_bot_nick, channel)
def build_service_key(key):
return "IRCbot:%s" % key
def create_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick):
"""
This will create a new IRC<->channel connection.
"""
if not type(channel) == ChannelDB:
new_channel = ChannelDB.objects.filter(db_key=channel)
if not new_channel:
logger.log_errmsg(_("Cannot attach IRC<->Evennia: Evennia Channel '%s' not found") % channel)
return False
channel = new_channel[0]
key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick)
old_conns = ExternalChannelConnection.objects.filter(db_external_key=key)
if old_conns:
return False
config = "%s|%s|%s|%s" % (irc_network, irc_port, irc_channel, irc_bot_nick)
# how the channel will be able to contact this protocol
send_code = "from src.comms.irc import IRC_CHANNELS\n"
send_code += "matched_ircs = [irc for irc in IRC_CHANNELS if irc.factory.key == '%s']\n" % key
send_code += "[irc.msg_irc(message, senders=[self]) for irc in matched_ircs]\n"
conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_send_code=send_code,
db_external_config=config)
conn.save()
# connect
connect_to_irc(conn)
return True
def delete_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick):
"Destroy a connection"
if hasattr(channel, 'key'):
channel = channel.key
key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick)
service_key = build_service_key(key)
try:
conn = ExternalChannelConnection.objects.get(db_external_key=key)
except Exception:
return False
conn.delete()
try:
service = SESSIONS.server.services.getServiceNamed(service_key)
except Exception:
return True
if service.running:
SESSIONS.server.services.removeService(service)
return True
def connect_to_irc(connection):
"Create the bot instance and connect to the IRC network and channel."
# get config
key = utils.to_str(connection.external_key)
service_key = build_service_key(key)
irc_network, irc_port, irc_channel, irc_bot_nick = [utils.to_str(conf) for conf in connection.external_config.split('|')]
# connect
bot = internet.TCPClient(irc_network, int(irc_port), IRCbotFactory(key, irc_channel, irc_network, irc_port, irc_bot_nick,
connection.channel.key))
bot.setName(service_key)
SESSIONS.server.services.addService(bot)
def connect_all():
"""
Activate all irc bots.
"""
for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'):
connect_to_irc(connection)
"""
This connects to an IRC network/channel and launches an 'bot' onto it.
The bot then pipes what is being said between the IRC channel and one or
more Evennia channels.
"""
from twisted.application import internet
from twisted.words.protocols import irc
from twisted.internet import protocol
from django.conf import settings
from src.comms.models import ExternalChannelConnection, ChannelDB
from src.utils import logger, utils
from src.server.sessionhandler import SESSIONS
from django.utils.translation import ugettext as _
INFOCHANNEL = ChannelDB.objects.channel_search(settings.CHANNEL_MUDINFO[0])
IRC_CHANNELS = []
def msg_info(message):
"""
Send info to default info channel
"""
message = '[%s][IRC]: %s' % (INFOCHANNEL[0].key, message)
try:
INFOCHANNEL[0].msg(message)
except AttributeError:
logger.log_infomsg("MUDinfo (irc): %s" % message)
class IRC_Bot(irc.IRCClient):
"""
This defines an IRC bot that connects to an IRC channel
and relays data to and from an evennia game.
"""
def _get_nickname(self):
"required for correct nickname setting"
return self.factory.nickname
nickname = property(_get_nickname)
def signedOn(self):
# This is the first point the protocol is instantiated.
# add this protocol instance to the global list so we
# can access it later to send data.
global IRC_CHANNELS
self.join(self.factory.channel)
IRC_CHANNELS.append(self)
#msg_info("Client connecting to %s.'" % (self.factory.channel))
def joined(self, channel):
msg = _("joined %s.") % self.factory.pretty_key
msg_info(msg)
logger.log_infomsg(msg)
def get_mesg_info(self, user, irc_channel, msg):
"""
Get basic information about a message posted in IRC.
"""
#find irc->evennia channel mappings
conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key)
if not conns:
return
#format message:
user = user.split("!")[0]
if user:
user.strip()
else:
user = _("Unknown")
msg = msg.strip()
sender_strings = ["%s@%s" % (user, irc_channel)]
return conns, msg, sender_strings
def privmsg(self, user, irc_channel, msg):
"Someone has written something in irc channel. Echo it to the evennia channel"
conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg)
#logger.log_infomsg("<IRC: " + msg)
for conn in conns:
if conn.channel:
conn.to_channel(msg, sender_strings=sender_strings)
def action(self, user, irc_channel, msg):
"Someone has performed an action, e.g. using /me <pose>"
#find irc->evennia channel mappings
conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key)
if not conns:
return
conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg)
# Transform this into a pose.
msg = ':' + msg
#logger.log_infomsg("<IRC: " + msg)
for conn in conns:
if conn.channel:
conn.to_channel(msg, sender_strings=sender_strings)
def msg_irc(self, msg, senders=None):
"""
Called by evennia when sending something to mapped IRC channel.
Note that this cannot simply be called msg() since that's the
name of of the twisted irc hook as well, this leads to some
initialization messages to be sent without checks, causing loops.
"""
self.msg(utils.to_str(self.factory.channel), utils.to_str(msg))
class IRCbotFactory(protocol.ClientFactory):
protocol = IRC_Bot
def __init__(self, key, channel, network, port, nickname, evennia_channel):
self.key = key
self.pretty_key = "%s:%s%s ('%s')" % (network, port, channel, nickname)
self.network = network
self.port = port
self.channel = channel
self.nickname = nickname
self.evennia_channel = evennia_channel
def clientConnectionLost(self, connector, reason):
from twisted.internet.error import ConnectionDone
if type(reason.type) == type(ConnectionDone):
msg_info(_("Connection closed."))
else:
msg_info(_("Lost connection %(key)s. Reason: '%(reason)s'. Reconnecting.") % {"key":self.pretty_key, "reason":reason})
connector.connect()
def clientConnectionFailed(self, connector, reason):
msg = _("Could not connect %(key)s Reason: '%(reason)s'") % {"key":self.pretty_key, "reason":reason}
msg_info(msg)
logger.log_errmsg(msg)
def build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick):
"Build an id hash for the connection"
if hasattr(channel, 'key'):
channel = channel.key
return "irc_%s:%s%s(%s)<>%s" % (irc_network, irc_port,
irc_channel, irc_bot_nick, channel)
def build_service_key(key):
return "IRCbot:%s" % key
def create_connection(channel, irc_network, irc_port,
irc_channel, irc_bot_nick):
"""
This will create a new IRC<->channel connection.
"""
if not type(channel) == ChannelDB:
new_channel = ChannelDB.objects.filter(db_key=channel)
if not new_channel:
logger.log_errmsg(_("Cannot attach IRC<->Evennia: Evennia Channel '%s' not found") % channel)
return False
channel = new_channel[0]
key = build_connection_key(channel, irc_network, irc_port,
irc_channel, irc_bot_nick)
old_conns = ExternalChannelConnection.objects.filter(db_external_key=key)
if old_conns:
return False
config = "%s|%s|%s|%s" % (irc_network, irc_port, irc_channel, irc_bot_nick)
# how the channel will be able to contact this protocol
send_code = "from src.comms.irc import IRC_CHANNELS\n"
send_code += "matched_ircs = [irc for irc in IRC_CHANNELS if irc.factory.key == '%s']\n" % key
send_code += "[irc.msg_irc(message, senders=[self]) for irc in matched_ircs]\n"
conn = ExternalChannelConnection(db_channel=channel,
db_external_key=key,
db_external_send_code=send_code,
db_external_config=config)
conn.save()
# connect
connect_to_irc(conn)
return True
def delete_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick):
"Destroy a connection"
if hasattr(channel, 'key'):
channel = channel.key
key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick)
service_key = build_service_key(key)
try:
conn = ExternalChannelConnection.objects.get(db_external_key=key)
except Exception:
return False
conn.delete()
try:
service = SESSIONS.server.services.getServiceNamed(service_key)
except Exception:
return True
if service.running:
SESSIONS.server.services.removeService(service)
return True
def connect_to_irc(connection):
"Create the bot instance and connect to the IRC network and channel."
# get config
key = utils.to_str(connection.external_key)
service_key = build_service_key(key)
irc_network, irc_port, irc_channel, irc_bot_nick = [utils.to_str(conf) for conf in connection.external_config.split('|')]
# connect
bot = internet.TCPClient(irc_network, int(irc_port), IRCbotFactory(key, irc_channel, irc_network, irc_port, irc_bot_nick,
connection.channel.key))
bot.setName(service_key)
SESSIONS.server.services.addService(bot)
def connect_all():
"""
Activate all irc bots.
"""
for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'):
connect_to_irc(connection)

View file

@ -18,10 +18,12 @@ _User = None
# error class
class CommError(Exception):
"Raise by comm system, to allow feedback to player when caught."
pass
#
# helper functions
#
@ -43,6 +45,7 @@ def dbref(dbref, reqhash=True):
return None
return dbref
def identify_object(inp):
"identify if an object is a player or an object; return its database model"
# load global stores
@ -61,18 +64,25 @@ def identify_object(inp):
return inp, None
# try to identify the type
try:
obj = _GA(inp, "dbobj") # this works for all typeclassed entities
obj = _GA(inp, "dbobj") # this works for all typeclassed entities
except AttributeError:
obj = inp
typ = type(obj)
if typ == _PlayerDB: return obj, "player"
elif typ == _ObjectDB: return obj, "object"
elif typ == _ChannelDB: return obj, "channel"
elif dbref(obj): return dbref(obj), "dbref"
elif typ == basestring: return obj, "string"
elif typ == _ExternalConnection: return obj, "external"
if typ == _PlayerDB:
return obj, "player"
elif typ == _ObjectDB:
return obj, "object"
elif typ == _ChannelDB:
return obj, "channel"
elif dbref(obj):
return dbref(obj), "dbref"
elif typ == basestring:
return obj, "string"
elif typ == _ExternalConnection:
return obj, "external"
return obj, None # Something else
def to_object(inp, objtype='player'):
"""
Locates the object related to the given
@ -85,28 +95,39 @@ def to_object(inp, objtype='player'):
if typ == objtype:
return obj
if objtype == 'player':
if typ == 'object': return obj.player
if typ == 'string': return _PlayerDB.objects.get(user_username__iexact=obj)
if typ == 'dbref': return _PlayerDB.objects.get(id=obj)
if typ == 'object':
return obj.player
if typ == 'string':
return _PlayerDB.objects.get(user_username__iexact=obj)
if typ == 'dbref':
return _PlayerDB.objects.get(id=obj)
print objtype, inp, obj, typ, type(inp)
raise CommError()
elif objtype == 'object':
if typ == 'player': return obj.obj
if typ == 'string': return _ObjectDB.objects.get(db_key__iexact=obj)
if typ == 'dbref': return _ObjectDB.objects.get(id=obj)
if typ == 'player':
return obj.obj
if typ == 'string':
return _ObjectDB.objects.get(db_key__iexact=obj)
if typ == 'dbref':
return _ObjectDB.objects.get(id=obj)
print objtype, inp, obj, typ, type(inp)
raise CommError()
elif objtype == 'channel':
if typ == 'string': return _ChannelDB.objects.get(db_key__iexact=obj)
if typ == 'dbref': return _ChannelDB.objects.get(id=obj)
if typ == 'string':
return _ChannelDB.objects.get(db_key__iexact=obj)
if typ == 'dbref':
return _ChannelDB.objects.get(id=obj)
print objtype, inp, obj, typ, type(inp)
raise CommError()
elif objtype == 'external':
if typ == 'string': return _ExternalConnection.objects.get(db_key=inp)
if typ == 'dbref': return _ExternalConnection.objects.get(id=obj)
if typ == 'string':
return _ExternalConnection.objects.get(db_key=inp)
if typ == 'dbref':
return _ExternalConnection.objects.get(id=obj)
print objtype, inp, obj, typ, type(inp)
raise CommError()
#
# Msg manager
#
@ -146,17 +167,21 @@ class MsgManager(models.Manager):
def get_messages_by_sender(self, obj, exclude_channel_messages=False):
"""
Get all messages sent by one entity - this could be either a player or an object
Get all messages sent by one entity - this could be either a
player or an object
only_non_channel: only return messages -not- aimed at a channel (e.g. private tells)
only_non_channel: only return messages -not- aimed at a channel
(e.g. private tells)
"""
obj, typ = identify_object(obj)
if exclude_channel_messages:
# explicitly exclude channel recipients
if typ == 'player':
return list(self.filter(db_sender_players=obj, db_receivers_channels__isnull=True).exclude(db_hide_from_players=obj))
return list(self.filter(db_sender_players=obj,
db_receivers_channels__isnull=True).exclude(db_hide_from_players=obj))
elif typ == 'object':
return list(self.filter(db_sender_objects=obj, db_receivers_channels__isnull=True).exclude(db_hide_from_objects=obj))
return list(self.filter(db_sender_objects=obj,
db_receivers_channels__isnull=True).exclude(db_hide_from_objects=obj))
else:
raise CommError
else:
@ -208,9 +233,10 @@ class MsgManager(models.Manager):
if msg:
return msg[0]
# We use Q objects to gradually build up the query - this way we only need to do one
# database lookup at the end rather than gradually refining with multiple filter:s.
# Django Note: Q objects can be combined with & and | (=AND,OR). ~ negates the queryset
# We use Q objects to gradually build up the query - this way we only
# need to do one database lookup at the end rather than gradually
# refining with multiple filter:s. Django Note: Q objects can be
# combined with & and | (=AND,OR). ~ negates the queryset
# filter by sender
sender, styp = identify_object(sender)
@ -238,6 +264,7 @@ class MsgManager(models.Manager):
# execute the query
return list(self.filter(sender_restrict & receiver_restrict & fulltext_restrict))
#
# Channel manager
#
@ -350,9 +377,12 @@ class ChannelManager(models.Manager):
channels = self.filter(db_key__iexact=ostring)
if not channels:
# still no match. Search by alias.
channels = [channel for channel in self.all() if ostring.lower() in [a.lower for a in channel.aliases.all()]]
channels = [channel for channel in self.all()
if ostring.lower() in [a.lower
for a in channel.aliases.all()]]
return channels
#
# PlayerChannelConnection manager
#
@ -419,6 +449,7 @@ class PlayerChannelConnectionManager(models.Manager):
for conn in conns:
conn.delete()
class ExternalChannelConnectionManager(models.Manager):
"""
This ExternalChannelConnectionManager implements methods for searching

View file

@ -30,12 +30,14 @@ from src.locks.lockhandler import LockHandler
from src.utils import logger
from src.utils.utils import is_iter, to_str, crop, make_iter
__all__ = ("Msg", "TempMsg", "ChannelDB", "PlayerChannelConnection", "ExternalChannelConnection")
__all__ = ("Msg", "TempMsg", "ChannelDB",
"PlayerChannelConnection", "ExternalChannelConnection")
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
#------------------------------------------------------------
#
# Msg
@ -66,9 +68,9 @@ class Msg(SharedMemoryModel):
# These databse fields are all set using their corresponding properties,
# named same as the field, but withtout the db_* prefix.
# Sender is either a player, an object or an external sender, like an IRC channel
# normally there is only one, but if co-modification of a message is allowed, there
# may be more than one "author"
# Sender is either a player, an object or an external sender, like
# an IRC channel; normally there is only one, but if co-modification of
# a message is allowed, there may be more than one "author"
db_sender_players = models.ManyToManyField("players.PlayerDB", related_name='sender_player_set', null=True, verbose_name='sender(player)', db_index=True)
db_sender_objects = models.ManyToManyField("objects.ObjectDB", related_name='sender_object_set', null=True, verbose_name='sender(object)', db_index=True)
db_sender_external = models.CharField('external sender', max_length=255, null=True, db_index=True,
@ -80,8 +82,8 @@ class Msg(SharedMemoryModel):
db_receivers_objects = models.ManyToManyField('objects.ObjectDB', related_name='receiver_object_set', null=True, help_text="object receivers")
db_receivers_channels = models.ManyToManyField("ChannelDB", related_name='channel_set', null=True, help_text="channel recievers")
# header could be used for meta-info about the message if your system needs it, or as a separate
# store for the mail subject line maybe.
# header could be used for meta-info about the message if your system needs
# it, or as a separate store for the mail subject line maybe.
db_header = models.TextField('header', null=True, blank=True)
# the message body itself
db_message = models.TextField('messsage')
@ -124,6 +126,7 @@ class Msg(SharedMemoryModel):
list(self.db_sender_players.all()) +
list(self.db_sender_objects.all()) +
self.extra_senders]
#@sender.setter
def __senders_set(self, value):
"Setter. Allows for self.sender = value"
@ -143,6 +146,7 @@ class Msg(SharedMemoryModel):
else:
raise ValueError(obj)
self.save()
#@sender.deleter
def __senders_del(self):
"Deleter. Clears all senders"
@ -173,12 +177,19 @@ class Msg(SharedMemoryModel):
# receivers property
#@property
def __receivers_get(self):
"Getter. Allows for value = self.receivers. Returns three lists of receivers: players, objects and channels."
"""
Getter. Allows for value = self.receivers.
Returns three lists of receivers: players, objects and channels.
"""
return [hasattr(o, "typeclass") and o.typeclass or o for o in
list(self.db_receivers_players.all()) + list(self.db_receivers_objects.all())]
#@receivers.setter
def __receivers_set(self, value):
"Setter. Allows for self.receivers = value. This appends a new receiver to the message."
"""
Setter. Allows for self.receivers = value.
This appends a new receiver to the message.
"""
for val in (v for v in make_iter(value) if v):
obj, typ = identify_object(val)
if typ == 'player':
@ -190,6 +201,7 @@ class Msg(SharedMemoryModel):
else:
raise ValueError
self.save()
#@receivers.deleter
def __receivers_del(self):
"Deleter. Clears all receivers"
@ -215,11 +227,15 @@ class Msg(SharedMemoryModel):
def __channels_get(self):
"Getter. Allows for value = self.channels. Returns a list of channels."
return self.db_receivers_channels.all()
#@channels.setter
def __channels_set(self, value):
"Setter. Allows for self.channels = value. Requires a channel to be added."
"""
Setter. Allows for self.channels = value.
Requires a channel to be added."""
for val in (v.dbobj for v in make_iter(value) if v):
self.db_receivers_channels.add(val)
#@channels.deleter
def __channels_del(self):
"Deleter. Allows for del self.channels"
@ -228,8 +244,12 @@ class Msg(SharedMemoryModel):
channels = property(__channels_get, __channels_set, __channels_del)
def __hide_from_get(self):
"Getter. Allows for value = self.hide_from. Returns 3 lists of players, objects and channels"
"""
Getter. Allows for value = self.hide_from.
Returns 3 lists of players, objects and channels
"""
return self.db_hide_from_players.all(), self.db_hide_from_objects.all(), self.db_hide_from_channels.all()
#@hide_from_sender.setter
def __hide_from_set(self, value):
"Setter. Allows for self.hide_from = value. Will append to hiders"
@ -243,6 +263,7 @@ class Msg(SharedMemoryModel):
else:
raise ValueError
self.save()
#@hide_from_sender.deleter
def __hide_from_del(self):
"Deleter. Allows for del self.hide_from_senders"
@ -275,7 +296,6 @@ class TempMsg(object):
temporary messages that will not be stored.
It mimics the "real" Msg object, but don't require
sender to be given.
"""
def __init__(self, senders=None, receivers=None, channels=None, message="", header="", type="", lockstring="", hide_from=None):
self.senders = senders and make_iter(senders) or []
@ -301,7 +321,7 @@ class TempMsg(object):
try:
self.senders.remove(o)
except ValueError:
pass # nothing to remove
pass # nothing to remove
def remove_receiver(self, obj):
"Remove a sender or a list of senders"
@ -309,11 +329,13 @@ class TempMsg(object):
try:
self.senders.remove(o)
except ValueError:
pass # nothing to remove
pass # nothing to remove
def access(self, accessing_obj, access_type='read', default=False):
"checks lock access"
return self.locks.check(accessing_obj, access_type=access_type, default=default)
return self.locks.check(accessing_obj,
access_type=access_type, default=default)
#------------------------------------------------------------
#
@ -347,7 +369,6 @@ class ChannelDB(TypedObject):
_SA(self, "aliases", AliasHandler(self, category_prefix="comm_"))
_SA(self, "attributes", AttributeHandler(self))
class Meta:
"Define Django meta options"
verbose_name = "Channel"
@ -415,6 +436,7 @@ class ChannelDB(TypedObject):
"""
return self.locks.check(accessing_obj, access_type=access_type, default=default)
class PlayerChannelConnection(SharedMemoryModel):
"""
This connects a player object to a particular comm channel.
@ -435,11 +457,13 @@ class PlayerChannelConnection(SharedMemoryModel):
def player_get(self):
"Getter. Allows for value = self.player"
return self.db_player
#@player.setter
def player_set(self, value):
"Setter. Allows for self.player = value"
self.db_player = value
self.save()
#@player.deleter
def player_del(self):
"Deleter. Allows for del self.player. Deletes connection."
@ -451,11 +475,13 @@ class PlayerChannelConnection(SharedMemoryModel):
def channel_get(self):
"Getter. Allows for value = self.channel"
return self.db_channel.typeclass
#@channel.setter
def channel_set(self, value):
"Setter. Allows for self.channel = value"
self.db_channel = value.dbobj
self.save()
#@channel.deleter
def channel_del(self):
"Deleter. Allows for del self.channel. Deletes connection."
@ -507,10 +533,12 @@ class ExternalChannelConnection(SharedMemoryModel):
"Getter. Allows for value = self.channel"
return self.db_channel
#@channel.setter
def channel_set(self, value):
"Setter. Allows for self.channel = value"
self.db_channel = value
self.save()
#@channel.deleter
def channel_del(self):
"Deleter. Allows for del self.channel. Deletes connection."
@ -522,11 +550,13 @@ class ExternalChannelConnection(SharedMemoryModel):
def external_key_get(self):
"Getter. Allows for value = self.external_key"
return self.db_external_key
#@external_key.setter
def external_key_set(self, value):
"Setter. Allows for self.external_key = value"
self.db_external_key = value
self.save()
#@external_key.deleter
def external_key_del(self):
"Deleter. Allows for del self.external_key. Deletes connection."
@ -538,11 +568,13 @@ class ExternalChannelConnection(SharedMemoryModel):
def external_send_code_get(self):
"Getter. Allows for value = self.external_send_code"
return self.db_external_send_code
#@external_send_code.setter
def external_send_code_set(self, value):
"Setter. Allows for self.external_send_code = value"
self.db_external_send_code = value
self.save()
#@external_send_code.deleter
def external_send_code_del(self):
"Deleter. Allows for del self.external_send_code. Deletes connection."
@ -555,11 +587,13 @@ class ExternalChannelConnection(SharedMemoryModel):
def external_config_get(self):
"Getter. Allows for value = self.external_config"
return self.db_external_config
#@external_config.setter
def external_config_set(self, value):
"Setter. Allows for self.external_config = value"
self.db_external_config = value
self.save()
#@external_config.deleter
def external_config_del(self):
"Deleter. Allows for del self.external_config. Deletes connection."
@ -572,11 +606,13 @@ class ExternalChannelConnection(SharedMemoryModel):
def is_enabled_get(self):
"Getter. Allows for value = self.is_enabled"
return self.db_is_enabled
#@is_enabled.setter
def is_enabled_set(self, value):
"Setter. Allows for self.is_enabled = value"
self.db_is_enabled = value
self.save()
#@is_enabled.deleter
def is_enabled_del(self):
"Deleter. Allows for del self.is_enabled. Deletes connection."
@ -589,8 +625,8 @@ class ExternalChannelConnection(SharedMemoryModel):
def to_channel(self, message, *args, **kwargs):
"Send external -> channel"
if 'from_obj' in kwargs and kwargs.pop('from_obj'):
from_obj = self.external_key
#if 'from_obj' in kwargs and kwargs.pop('from_obj'):
# from_obj = self.external_key
self.channel.msg(message, senders=[self], *args, **kwargs)
def to_external(self, message, senders=None, from_channel=None):
@ -599,12 +635,13 @@ class ExternalChannelConnection(SharedMemoryModel):
# make sure we are not echoing back our own message to ourselves
# (this would result in a nasty infinite loop)
#print senders
if self in make_iter(senders):#.external_key:
if self in make_iter(senders): #.external_key:
return
try:
# we execute the code snippet that should make it possible for the
# connection to contact the protocol correctly (as set by the protocol).
# connection to contact the protocol correctly (as set by the
# protocol).
# Note that the code block has access to the variables here, such
# as message, from_obj and from_channel.
exec(to_str(self.external_send_code))

View file

@ -21,6 +21,7 @@ RETAG = re.compile(r'<[^>]*?>')
# holds rss readers they can be shut down at will.
RSS_READERS = {}
def msg_info(message):
"""
Send info to default info channel
@ -37,6 +38,7 @@ if RSS_ENABLED:
except ImportError:
raise ImportError("RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False.")
class RSSReader(object):
"""
Reader script used to connect to each individual RSS feed
@ -50,7 +52,7 @@ class RSSReader(object):
self.key = key
self.url = url
self.interval = interval
self.entries = {} # stored feeds
self.entries = {} # stored feeds
self.task = None
# first we do is to load the feed so we don't resend
# old entries whenever the reader starts.
@ -63,7 +65,8 @@ class RSSReader(object):
feed = feedparser.parse(self.url)
new = []
for entry in (e for e in feed['entries'] if e['id'] not in self.entries):
txt = "[RSS] %s: %s" % (RETAG.sub("", entry['title']), entry['link'].replace('\n','').encode('utf-8'))
txt = "[RSS] %s: %s" % (RETAG.sub("", entry['title']),
entry['link'].replace('\n','').encode('utf-8'))
self.entries[entry['id']] = txt
new.append(txt)
return new
@ -92,12 +95,14 @@ class RSSReader(object):
self.task.start(self.interval, now=False)
RSS_READERS[self.key] = self
def build_connection_key(channel, url):
"This is used to id the connection"
if hasattr(channel, 'key'):
channel = channel.key
return "rss_%s>%s" % (url, channel)
def create_connection(channel, url, interval):
"""
This will create a new RSS->channel connection
@ -113,13 +118,17 @@ def create_connection(channel, url, interval):
if old_conns:
return False
config = "%s|%i" % (url, interval)
# There is no sendback from evennia to the rss, so we need not define any sendback code.
conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_config=config)
# There is no sendback from evennia to the rss, so we need not define
# any sendback code.
conn = ExternalChannelConnection(db_channel=channel,
db_external_key=key,
db_external_config=config)
conn.save()
connect_to_rss(conn)
return True
def delete_connection(channel, url):
"""
Delete rss connection between channel and url
@ -135,6 +144,7 @@ def delete_connection(channel, url):
reader.task.stop()
return True
def connect_to_rss(connection):
"""
Create the parser instance and connect to RSS feed and channel
@ -145,6 +155,7 @@ def connect_to_rss(connection):
# Create reader (this starts the running task and stores a reference in RSS_TASKS)
RSSReader(key, url, int(interval))
def connect_all():
"""
Activate all rss feed parsers

View file

@ -1,11 +1,11 @@
"""
Makes it easier to import by grouping all relevant things already at this level.
Makes it easier to import by grouping all relevant things already at this level.
You can henceforth import most things directly from src.help
Also, the initiated object manager is available as src.help.manager.
"""
from src.help.models import *
from src.help.models import *
manager = HelpEntry.objects

View file

@ -1,36 +1,37 @@
"""
This defines how to edit help entries in Admin.
"""
from django import forms
from django.contrib import admin
from src.help.models import HelpEntry
class HelpEntryForm(forms.ModelForm):
"Defines how to display the help entry"
class Meta:
model = HelpEntry
db_help_category = forms.CharField(label="Help category", initial='General',
help_text="organizes help entries in lists")
db_lock_storage = forms.CharField(label="Locks", initial='view:all()',required=False,
widget=forms.TextInput(attrs={'size':'40'}),)
class HelpEntryAdmin(admin.ModelAdmin):
"Sets up the admin manaager for help entries"
list_display = ('id', 'db_key', 'db_help_category', 'db_lock_storage')
list_display_links = ('id', 'db_key')
search_fields = ['^db_key', 'db_entrytext']
ordering = ['db_help_category', 'db_key']
save_as = True
save_on_top = True
list_select_related = True
form = HelpEntryForm
fieldsets = (
(None, {'fields':(('db_key', 'db_help_category'), 'db_entrytext', 'db_lock_storage'),
'description':"Sets a Help entry. Set lock to <i>view:all()</I> unless you want to restrict it."}),)
admin.site.register(HelpEntry, HelpEntryAdmin)
"""
This defines how to edit help entries in Admin.
"""
from django import forms
from django.contrib import admin
from src.help.models import HelpEntry
class HelpEntryForm(forms.ModelForm):
"Defines how to display the help entry"
class Meta:
model = HelpEntry
db_help_category = forms.CharField(label="Help category", initial='General',
help_text="organizes help entries in lists")
db_lock_storage = forms.CharField(label="Locks", initial='view:all()',required=False,
widget=forms.TextInput(attrs={'size':'40'}),)
class HelpEntryAdmin(admin.ModelAdmin):
"Sets up the admin manaager for help entries"
list_display = ('id', 'db_key', 'db_help_category', 'db_lock_storage')
list_display_links = ('id', 'db_key')
search_fields = ['^db_key', 'db_entrytext']
ordering = ['db_help_category', 'db_key']
save_as = True
save_on_top = True
list_select_related = True
form = HelpEntryForm
fieldsets = (
(None, {'fields':(('db_key', 'db_help_category'),
'db_entrytext', 'db_lock_storage'),
'description':"Sets a Help entry. Set lock to <i>view:all()</I> unless you want to restrict it."}),)
admin.site.register(HelpEntry, HelpEntryAdmin)

View file

@ -5,6 +5,7 @@ from django.db import models
from src.utils import logger, utils
__all__ = ("HelpEntryManager",)
class HelpEntryManager(models.Manager):
"""
This HelpEntryManager implements methods for searching
@ -48,7 +49,7 @@ class HelpEntryManager(models.Manager):
Do a fuzzy match, preferably within the category of the
current topic.
"""
return self.filter(db_key__icontains=topicstring).exclude(db_key__iexact=topicstring)
return self.filter(db_key__icontains=topicstr).exclude(db_key__iexact=topicstr)
def find_topics_with_category(self, help_category):
"""
@ -92,6 +93,7 @@ class HelpEntryManager(models.Manager):
"""
ostring = ostring.strip().lower()
if help_category:
return self.filter(db_key__iexact=ostring, db_help_category__iexact=help_category)
return self.filter(db_key__iexact=ostring,
db_help_category__iexact=help_category)
else:
return self.filter(db_key__iexact=ostring)

View file

@ -12,12 +12,11 @@ game world, policy info, rules and similar.
from django.db import models
from src.utils.idmapper.models import SharedMemoryModel
from src.help.manager import HelpEntryManager
from src.utils import ansi
from src.typeclasses.models import Tag, TagHandler
from src.locks.lockhandler import LockHandler
from src.utils.utils import is_iter
__all__ = ("HelpEntry",)
#------------------------------------------------------------
#
# HelpEntry

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -86,6 +86,7 @@ from src.utils import utils
_PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
def _to_player(accessing_obj):
"Helper function. Makes sure an accessing object is a player object"
if utils.inherits_from(accessing_obj, "src.objects.objects.Object"):
@ -99,14 +100,21 @@ def _to_player(accessing_obj):
def true(*args, **kwargs):
"Always returns True."
return True
def all(*args, **kwargs):
return True
def false(*args, **kwargs):
"Always returns False"
return False
def none(*args, **kwargs):
return False
def self(accessing_obj, accessed_obj, *args, **kwargs):
"""
Check if accessing_obj is the same as accessed_obj
@ -172,7 +180,8 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs):
else:
return hpos_target <= hpos_player
elif not is_quell and perm in perms_player:
# if we get here, check player perms first, otherwise continue as normal
# if we get here, check player perms first, otherwise
# continue as normal
return True
if perm in perms_object:
@ -185,6 +194,7 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs):
if hperm in perms_object and hpos_target < hpos)
return False
def perm_above(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only allow objects with a permission *higher* in the permission
@ -193,7 +203,8 @@ def perm_above(accessing_obj, accessed_obj, *args, **kwargs):
this function has no meaning and returns False.
"""
kwargs["_greater_than"] = True
return perm(accessing_obj,accessed_obj, *args, **kwargs)
return perm(accessing_obj, accessed_obj, *args, **kwargs)
def pperm(accessing_obj, accessed_obj, *args, **kwargs):
"""
@ -209,6 +220,7 @@ def pperm(accessing_obj, accessed_obj, *args, **kwargs):
"""
return perm(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
def pperm_above(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only allow Player objects with a permission *higher* in the permission
@ -218,6 +230,7 @@ def pperm_above(accessing_obj, accessed_obj, *args, **kwargs):
"""
return perm_above(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
def dbref(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -238,16 +251,19 @@ def dbref(accessing_obj, accessed_obj, *args, **kwargs):
return dbref == accessing_obj.dbid
return False
def pdbref(accessing_obj, accessed_obj, *args, **kwargs):
"""
Same as dbref, but making sure accessing_obj is a player.
"""
return dbref(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
def id(accessing_obj, accessed_obj, *args, **kwargs):
"Alias to dbref"
return dbref(accessing_obj, accessed_obj, *args, **kwargs)
def pid(accessing_obj, accessed_obj, *args, **kwargs):
"Alias to dbref, for Players"
return dbref(_to_player(accessing_obj), accessed_obj, *args, **kwargs)
@ -262,6 +278,7 @@ CF_MAPPING = {'eq': lambda val1, val2: val1 == val2 or int(val1) == int(val2),
'ne': lambda val1, val2: int(val1) != int(val2),
'default': lambda val1, val2: False}
def attr(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -297,22 +314,26 @@ def attr(accessing_obj, accessed_obj, *args, **kwargs):
try:
return CF_MAPPING.get(typ, 'default')(val1, val2)
except Exception:
# this might happen if we try to compare two things that cannot be compared
# this might happen if we try to compare two things
# that cannot be compared
return False
# first, look for normal properties on the object trying to gain access
if hasattr(accessing_obj, attrname):
if value:
return valcompare(str(getattr(accessing_obj, attrname)), value, compare)
return bool(getattr(accessing_obj, attrname)) # will return Fail on False value etc
# will return Fail on False value etc
return bool(getattr(accessing_obj, attrname))
# check attributes, if they exist
if (hasattr(accessing_obj, 'attributes') and accessing_obj.attributes.has(attrname)):
if value:
return (hasattr(accessing_obj, 'attributes')
and valcompare(accessing_obj.attributes.get(attrname), value, compare))
return bool(accessing_obj.attributes.get(attrname)) # fails on False/None values
# fails on False/None values
return bool(accessing_obj.attributes.get(attrname))
return False
def objattr(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -328,6 +349,7 @@ def objattr(accessing_obj, accessed_obj, *args, **kwargs):
if hasattr(accessing_obj, "obj"):
return attr(accessing_obj.obj, accessed_obj, *args, **kwargs)
def locattr(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -350,6 +372,7 @@ def attr_eq(accessing_obj, accessed_obj, *args, **kwargs):
"""
return attr(accessing_obj, accessed_obj, *args, **kwargs)
def attr_gt(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -357,7 +380,9 @@ def attr_gt(accessing_obj, accessed_obj, *args, **kwargs):
Only true if access_obj's attribute > the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare':'gt'})
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'gt'})
def attr_ge(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -365,7 +390,9 @@ def attr_ge(accessing_obj, accessed_obj, *args, **kwargs):
Only true if access_obj's attribute >= the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare':'ge'})
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ge'})
def attr_lt(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -373,7 +400,9 @@ def attr_lt(accessing_obj, accessed_obj, *args, **kwargs):
Only true if access_obj's attribute < the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare':'lt'})
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'lt'})
def attr_le(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -381,7 +410,9 @@ def attr_le(accessing_obj, accessed_obj, *args, **kwargs):
Only true if access_obj's attribute <= the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare':'le'})
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'le'})
def attr_ne(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
@ -389,18 +420,22 @@ def attr_ne(accessing_obj, accessed_obj, *args, **kwargs):
Only true if access_obj's attribute != the value given.
"""
return attr(accessing_obj, accessed_obj, *args, **{'compare':'ne'})
return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ne'})
def holds(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
holds() # checks if accessed_obj or accessed_obj.obj is held by accessing_obj
holds(key/dbref) # checks if accessing_obj holds an object with given key/dbref
holds(attrname, value) # checks if accessing_obj holds an object with the given attrname and value
holds() checks if accessed_obj or accessed_obj.obj
is held by accessing_obj
holds(key/dbref) checks if accessing_obj holds an object
with given key/dbref
holds(attrname, value) checks if accessing_obj holds an
object with the given attrname and value
This is passed if accessed_obj is carried by accessing_obj (that is,
accessed_obj.location == accessing_obj), or if accessing_obj itself holds an
object matching the given key.
accessed_obj.location == accessing_obj), or if accessing_obj itself holds
an object matching the given key.
"""
try:
# commands and scripts don't have contents, so we are usually looking
@ -412,6 +447,7 @@ def holds(accessing_obj, accessed_obj, *args, **kwargs):
contents = accessing_obj.obj.contents
except AttributeError:
return False
def check_holds(objid):
# helper function. Compares both dbrefs and keys/aliases.
objid = str(objid)
@ -449,9 +485,11 @@ def superuser(*args, **kwargs):
"""
return False
def serversetting(accessing_obj, accessed_obj, *args, **kwargs):
"""
Only returns true if the Evennia settings exists, alternatively has a certain value.
Only returns true if the Evennia settings exists, alternatively has
a certain value.
Usage:
serversetting(IRC_ENABLED)

View file

@ -67,14 +67,14 @@ Here, the lock-function perm() will be called with the string
'Builders' (accessing_obj and accessed_obj are added automatically,
you only need to add the args/kwargs, if any).
If we wanted to make sure the accessing object was BOTH a Builders and a GoodGuy, we
could use AND:
If we wanted to make sure the accessing object was BOTH a Builders and a
GoodGuy, we could use AND:
'edit:perm(Builders) AND perm(GoodGuy)'
To allow EITHER Builders and GoodGuys, we replace AND with OR. perm() is just one example,
the lock function can do anything and compare any properties of the calling object to
decide if the lock is passed or not.
To allow EITHER Builders and GoodGuys, we replace AND with OR. perm() is just
one example, the lock function can do anything and compare any properties of
the calling object to decide if the lock is passed or not.
'lift:attrib(very_strong) AND NOT attrib(bad_back)'
@ -89,7 +89,8 @@ object would do something like this:
if not target_obj.lockhandler.has_perm(caller, 'edit'):
caller.msg("Sorry, you cannot edit that.")
All objects also has a shortcut called 'access' that is recommended to use instead:
All objects also has a shortcut called 'access' that is recommended to
use instead:
if not target_obj.access(caller, 'edit'):
caller.msg("Sorry, you cannot edit that.")
@ -104,13 +105,15 @@ to any other identifier you can use.
"""
import re, inspect
import re
import inspect
from django.conf import settings
from src.utils import logger, utils
from django.utils.translation import ugettext as _
__all__ = ("LockHandler", "LockException")
#
# Exception class
#
@ -119,6 +122,7 @@ class LockException(Exception):
"raised during an error in a lock."
pass
#
# Cached lock functions
#
@ -186,15 +190,16 @@ class LockHandler(object):
"""
Helper function. This is normally only called when the
lockstring is cached and does preliminary checking. locks are
stored as a string 'atype:[NOT] lock()[[ AND|OR [NOT] lock()[...]];atype...
stored as a string
'atype:[NOT] lock()[[ AND|OR [NOT] lock()[...]];atype...
"""
locks = {}
if not storage_lockstring:
return locks
duplicates = 0
elist = [] # errors
wlist = [] # warnings
elist = [] # errors
wlist = [] # warnings
for raw_lockstring in storage_lockstring.split(';'):
lock_funcs = []
try:
@ -234,7 +239,8 @@ class LockHandler(object):
{"access_type":access_type, "source":locks[access_type][2], "goal":raw_lockstring}))
locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring)
if wlist and self.log_obj:
# a warning text was set, it's not an error, so only report if log_obj is available.
# a warning text was set, it's not an error, so only report
# if log_obj is available.
self._log_error("\n".join(wlist))
if elist:
# an error text was set, raise exception.
@ -252,10 +258,12 @@ class LockHandler(object):
def cache_lock_bypass(self, obj):
"""
We cache superuser bypass checks here for efficiency. This needs to be re-run when a player is assigned to a character.
We need to grant access to superusers. We need to check both directly on the object (players), through obj.player and using the
get_player method (this sits on serversessions, in some rare cases where a check is done
before the login process has yet been fully finalized)
We cache superuser bypass checks here for efficiency. This needs to
be re-run when a player is assigned to a character.
We need to grant access to superusers. We need to check both directly
on the object (players), through obj.player and using the get_player()
method (this sits on serversessions, in some rare cases where a
check is done before the login process has yet been fully finalized)
"""
self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser
@ -308,7 +316,7 @@ class LockHandler(object):
def get(self, access_type=None):
"get the full lockstring or the lockstring of a particular access type."
if access_type:
return self.locks.get(access_type, ["","",""])[2]
return self.locks.get(access_type, ["", "", ""])[2]
return str(self)
def delete(self, access_type):
@ -342,7 +350,7 @@ class LockHandler(object):
access_type - the type of access wanted
default - if no suitable lock type is found, use this
no_superuser_bypass - don't use this unless you really, really need to,
it makes supersusers susceptible to the lock check.
it makes supersusers susceptible to the lock check.
A lock is executed in the follwoing way:
@ -403,9 +411,11 @@ class LockHandler(object):
locks = self._parse_lockstring(lockstring)
for access_type in locks:
evalstring, func_tup, raw_string = locks[access_type]
true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1], **tup[2]) for tup in func_tup)
true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1],**tup[2])
for tup in func_tup)
return eval(evalstring % true_false)
def _test():
# testing

View file

@ -15,7 +15,7 @@ except ImportError:
from django.test import TestCase
from django.conf import settings
from src.locks import lockhandler, lockfuncs
from src.locks import lockfuncs
from src.utils import create
#------------------------------------------------------------

View file

@ -1,140 +1,138 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from django import forms
from django.conf import settings
from django.contrib import admin
from src.typeclasses.models import Attribute, Tag
from src.objects.models import ObjectDB
class AttributeInline(admin.TabularInline):
# This class is currently not used, because PickleField objects are not editable.
# It's here for us to ponder making a way that allows them to be edited.
model = Attribute
fields = ('db_key', 'db_value')
extra = 0
class TagInline(admin.TabularInline):
model = ObjectDB.db_tags.through
raw_id_fields = ('tag',)
extra = 0
class TagAdmin(admin.ModelAdmin):
fields = ('db_key', 'db_category', 'db_data')
class ObjectCreateForm(forms.ModelForm):
"This form details the look of the fields"
class Meta:
model = ObjectDB
db_key = forms.CharField(label="Name/Key",
widget=forms.TextInput(attrs={'size':'78'}),
help_text="Main identifier, like 'apple', 'strong guy', 'Elizabeth' etc. If creating a Character, check so the name is unique among characters!",)
db_typeclass_path = forms.CharField(label="Typeclass",
initial=settings.BASE_OBJECT_TYPECLASS,
widget=forms.TextInput(attrs={'size':'78'}),
help_text="This defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass. If you are creating a Character you should use the typeclass defined by settings.BASE_CHARACTER_TYPECLASS or one derived from that.")
#db_permissions = forms.CharField(label="Permissions",
# initial=settings.PERMISSION_PLAYER_DEFAULT,
# required=False,
# widget=forms.TextInput(attrs={'size':'78'}),
# help_text="a comma-separated list of text strings checked by certain locks. They are mainly of use for Character objects. Character permissions overload permissions defined on a controlling Player. Most objects normally don't have any permissions defined.")
db_cmdset_storage = forms.CharField(label="CmdSet",
initial="",
required=False,
widget=forms.TextInput(attrs={'size':'78'}),
help_text="Most non-character objects don't need a cmdset and can leave this field blank.")
raw_id_fields = ('db_destination', 'db_location', 'db_home')
class ObjectEditForm(ObjectCreateForm):
"Form used for editing. Extends the create one with more fields"
db_lock_storage = forms.CharField(label="Locks",
required=False,
widget=forms.Textarea(attrs={'cols':'100', 'rows':'2'}),
help_text="In-game lock definition string. If not given, defaults will be used. This string should be on the form <i>type:lockfunction(args);type2:lockfunction2(args);...")
class ObjectDBAdmin(admin.ModelAdmin):
list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path')
list_display_links = ('id', 'db_key')
ordering = ['db_player', 'db_typeclass_path', 'id']
search_fields = ['^db_key', 'db_typeclass_path']
raw_id_fields = ('db_destination', 'db_location', 'db_home')
save_as = True
save_on_top = True
list_select_related = True
list_filter = ('db_typeclass_path',)
#list_filter = ('db_permissions', 'db_typeclass_path')
# editing fields setup
form = ObjectEditForm
fieldsets = (
(None, {
'fields': (('db_key','db_typeclass_path'), ('db_lock_storage', ),
('db_location', 'db_home'), 'db_destination','db_cmdset_storage'
)}),
)
#fieldsets = (
# (None, {
# 'fields': (('db_key','db_typeclass_path'), ('db_permissions', 'db_lock_storage'),
# ('db_location', 'db_home'), 'db_destination','db_cmdset_storage'
# )}),
# )
#deactivated temporarily, they cause empty objects to be created in admin
inlines = [TagInline]
# Custom modification to give two different forms wether adding or not.
add_form = ObjectCreateForm
add_fieldsets = (
(None, {
'fields': (('db_key','db_typeclass_path'),
('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage'
)}),
)
#add_fieldsets = (
# (None, {
# 'fields': (('db_key','db_typeclass_path'), 'db_permissions',
# ('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage'
# )}),
# )
def get_fieldsets(self, request, obj=None):
if not obj:
return self.add_fieldsets
return super(ObjectDBAdmin, self).get_fieldsets(request, obj)
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during creation
"""
defaults = {}
if obj is None:
defaults.update({
'form': self.add_form,
'fields': admin.util.flatten_fieldsets(self.add_fieldsets),
})
defaults.update(kwargs)
return super(ObjectDBAdmin, self).get_form(request, obj, **defaults)
def save_model(self, request, obj, form, change):
obj.save()
if not change:
# adding a new object
obj = obj.typeclass
obj.basetype_setup()
obj.basetype_posthook_setup()
obj.at_object_creation()
obj.at_init()
admin.site.register(ObjectDB, ObjectDBAdmin)
admin.site.register(Tag, TagAdmin)
#
# This sets up how models are displayed
# in the web admin interface.
#
from django import forms
from django.conf import settings
from django.contrib import admin
from src.typeclasses.models import Attribute, Tag
from src.objects.models import ObjectDB
class AttributeInline(admin.TabularInline):
# This class is currently not used, because PickleField objects are
# not editable. It's here for us to ponder making a way that allows
# them to be edited.
model = Attribute
fields = ('db_key', 'db_value')
extra = 0
class TagInline(admin.TabularInline):
model = ObjectDB.db_tags.through
raw_id_fields = ('tag',)
extra = 0
class TagAdmin(admin.ModelAdmin):
fields = ('db_key', 'db_category', 'db_data')
class ObjectCreateForm(forms.ModelForm):
"This form details the look of the fields"
class Meta:
model = ObjectDB
db_key = forms.CharField(label="Name/Key",
widget=forms.TextInput(attrs={'size': '78'}),
help_text="Main identifier, like 'apple', 'strong guy', 'Elizabeth' etc. If creating a Character, check so the name is unique among characters!",)
db_typeclass_path = forms.CharField(label="Typeclass",
initial=settings.BASE_OBJECT_TYPECLASS,
widget=forms.TextInput(attrs={'size': '78'}),
help_text="This defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass. If you are creating a Character you should use the typeclass defined by settings.BASE_CHARACTER_TYPECLASS or one derived from that.")
db_cmdset_storage = forms.CharField(label="CmdSet",
initial="",
required=False,
widget=forms.TextInput(attrs={'size': '78'}),
help_text="Most non-character objects don't need a cmdset and can leave this field blank.")
raw_id_fields = ('db_destination', 'db_location', 'db_home')
class ObjectEditForm(ObjectCreateForm):
"Form used for editing. Extends the create one with more fields"
db_lock_storage = forms.CharField(label="Locks",
required=False,
widget=forms.Textarea(attrs={'cols':'100', 'rows':'2'}),
help_text="In-game lock definition string. If not given, defaults will be used. This string should be on the form <i>type:lockfunction(args);type2:lockfunction2(args);...")
class ObjectDBAdmin(admin.ModelAdmin):
list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path')
list_display_links = ('id', 'db_key')
ordering = ['db_player', 'db_typeclass_path', 'id']
search_fields = ['^db_key', 'db_typeclass_path']
raw_id_fields = ('db_destination', 'db_location', 'db_home')
save_as = True
save_on_top = True
list_select_related = True
list_filter = ('db_typeclass_path',)
#list_filter = ('db_permissions', 'db_typeclass_path')
# editing fields setup
form = ObjectEditForm
fieldsets = (
(None, {
'fields': (('db_key','db_typeclass_path'), ('db_lock_storage', ),
('db_location', 'db_home'), 'db_destination','db_cmdset_storage'
)}),
)
#fieldsets = (
# (None, {
# 'fields': (('db_key','db_typeclass_path'), ('db_permissions', 'db_lock_storage'),
# ('db_location', 'db_home'), 'db_destination','db_cmdset_storage'
# )}),
# )
#deactivated temporarily, they cause empty objects to be created in admin
inlines = [TagInline]
# Custom modification to give two different forms wether adding or not.
add_form = ObjectCreateForm
add_fieldsets = (
(None, {
'fields': (('db_key','db_typeclass_path'),
('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage'
)}),
)
#add_fieldsets = (
# (None, {
# 'fields': (('db_key','db_typeclass_path'), 'db_permissions',
# ('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage'
# )}),
# )
def get_fieldsets(self, request, obj=None):
if not obj:
return self.add_fieldsets
return super(ObjectDBAdmin, self).get_fieldsets(request, obj)
def get_form(self, request, obj=None, **kwargs):
"""
Use special form during creation
"""
defaults = {}
if obj is None:
defaults.update({
'form': self.add_form,
'fields': admin.util.flatten_fieldsets(self.add_fieldsets),
})
defaults.update(kwargs)
return super(ObjectDBAdmin, self).get_form(request, obj, **defaults)
def save_model(self, request, obj, form, change):
obj.save()
if not change:
# adding a new object
obj = obj.typeclass
obj.basetype_setup()
obj.basetype_posthook_setup()
obj.at_object_creation()
obj.at_init()
admin.site.register(ObjectDB, ObjectDBAdmin)
admin.site.register(Tag, TagAdmin)

View file

@ -21,6 +21,7 @@ _ATTR = None
_AT_MULTIMATCH_INPUT = utils.variable_from_module(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1))
class ObjectManager(TypedObjectManager):
"""
This ObjectManager implementes methods for searching
@ -43,7 +44,8 @@ class ObjectManager(TypedObjectManager):
get_objs_with_db_property_match
get_objs_with_key_or_alias
get_contents
object_search (interface to many of the above methods, equivalent to ev.search_object)
object_search (interface to many of the above methods,
equivalent to ev.search_object)
copy_object
"""
@ -93,8 +95,8 @@ class ObjectManager(TypedObjectManager):
@returns_typeclass_list
def get_objs_with_attr(self, attribute_name, candidates=None):
"""
Returns all objects having the given attribute_name defined at all. Location
should be a valid location object.
Returns all objects having the given attribute_name defined at all.
Location should be a valid location object.
"""
cand_restriction = candidates != None and Q(objattribute__db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q()
return list(self.filter(cand_restriction & Q(objattribute__db_key=attribute_name)))
@ -178,10 +180,11 @@ class ObjectManager(TypedObjectManager):
return self.filter(db_location=location).exclude(exclude_restriction)
@returns_typeclass_list
def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None, typeclasses=None):
def get_objs_with_key_or_alias(self, ostring, exact=True,
candidates=None, typeclasses=None):
"""
Returns objects based on key or alias match. Will also do fuzzy matching based on
the utils.string_partial_matching function.
Returns objects based on key or alias match. Will also do fuzzy
matching based on the utils.string_partial_matching function.
candidates - list of candidate objects to restrict on
typeclasses - list of typeclass path strings to restrict on
"""
@ -191,7 +194,8 @@ class ObjectManager(TypedObjectManager):
else:
return []
if is_iter(candidates) and not len(candidates):
# if candidates is an empty iterable there can be no matches. Exit early.
# if candidates is an empty iterable there can be no matches
# Exit early.
return []
# build query objects
@ -231,33 +235,42 @@ class ObjectManager(TypedObjectManager):
candidates=None,
exact=True):
"""
Search as an object globally or in a list of candidates and return results. The result is always an Object.
Always returns a list.
Search as an object globally or in a list of candidates and return
results. The result is always an Object. Always returns a list.
Arguments:
searchdata: (str or obj) The entity to match for. This is usually a key string but may also be an object itself.
By default (if not attribute_name is set), this will search object.key and object.aliases in order. Can also
be on the form #dbref, which will, if exact=True be matched against primary key.
attribute_name: (str): Use this named ObjectAttribute to match searchdata against, instead
of the defaults. If this is the name of a database field (with or without the db_ prefix), that
will be matched too.
typeclass (str or TypeClass): restrict matches to objects having this typeclass. This will help
speed up global searches.
candidates (list obj ObjectDBs): If supplied, search will only be performed among the candidates
in this list. A common list of candidates is the contents of the current location searched.
exact (bool): Match names/aliases exactly or partially. Partial matching matches the
beginning of words in the names/aliases, using a matching routine to separate
multiple matches in names with multiple components (so "bi sw" will match
"Big sword"). Since this is more expensive than exact matching, it is
recommended to be used together with the objlist keyword to limit the number
of possibilities. This value has no meaning if searching for attributes/properties.
searchdata: (str or obj) The entity to match for. This is usually a
key string but may also be an object itself. By default (if
not attribute_name is set), this will search object.key and
object.aliases in order. Can also be on the form #dbref,
which will, if exact=True be matched against primary key.
attribute_name: (str): Use this named ObjectAttribute to match
searchdata against, instead of the defaults. If this is
the name of a database field (with or without the db_ prefix),
that will be matched too.
typeclass (str or TypeClass): restrict matches to objects having this
typeclass. This will help speed up global searches.
candidates (list obj ObjectDBs): If supplied, search will only be
performed among the candidates in this list. A common list
of candidates is the contents of the current location
searched.
exact (bool): Match names/aliases exactly or partially. Partial
matching matches the beginning of words in the names/aliases,
using a matching routine to separate multiple matches in
names with multiple components (so "bi sw" will match
"Big sword"). Since this is more expensive than exact
matching, it is recommended to be used together with the
objlist keyword to limit the number of possibilities. This
value has no meaning if searching for attributes/properties.
Returns:
A list of matching objects (or a list with one unique match)
"""
def _searcher(searchdata, candidates, typeclass, exact=False):
"Helper method for searching objects. typeclass is only used for global searching (no candidates)"
"""
Helper method for searching objects. typeclass is only used
for global searching (no candidates)
"""
if attribute_name:
# attribute/property search (always exact).
matches = self.get_objs_with_db_property_value(attribute_name, searchdata, candidates=candidates, typeclasses=typeclass)
@ -285,10 +298,11 @@ class ObjectManager(TypedObjectManager):
# Convenience check to make sure candidates are really dbobjs
candidates = [cand.dbobj for cand in make_iter(candidates) if cand]
if typeclass:
candidates = [cand for cand in candidates if _GA(cand, "db_typeclass_path") in typeclass]
candidates = [cand for cand in candidates
if _GA(cand, "db_typeclass_path") in typeclass]
dbref = not attribute_name and exact and self.dbref(searchdata)
if dbref != None:
if dbref is not None:
# Easiest case - dbref matching (always exact)
dbref_match = self.dbref_search(dbref)
if dbref_match:
@ -299,17 +313,19 @@ class ObjectManager(TypedObjectManager):
# Search through all possibilities.
match_number = None
# always run first check exact - we don't want partial matches if on the form of 1-keyword etc.
# always run first check exact - we don't want partial matches
# if on the form of 1-keyword etc.
matches = _searcher(searchdata, candidates, typeclass, exact=True)
if not matches:
# no matches found - check if we are dealing with N-keyword query - if so, strip it.
# no matches found - check if we are dealing with N-keyword
# query - if so, strip it.
match_number, searchdata = _AT_MULTIMATCH_INPUT(searchdata)
# run search again, with the exactness set by call
if match_number != None or not exact:
if match_number is not None or not exact:
matches = _searcher(searchdata, candidates, typeclass, exact=exact)
# deal with result
if len(matches) > 1 and match_number != None:
if len(matches) > 1 and match_number is not None:
# multiple matches, but a number was given to separate them
try:
matches = [matches[match_number]]
@ -324,11 +340,12 @@ class ObjectManager(TypedObjectManager):
def copy_object(self, original_object, new_key=None,
new_location=None, new_home=None,
new_permissions=None, new_locks=None, new_aliases=None, new_destination=None):
new_permissions=None, new_locks=None,
new_aliases=None, new_destination=None):
"""
Create and return a new object as a copy of the original object. All will
be identical to the original except for the arguments given specifically
to this method.
Create and return a new object as a copy of the original object. All
will be identical to the original except for the arguments given
specifically to this method.
original_object (obj) - the object to make a copy from
new_key (str) - name the copy differently from the original.
@ -358,9 +375,14 @@ class ObjectManager(TypedObjectManager):
# create new object
from src.utils import create
from src.scripts.models import ScriptDB
new_object = create.create_object(typeclass_path, key=new_key, location=new_location,
home=new_home, permissions=new_permissions,
locks=new_locks, aliases=new_aliases, destination=new_destination)
new_object = create.create_object(typeclass_path,
key=new_key,
location=new_location,
home=new_home,
permissions=new_permissions,
locks=new_locks,
aliases=new_aliases,
destination=new_destination)
if not new_object:
return None
@ -381,7 +403,6 @@ class ObjectManager(TypedObjectManager):
return new_object
def clear_all_sessids(self):
"""
Clear the db_sessid field of all objects having also the db_player field

View file

@ -18,17 +18,15 @@ import traceback
from django.db import models
from django.conf import settings
from src.typeclasses.models import TypedObject, TagHandler, NickHandler, AliasHandler, AttributeHandler
from src.server.caches import get_prop_cache, set_prop_cache
from src.typeclasses.typeclass import TypeClass
from src.typeclasses.models import (TypedObject, TagHandler, NickHandler,
AliasHandler, AttributeHandler)
from src.objects.manager import ObjectManager
from src.players.models import PlayerDB
from src.commands.cmdsethandler import CmdSetHandler
from src.commands import cmdhandler
from src.scripts.scripthandler import ScriptHandler
from src.utils import logger
from src.utils.utils import make_iter, to_str, to_unicode, variable_from_module, inherits_from
from src.utils.utils import make_iter, to_str, to_unicode, variable_from_module
from django.utils.translation import ugettext as _
@ -46,6 +44,7 @@ _ME = _("me")
_SELF = _("self")
_HERE = _("here")
#------------------------------------------------------------
#
# ObjectDB
@ -98,9 +97,10 @@ class ObjectDB(TypedObject):
# db_key (also 'name' works), db_typeclass_path, db_date_created,
# db_permissions
#
# These databse fields (including the inherited ones) should normally be set
# using their corresponding wrapper properties, named same as the field, but without
# the db_* prefix (e.g. the db_key field is set with self.key instead). The wrappers
# These databse fields (including the inherited ones) should normally be
# managed by their corresponding wrapper properties, named same as the
# field, but without the db_* prefix (e.g. the db_key field is set with
# self.key instead). The wrappers are created at the metaclass level and
# will automatically save and cache the data more efficiently.
# If this is a character object, the player is connected here.
@ -112,7 +112,7 @@ class ObjectDB(TypedObject):
# The location in the game world. Since this one is likely
# to change often, we set this with the 'location' property
# to transparently handle Typeclassing.
db_location = models.ForeignKey('self', related_name="locations_set",db_index=True,
db_location = models.ForeignKey('self', related_name="locations_set", db_index=True,
blank=True, null=True, verbose_name='game location')
# a safety location, this usually don't change much.
db_home = models.ForeignKey('self', related_name="homes_set",
@ -158,13 +158,19 @@ class ObjectDB(TypedObject):
# seems very sensitive to caching, so leaving it be for now. /Griatch
#@property
def __cmdset_storage_get(self):
"Getter. Allows for value = self.name. Returns a list of cmdset_storage."
"""
Getter. Allows for value = self.name.
Returns a list of cmdset_storage.
"""
storage = _GA(self, "db_cmdset_storage")
# we need to check so storage is not None
return [path.strip() for path in storage.split(',')] if storage else []
return [path.strip() for path in storage.split(',')] if storage else []
#@cmdset_storage.setter
def __cmdset_storage_set(self, value):
"Setter. Allows for self.name = value. Stores as a comma-separated string."
"""
Setter. Allows for self.name = value.
Stores as a comma-separated string.
"""
_SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value)))
_GA(self, "save")()
#@cmdset_storage.deleter
@ -236,14 +242,13 @@ class ObjectDB(TypedObject):
if exi.destination]
exits = property(__exits_get)
#
# Main Search method
#
def search(self, searchdata,
global_search=False,
use_nicks=True, # should this default to off?
use_nicks=True, # should this default to off?
typeclass=None,
location=None,
attribute_name=None,
@ -259,33 +264,46 @@ class ObjectDB(TypedObject):
Inputs:
searchdata (str or obj): Primary search criterion. Will be matched against object.key (with object.aliases second)
unless the keyword attribute_name specifies otherwise. Special strings:
#<num> - search by unique dbref. This is always a global search.
searchdata (str or obj): Primary search criterion. Will be matched
against object.key (with object.aliases second) unless
the keyword attribute_name specifies otherwise.
Special strings:
#<num> - search by unique dbref. This is always
a global search.
me,self - self-reference to this object
<num>-<string> - can be used to differentiate between multiple same-named matches
global_search (bool): Search all objects globally. This is overruled by "location" keyword.
use_nicks (bool): Use nickname-replace (nicktype "object") on the search string
typeclass (str or Typeclass, or list of either): Limit search only to Objects with this typeclass. May
be a list of typeclasses for a broader search.
location (Object): Specify a location to search, if different from the self's given location
plus its contents. This can also be a list of locations.
attribute_name (str): Define which property to search. If set, no key+alias search will be performed. This can be used to
search database fields (db_ will be automatically appended), and if that fails, it will try to
return objects having Attributes with this name and value equal to searchdata. A special
use is to search for "key" here if you want to do a key-search without including aliases.
quiet (bool) - don't display default error messages - return multiple matches as a list and
no matches as None. If not set (default), will echo error messages and return None.
exact (bool) - if unset (default) - prefers to match to beginning of string rather than not matching
at all. If set, requires exact mathing of entire string.
<num>-<string> - can be used to differentiate
between multiple same-named matches
global_search (bool): Search all objects globally. This is overruled
by "location" keyword.
use_nicks (bool): Use nickname-replace (nicktype "object") on the
search string
typeclass (str or Typeclass, or list of either): Limit search only
to Objects with this typeclass. May be a list of typeclasses
for a broader search.
location (Object): Specify a location to search, if different from the
self's given location plus its contents. This can also
be a list of locations.
attribute_name (str): Define which property to search. If set, no
key+alias search will be performed. This can be used to
search database fields (db_ will be automatically
appended), and if that fails, it will try to return
objects having Attributes with this name and value
equal to searchdata. A special use is to search for
"key" here if you want to do a key-search without
including aliases.
quiet (bool) - don't display default error messages - return multiple
matches as a list and no matches as None. If not
set (default), will echo error messages and return None.
exact (bool) - if unset (default) - prefers to match to beginning of
string rather than not matching at all. If set, requires
exact mathing of entire string.
Returns:
quiet=False (default):
no match or multimatch:
auto-echoes errors to self.msg, then returns None
(results are handled by modules set by settings.SEARCH_AT_RESULT
and settings.SEARCH_AT_MULTIMATCH_INPUT)
auto-echoes errors to self.msg, then returns None
(results are handled by settings.SEARCH_AT_RESULT
and settings.SEARCH_AT_MULTIMATCH_INPUT)
match:
a unique object match
quiet=True:
@ -315,8 +333,10 @@ class ObjectDB(TypedObject):
break
candidates=None
if global_search or (is_string and searchdata.startswith("#") and len(searchdata) > 1 and searchdata[1:].isdigit()):
# only allow exact matching if searching the entire database or unique #dbrefs
if(global_search or (is_string and searchdata.startswith("#") and
len(searchdata) > 1 and searchdata[1:].isdigit())):
# only allow exact matching if searching the entire database
# or unique #dbrefs
exact = True
elif location:
# location(s) were given
@ -324,13 +344,15 @@ class ObjectDB(TypedObject):
for obj in make_iter(location):
candidates.extend([o.dbobj for o in obj.contents])
else:
# local search. Candidates are self.contents, self.location and self.location.contents
# local search. Candidates are self.contents, self.location
# and self.location.contents
location = self.location
candidates = self.contents
if location:
candidates = candidates + [location] + location.contents
else:
candidates.append(self) # normally we are included in location.contents
# normally we are included in location.contents
candidates.append(self)
# db manager expects database objects
candidates = [obj.dbobj for obj in candidates]
@ -360,23 +382,24 @@ class ObjectDB(TypedObject):
def execute_cmd(self, raw_string, sessid=None):
"""
Do something as this object. This method is a copy of the execute_cmd method on the
session. This is never called normally, it's only used when wanting specifically to
let an object be the caller of a command. It makes use of nicks of eventual connected
players as well.
Do something as this object. This method is a copy of the execute_
cmd method on the session. This is never called normally, it's only
used when wanting specifically to let an object be the caller of a
command. It makes use of nicks of eventual connected players as well.
Argument:
raw_string (string) - raw command input
sessid (int) - optional session id to return results to
Returns Deferred - this is an asynchronous Twisted object that will
not fire until the command has actually finished executing. To overload
this one needs to attach callback functions to it, with addCallback(function).
This function will be called with an eventual return value from the command
execution.
not fire until the command has actually finished executing. To
overload this one needs to attach callback functions to it, with
addCallback(function). This function will be called with an
eventual return value from the command execution.
This return is not used at all by Evennia by default, but might be useful
for coders intending to implement some sort of nested command structure.
This return is not used at all by Evennia by default, but might
be useful for coders intending to implement some sort of nested
command structure.
"""
# nick replacement - we require full-word matching.
@ -384,7 +407,8 @@ class ObjectDB(TypedObject):
raw_string = to_unicode(raw_string)
raw_list = raw_string.split(None)
raw_list = [" ".join(raw_list[:i+1]) for i in range(len(raw_list)) if raw_list[:i+1]]
raw_list = [" ".join(raw_list[:i + 1]) for i in range(len(raw_list))
if raw_list[:i + 1]]
# fetch the nick data efficiently
nicks = self.db_attributes.filter(db_category__in=("nick_inputline", "nick_channel"))
if self.has_player:
@ -416,7 +440,6 @@ class ObjectDB(TypedObject):
if "data" in kwargs:
# deprecation warning
from src.utils import logger
logger.log_depmsg("ObjectDB.msg(): 'data'-dict keyword is deprecated. Use **kwargs instead.")
data = kwargs.pop("data")
if isinstance(data, dict):
@ -437,7 +460,8 @@ class ObjectDB(TypedObject):
"""
Emits something to all objects inside an object.
exclude is a list of objects not to send to. See self.msg() for more info.
exclude is a list of objects not to send to. See self.msg() for
more info.
"""
contents = _GA(self, "contents")
if exclude:
@ -454,22 +478,27 @@ class ObjectDB(TypedObject):
Moves this object to a new location. Note that if <destination> is an
exit object (i.e. it has "destination"!=None), the move_to will
happen to this destination and -not- into the exit object itself, unless
use_destination=False. Note that no lock checks are done by this function,
such things are assumed to have been handled before calling move_to.
use_destination=False. Note that no lock checks are done by this
function, such things are assumed to have been handled before calling
move_to.
destination: (Object) Reference to the object to move to. This
can also be an exit object, in which case the destination
property is used as destination.
quiet: (bool) If true, don't emit left/arrived messages.
emit_to_obj: (Object) object to receive error messages
use_destination (bool): Default is for objects to use the "destination" property
of destinations as the target to move to. Turning off this
keyword allows objects to move "inside" exit objects.
to_none - allow destination to be None. Note that no hooks are run when moving
to a None location. If you want to run hooks, run them manually.
use_destination (bool): Default is for objects to use the "destination"
property of destinations as the target to move to.
Turning off this keyword allows objects to move
"inside" exit objects.
to_none - allow destination to be None. Note that no hooks are run when
moving to a None location. If you want to run hooks,
run them manually (and make sure they can manage None
locations).
Returns True/False depending on if there were problems with the move. This method
may also return various error messages to the emit_to_obj.
Returns True/False depending on if there were problems with the move.
This method may also return various error messages to the
emit_to_obj.
"""
def logerr(string=""):
trc = traceback.format_exc()
@ -633,11 +662,13 @@ class ObjectDB(TypedObject):
def copy(self, new_key=None):
"""
Makes an identical copy of this object. If you want to customize the copy by
changing some settings, use ObjectDB.object.copy_object() directly.
Makes an identical copy of this object. If you want to customize the
copy by changing some settings, use ObjectDB.object.copy_object()
directly.
new_key (string) - new key/name of copied object. If new_key is not specified, the copy will be named
<old_key>_copy by default.
new_key (string) - new key/name of copied object. If new_key is not
specified, the copy will be named <old_key>_copy
by default.
Returns: Object (copy of this one)
"""
def find_clone_key():
@ -650,7 +681,8 @@ class ObjectDB(TypedObject):
key = _GA(self, "key")
num = 1
for obj in (obj for obj in self.location.contents
if obj.key.startswith(key) and obj.key.lstrip(key).isdigit()):
if obj.key.startswith(key) and
obj.key.lstrip(key).isdigit()):
num += 1
return "%s%03i" % (key, num)
new_key = new_key or find_clone_key()
@ -705,7 +737,7 @@ class ObjectDB(TypedObject):
_GA(self, "clear_exits")()
# Clear out any non-exit objects located within the object
_GA(self, "clear_contents")()
old_loc = _GA(self, "location")
#old_loc = _GA(self, "location")
# Perform the deletion of the object
super(ObjectDB, self).delete()
# clear object's old location's content cache of this object

View file

@ -25,6 +25,7 @@ _GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
#
# Base class to inherit from.
#
@ -55,41 +56,53 @@ class Object(TypeClass):
key (string) - name of object
name (string) - same as key
aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings.
aliases (list of strings) - aliases to the object. Will be saved to
database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class
typeclass (Object, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch.
dbobj (Object, read-only) - link to database model. dbobj.typeclass
points back to this class
typeclass (Object, read-only) - this links back to this class as an
identified only. Use self.swap_typeclass() to switch.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
player (Player) - controlling player (if any, only set together with sessid below)
sessid (int, read-only) - session id (if any, only set together with player above)
player (Player) - controlling player (if any, only set together with
sessid below)
sessid (int, read-only) - session id (if any, only set together with
player above)
location (Object) - current location. Is None if this is a room
home (Object) - safety start-location
sessions (list of Sessions, read-only) - returns all sessions connected to this object
sessions (list of Sessions, read-only) - returns all sessions
connected to this object
has_player (bool, read-only)- will only return *connected* players
contents (list of Objects, read-only) - returns all objects inside this object (including exits)
exits (list of Objects, read-only) - returns all exits from this object, if any
contents (list of Objects, read-only) - returns all objects inside
this object (including exits)
exits (list of Objects, read-only) - returns all exits from this
object, if any
destination (Object) - only set if this object is an exit.
is_superuser (bool, read-only) - True/False if this user is a superuser
* Handlers available
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
db - attribute-handler: store/retrieve database attributes on this
self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not
create a database entry when storing data
scripts - script-handler. Add new scripts to object with scripts.add()
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
nicks - nick-handler. New nicks with nicks.add().
* Helper methods (see src.objects.objects.py for full headers)
search(ostring, global_search=False, global_dbref=False, attribute_name=None,
use_nicks=True, location=None, ignore_errors=False, player=False)
search(ostring, global_search=False, global_dbref=False,
attribute_name=None, use_nicks=True, location=None,
ignore_errors=False, player=False)
execute_cmd(raw_string)
msg(message, **kwargs)
msg_contents(message, exclude=None, from_obj=None, **kwargs)
move_to(destination, quiet=False, emit_to_obj=None, use_destination=True, to_none=False)
move_to(destination, quiet=False, emit_to_obj=None,
use_destination=True, to_none=False)
copy(new_key=None)
delete()
is_typeclass(typeclass, exact=False)
@ -99,42 +112,73 @@ class Object(TypeClass):
* Hook methods
basetype_setup() - only called once, used for behind-the-scenes setup. Normally not modified.
basetype_posthook_setup() - customization in basetype, after the object has been created; Normally not modified.
basetype_setup() - only called once, used for behind-the-scenes
setup. Normally not modified.
basetype_posthook_setup() - customization in basetype, after the
object has been created; Normally not modified.
at_object_creation() - only called once, when object is first created. Object customizations go here.
at_object_delete() - called just before deleting an object. If returning False, deletion is aborted. Note that all objects
inside a deleted object are automatically moved to their <home>, they don't need to be removed here.
at_object_creation() - only called once, when object is first created.
Object customizations go here.
at_object_delete() - called just before deleting an object. If
returning False, deletion is aborted. Note that
all objects inside a deleted object are
automatically moved to their <home>, they don't
need to be removed here.
at_init() - called whenever typeclass is cached from memory, at least once every server restart/reload
at_cmdset_get() - this is called just before the command handler requests a cmdset from this objecth
at_pre_puppet(player)- (player-controlled objects only) called just before puppeting
at_post_puppet() - (player-controlled objects only) called just after completing connection player<->object
at_pre_unpuppet() - (player-controlled objects only) called just before un-puppeting
at_post_unpuppet(player) - (player-controlled objects only) called just after disconnecting player<->object link
at_init() called whenever typeclass is cached from
memory, at least once every server restart/reload
at_cmdset_get() - this is called just before the command
handler requests a cmdset from this objecth
at_pre_puppet(player)- (player-controlled objects only) called just
before puppeting
at_post_puppet() - (player-controlled objects only) called just
after completing connection player<->object
at_pre_unpuppet() - (player-controlled objects only) called just
before un-puppeting
at_post_unpuppet(player) (player-controlled objects only) called
just after disconnecting player<->object link
at_server_reload() - called before server is reloaded
at_server_shutdown() - called just before server is fully shut down
at_before_move(destination) - called just before moving object to the destination. If returns False, move is cancelled.
announce_move_from(destination) - called in old location, just before move, if obj.move_to() has quiet=False
announce_move_to(source_location) - called in new location, just after move, if obj.move_to() has quiet=False
at_after_move(source_location) - always called after a move has been successfully performed.
at_object_leave(obj, target_location) - called when an object leaves this object in any fashion
at_object_receive(obj, source_location) - called when this object receives another object
at_before_move(destination) called just before moving
object to the destination. If returns
False, move is cancelled.
announce_move_from(destination) - called in old location, just before
move, if obj.move_to() has
quiet=False
announce_move_to(source_location) - called in new location,
just after move, if obj.move_to()
has quiet=False
at_after_move(source_location) - always called after a move
has been successfully performed.
at_object_leave(obj, target_location) - called when an object leaves
this object in any fashion
at_object_receive(obj, source_location) - called when this object
receives another object
at_before_traverse(traversing_object) - (exit-objects only) called just before an object traverses this object
at_after_traverse(traversing_object, source_location) - (exit-objects only) called just after a traversal has happened.
at_failed_traverse(traversing_object) - (exit-objects only) called if traversal fails and property err_traverse is not defined.
at_before_traverse(traversing_object) - (exit-objects only) called
just before an object
traverses this object
at_after_traverse(traversing_object, source_location) - (exit-objects
only) called just after a traversal has happened.
at_failed_traverse(traversing_object) - (exit-objects only) called
if traversal fails and property err_traverse is not defined.
at_msg_receive(self, msg, from_obj=None, data=None) - called when a message (via self.msg()) is sent to this obj.
If returns false, aborts send.
at_msg_send(self, msg, to_obj=None, data=None) - called when this objects sends a message to someone via self.msg().
at_msg_receive(self, msg, from_obj=None, data=None) - called when a
message (via self.msg()) is sent to this obj.
If returns false, aborts send.
at_msg_send(self, msg, to_obj=None, data=None) - called when this
objects sends a message to someone via self.msg().
return_appearance(looker) - describes this object. Used by "look" command by default
at_desc(looker=None) - called by 'look' whenever the appearance is requested.
at_get(getter) - called after object has been picked up. Does not stop pickup.
return_appearance(looker) - describes this object. Used by "look"
command by default
at_desc(looker=None) - called by 'look' whenever the appearance
is requested.
at_get(getter) - called after object has been picked up.
Does not stop pickup.
at_drop(dropper) - called when this object has been dropped.
at_say(speaker, message) - by default, called if an object inside this object speaks
at_say(speaker, message) - by default, called if an object inside
this object speaks
"""
super(Object, self).__init__(dbobj)
@ -159,30 +203,41 @@ class Object(TypeClass):
Inputs:
ostring (str): Primary search criterion. Will be matched against object.key (with object.aliases second)
unless the keyword attribute_name specifies otherwise. Special strings:
#<num> - search by unique dbref. This is always a global search.
ostring (str): Primary search criterion. Will be matched against
object.key (with object.aliases second)
unless the keyword attribute_name specifies otherwise.
Special strings:
#<num> - search by unique dbref. This is always a
global search.
me,self - self-reference to this object
<num>-<string> - can be used to differentiate between multiple same-named matches
global_search (bool): Search all objects globally. This is overruled by "location" keyword.
use_nicks (bool): Use nickname-replace (nicktype "object") on the search string
typeclass (str or Typeclass): Limit search only to Objects with this typeclass. May be a list of typeclasses
for a broader search.
location (Object): Specify a location to search, if different from the self's given location
<num>-<string> - can be used to differentiate between
multiple same-named matches
global_search (bool): Search all objects globally. This is overruled
by "location" keyword.
use_nicks (bool): Use nickname-replace (nicktype "object") on the
search string
typeclass (str or Typeclass): Limit search only to Objects with this
typeclass. May be a list of typeclasses for a
broader search.
location (Object): Specify a location to search, if different from the
self's given location
plus its contents. This can also be a list of locations.
attribute_name (str): Use this named Attribute to match ostring against, instead of object.key.
quiet (bool) - don't display default error messages - return multiple matches as a list and
no matches as None. If not set (default), will echo error messages and return None.
exact (bool) - if unset (default) - prefers to match to beginning of string rather than not matching
at all. If set, requires exact mathing of entire string.
attribute_name (str): Use this named Attribute to match ostring against,
instead of object.key.
quiet (bool) - don't display default error messages - return multiple
matches as a list and no matches as None. If not
set (default), will echo error messages and return None.
exact (bool) - if unset (default) - prefers to match to beginning of
string rather than not matching at all. If set,
requires exact mathing of entire string.
Returns:
quiet=False (default):
no match or multimatch:
auto-echoes errors to self.msg, then returns None
(results are handled by modules set by settings.SEARCH_AT_RESULT
and settings.SEARCH_AT_MULTIMATCH_INPUT)
(results are handled by settings.SEARCH_AT_RESULT
and settings.SEARCH_AT_MULTIMATCH_INPUT)
match:
a unique object match
quiet=True:
@ -209,16 +264,18 @@ class Object(TypeClass):
Argument:
raw_string (string) - raw command input
sessid (int) - id of session executing the command. This sets the sessid property on the command.
sessid (int) - id of session executing the command. This sets the
sessid property on the command.
Returns Deferred - this is an asynchronous Twisted object that will
not fire until the command has actually finished executing. To overload
this one needs to attach callback functions to it, with addCallback(function).
This function will be called with an eventual return value from the command
execution.
not fire until the command has actually finished executing. To
overload this one needs to attach callback functions to it, with
addCallback(function). This function will be called with an
eventual return value from the command execution.
This return is not used at all by Evennia by default, but might be useful
for coders intending to implement some sort of nested command structure.
This return is not used at all by Evennia by default, but might be
useful for coders intending to implement some sort of nested
command structure.
"""
return self.dbobj.execute_cmd(raw_string, sessid=sessid)
@ -233,48 +290,59 @@ class Object(TypeClass):
sessid: optional session target. If sessid=0, the session will
default to self.sessid or from_obj.sessid.
"""
self.dbobj.msg(text=text, **kwargs)#message, from_obj=from_obj, data=data, sessid=0)
self.dbobj.msg(text=text, **kwargs)
def msg_contents(self, text=None, exclude=None, from_obj=None, **kwargs):
"""
Emits something to all objects inside an object.
exclude is a list of objects not to send to. See self.msg() for more info.
exclude is a list of objects not to send to. See self.msg() for
more info.
"""
self.dbobj.msg_contents(text, exclude=exclude, from_obj=from_obj, **kwargs)
self.dbobj.msg_contents(text, exclude=exclude,
from_obj=from_obj, **kwargs)
def move_to(self, destination, quiet=False,
emit_to_obj=None, use_destination=True, to_none=False):
"""
Moves this object to a new location. Note that if <destination> is an
exit object (i.e. it has "destination"!=None), the move_to will
happen to this destination and -not- into the exit object itself, unless
use_destination=False. Note that no lock checks are done by this function,
such things are assumed to have been handled before calling move_to.
happen to this destination and -not- into the exit object itself,
unless use_destination=False. Note that no lock checks are done by
this function, such things are assumed to have been handled before
calling move_to.
destination: (Object) Reference to the object to move to. This
can also be an exit object, in which case the destination
property is used as destination.
quiet: (bool) If true, don't emit left/arrived messages.
emit_to_obj: (Object) object to receive error messages
use_destination (bool): Default is for objects to use the "destination" property
of destinations as the target to move to. Turning off this
keyword allows objects to move "inside" exit objects.
to_none - allow destination to be None. Note that no hooks are run when moving
to a None location. If you want to run hooks, run them manually.
Returns True/False depending on if there were problems with the move. This method
may also return various error messages to the emit_to_obj.
use_destination (bool): Default is for objects to use the "destination"
property of destinations as the target to move to.
Turning off this keyword allows objects to move
"inside" exit objects.
to_none - allow destination to be None. Note that no hooks are run
when moving to a None location. If you want to run hooks, run
them manually (and make sure the hooks can handle a None
location).
Returns True/False depending on if there were problems with the move.
This method may also return various error messages to the
emit_to_obj.
"""
return self.dbobj.move_to(destination, quiet=quiet,
emit_to_obj=emit_to_obj, use_destination=use_destination)
emit_to_obj=emit_to_obj,
use_destination=use_destination)
def copy(self, new_key=None):
"""
Makes an identical copy of this object. If you want to customize the copy by
changing some settings, use ObjectDB.object.copy_object() directly.
Makes an identical copy of this object. If you want to customize the
copy by changing some settings, use ObjectDB.object.copy_object()
directly.
new_key (string) - new key/name of copied object. If new_key is not specified, the copy will be named
new_key (string) - new key/name of copied object. If new_key is not
specified, the copy will be named
<old_key>_copy by default.
Returns: Object (copy of this one)
"""
@ -293,7 +361,6 @@ class Object(TypeClass):
"""
return self.dbobj.delete()
# methods inherited from the typeclass system
def is_typeclass(self, typeclass, exact=False):
@ -347,18 +414,20 @@ class Object(TypeClass):
"""
return self.dbobj.swap_typeclass(new_typeclass, clean_attributes=clean_attributes, no_default=no_default)
return self.dbobj.swap_typeclass(new_typeclass,
clean_attributes=clean_attributes, no_default=no_default)
def access(self, accessing_obj, access_type='read', default=False):
"""
Determines if another object has permission to access this object in whatever way.
Determines if another object has permission to access this object in
whatever way.
accessing_obj (Object)- object trying to access this one
access_type (string) - type of access sought
default (bool) - what to return if no lock of access_type was found
This function will call at_access_success or at_access_failure depending on the
outcome of the access check.
This function will call at_access_success or at_access_failure
depending on the outcome of the access check.
"""
if self.dbobj.access(accessing_obj, access_type=access_type, default=default):
@ -373,12 +442,12 @@ class Object(TypeClass):
This explicitly checks the given string against this object's
'permissions' property without involving any locks.
permstring (string) - permission string that need to match a permission on the object.
permstring (string) - permission string that need to match a
permission on the object.
(example: 'Builders')
"""
return self.dbobj.check_permstring(permstring)
def __eq__(self, other):
"""
Checks for equality against an id string or another object or user.
@ -387,24 +456,23 @@ class Object(TypeClass):
parent doesn't work.
"""
try:
return _GA(_GA(self, "dbobj"),"dbid") == _GA(_GA(other,"dbobj"),"dbid")
return _GA(_GA(self, "dbobj"), "dbid") == _GA(_GA(other, "dbobj"), "dbid")
except AttributeError:
# compare players instead
try:
return _GA(_GA(_GA(self, "dbobj"),"player"),"uid") == _GA(_GA(other, "player"),"uid")
return _GA(_GA(_GA(self, "dbobj"), "player"), "uid") == _GA(_GA(other, "player"), "uid")
except AttributeError:
return False
## hooks called by the game engine
def basetype_setup(self):
"""
This sets up the default properties of an Object,
just before the more general at_object_creation.
You normally don't need to change this unless you change some fundamental
things like names of permission groups.
You normally don't need to change this unless you change some
fundamental things like names of permission groups.
"""
# the default security setup fallback for a generic
# object. Overload in child for a custom setup. Also creation
@ -412,22 +480,25 @@ class Object(TypeClass):
# controller, for example)
dbref = self.dbobj.dbref
self.locks.add(";".join(["control:perm(Immortals)", # edit locks/permissions, delete
"examine:perm(Builders)", # examine properties
"view:all()", # look at object (visibility)
"edit:perm(Wizards)", # edit properties/attributes
"delete:perm(Wizards)", # delete object
"get:all()", # pick up object
"call:true()", # allow to call commands on this object
"tell:perm(Wizards)", # allow emits to this object
"puppet:pid(%s) or perm(Immortals) or pperm(Immortals)" % dbref])) # restricts puppeting of this object
self.locks.add(";".join([
"control:perm(Immortals)", # edit locks/permissions, delete
"examine:perm(Builders)", # examine properties
"view:all()", # look at object (visibility)
"edit:perm(Wizards)", # edit properties/attributes
"delete:perm(Wizards)", # delete object
"get:all()", # pick up object
"call:true()", # allow to call commands on this object
"tell:perm(Wizards)", # allow emits to this object
# restricts puppeting of this object
"puppet:pid(%s) or perm(Immortals) or pperm(Immortals)" % dbref]))
def basetype_posthook_setup(self):
"""
Called once, after basetype_setup and at_object_creation. This should generally not be overloaded unless
you are redefining how a room/exit/object works. It allows for basetype-like setup
after the object is created. An example of this is EXITs, who need to know keys, aliases, locks
etc to set up their exit-cmdsets.
Called once, after basetype_setup and at_object_creation. This should
generally not be overloaded unless you are redefining how a
room/exit/object works. It allows for basetype-like setup after the
object is created. An example of this is EXITs, who need to know keys,
aliases, locks etc to set up their exit-cmdsets.
"""
pass
@ -437,7 +508,6 @@ class Object(TypeClass):
"""
pass
def at_object_delete(self):
"""
Called just before the database object is
@ -502,34 +572,34 @@ class Object(TypeClass):
def at_server_reload(self):
"""
This hook is called whenever the server is shutting down for restart/reboot.
If you want to, for example, save non-persistent properties across a restart,
this is the place to do it.
This hook is called whenever the server is shutting down for
restart/reboot. If you want to, for example, save non-persistent
properties across a restart, this is the place to do it.
"""
pass
def at_server_shutdown(self):
"""
This hook is called whenever the server is shutting down fully (i.e. not for
a restart).
This hook is called whenever the server is shutting down fully
(i.e. not for a restart).
"""
pass
def at_access_success(self, accessing_obj, access_type):
"""
This hook is called whenever accessing_obj succeed a lock check of type access_type
on this object, for whatever reason. The return value of this hook is not used,
the lock will still pass regardless of what this hook does (use lockstring/funcs to tweak
the lock result).
This hook is called whenever accessing_obj succeed a lock check of
type access_type on this object, for whatever reason. The return value
of this hook is not used, the lock will still pass regardless of what
this hook does (use lockstring/funcs to tweak the lock result).
"""
pass
def at_access_failure(self, accessing_obj, access_type):
"""
This hook is called whenever accessing_obj fails a lock check of type access_type
on this object, for whatever reason. The return value of this hook is not used, the
lock will still fail regardless of what this hook does (use lockstring/funcs to tweak the
lock result).
This hook is called whenever accessing_obj fails a lock check of type
access_type on this object, for whatever reason. The return value of
this hook is not used, the lock will still fail regardless of what
this hook does (use lockstring/funcs to tweak the lock result).
"""
pass
@ -588,7 +658,6 @@ class Object(TypeClass):
string = "%s arrives to %s from %s."
self.location.msg_contents(string % (name, loc_name, src_name), exclude=self)
def at_after_move(self, source_location):
"""
Called after move has completed, regardless of quiet mode or not.
@ -598,7 +667,6 @@ class Object(TypeClass):
"""
pass
def at_object_leave(self, moved_obj, target_location):
"""
Called just before an object leaves from inside this object
@ -630,9 +698,9 @@ class Object(TypeClass):
"""
This hook is responsible for handling the actual traversal, normally
by calling traversing_object.move_to(target_location). It is normally
only implemented by Exit objects. If it returns False (usually because move_to
returned False), at_after_traverse below should not be called and
instead at_failed_traverse should be called.
only implemented by Exit objects. If it returns False (usually because
move_to returned False), at_after_traverse below should not be called
and instead at_failed_traverse should be called.
"""
pass
@ -687,7 +755,6 @@ class Object(TypeClass):
"""
pass
# hooks called by the default cmdset.
def return_appearance(self, pobject):
@ -698,7 +765,8 @@ class Object(TypeClass):
if not pobject:
return
# get and identify all objects
visible = (con for con in self.contents if con != pobject and con.access(pobject, "view"))
visible = (con for con in self.contents if con != pobject and
con.access(pobject, "view"))
exits, users, things = [], [], []
for con in visible:
key = con.key
@ -744,6 +812,7 @@ class Object(TypeClass):
dropper - the object which just dropped this object.
"""
pass
def at_say(self, speaker, message):
"""
Called on this object if an object inside this object speaks.
@ -797,12 +866,13 @@ class Character(Object):
def at_pre_puppet(self, player, sessid=None):
"""
This recovers the character again after having been "stoved away" at the unpuppet
This recovers the character again after having been "stoved away"
at the unpuppet
"""
if self.db.prelogout_location:
# try to recover
self.location = self.db.prelogout_location
if self.location == None:
if self.location is None:
# make sure location is never None (home should always exist)
self.location = self.home
if self.location:
@ -824,8 +894,9 @@ class Character(Object):
def at_post_unpuppet(self, player, sessid=None):
"""
We stove away the character when the player goes ooc/logs off, otherwise the character object will
remain in the room also after the player logged off ("headless", so to say).
We stove away the character when the player goes ooc/logs off,
otherwise the character object will remain in the room also after the
player logged off ("headless", so to say).
"""
if self.location: # have to check, in case of multiple connections closing
self.location.msg_contents("%s has left the game." % self.name, exclude=[self])
@ -852,6 +923,7 @@ class Room(Object):
"puppet:false()"])) # would be weird to puppet a room ...
self.location = None
#
# Base Exit object
#
@ -905,7 +977,8 @@ class Exit(Object):
# No shorthand error message. Call hook.
self.obj.at_failed_traverse(self.caller)
# create an exit command. We give the properties here, to always trigger metaclass preparations
# create an exit command. We give the properties here,
# to always trigger metaclass preparations
cmd = ExitCommand(key=exidbobj.db_key.strip().lower(),
aliases=exidbobj.aliases.all(),
locks=str(exidbobj.locks),
@ -944,8 +1017,9 @@ class Exit(Object):
def at_cmdset_get(self):
"""
Called when the cmdset is requested from this object, just before the cmdset is
actually extracted. If no Exit-cmdset is cached, create it now.
Called when the cmdset is requested from this object, just before the
cmdset is actually extracted. If no Exit-cmdset is cached, create
it now.
"""
if self.ndb.exit_reset or not self.cmdset.has_cmdset("_exitset", must_be_default=True):
@ -984,8 +1058,9 @@ class Exit(Object):
def at_failed_traverse(self, traversing_object):
"""
This is called if an object fails to traverse this object for some
reason. It will not be called if the attribute "err_traverse" is defined,
that attribute will then be echoed back instead as a convenient shortcut.
reason. It will not be called if the attribute "err_traverse" is
defined, that attribute will then be echoed back instead as a
convenient shortcut.
(See also hooks at_before_traverse and at_after_traverse).
"""

View file

@ -8,10 +8,12 @@ Runs as part of the Evennia's test suite with 'manage.py test"
Please add new tests to this module as needed.
Guidelines:
A 'test case' is testing a specific component and is defined as a class inheriting from unittest.TestCase.
The test case class can have a method setUp() that creates and sets up the testing environment.
All methods inside the test case class whose names start with 'test' are used as test methods by the runner.
Inside the test methods, special member methods assert*() are used to test the behaviour.
A 'test case' is testing a specific component and is defined as a class
inheriting from unittest.TestCase. The test case class can have a method
setUp() that creates and sets up the testing environment.
All methods inside the test case class whose names start with 'test' are
used as test methods by the runner. Inside the test methods, special member
methods assert*() are used to test the behaviour.
"""
import sys
@ -24,12 +26,10 @@ try:
except ImportError:
import unittest
from django.conf import settings
from src.objects import models, objects
from src.utils import create
from src.commands.default import tests as commandtests
from src.locks import tests as locktests
class TestObjAttrs(TestCase):
"""
Test aspects of ObjAttributes
@ -49,6 +49,7 @@ class TestObjAttrs(TestCase):
# self.assertEqual(self.obj2 ,self.obj1.db.testattr)
# self.assertEqual(self.obj2.location, self.obj1.db.testattr.location)
def suite():
"""
This function is called automatically by the django test runner.

View file

@ -1,12 +1,13 @@
"""
Makes it easier to import by grouping all relevant things already at this level.
Makes it easier to import by grouping all relevant things already at this
level.
You can henceforth import most things directly from src.player
Also, the initiated object manager is available as src.players.manager.
"""
from src.players.player import *
from src.players.player import *
from src.players.models import PlayerDB
manager = PlayerDB.objects

View file

@ -4,16 +4,16 @@
#
from django import forms
from django.db import models
#from django.db import models
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.admin import widgets
#from django.contrib.admin import widgets
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from django.contrib.auth.models import User
#from django.contrib.auth.models import User
from src.players.models import PlayerDB
from src.typeclasses.models import Attribute
from src.utils import logger, create
#from src.typeclasses.models import Attribute
from src.utils import create
# handle the custom User editor
@ -109,6 +109,7 @@ class PlayerForm(forms.ModelForm):
required=False,
help_text="python path to player cmdset class (set in settings.CMDSET_PLAYER by default)")
class PlayerInline(admin.StackedInline):
"Inline creation of Player"
model = PlayerDB
@ -127,6 +128,7 @@ class PlayerInline(admin.StackedInline):
extra = 1
max_num = 1
class PlayerDBAdmin(BaseUserAdmin):
"This is the main creation screen for Users/players"
@ -136,17 +138,17 @@ class PlayerDBAdmin(BaseUserAdmin):
fieldsets = (
(None, {'fields': ('username', 'password', 'email')}),
('Website profile', {'fields': ('first_name', 'last_name'),
'description':"<i>These are not used in the default system.</i>"}),
'description': "<i>These are not used in the default system.</i>"}),
('Website dates', {'fields': ('last_login', 'date_joined'),
'description':'<i>Relevant only to the website.</i>'}),
('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'user_permissions','groups'),
'description': '<i>Relevant only to the website.</i>'}),
('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser',
'user_permissions', 'groups'),
'description': "<i>These are permissions/permission groups for accessing the admin site. They are unrelated to in-game access rights.</i>"}),
('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_lock_storage'),
'description': '<i>These are attributes that are more relevant to gameplay.</i>'}))
#('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_permissions', 'db_lock_storage'),
# 'description': '<i>These are attributes that are more relevant to gameplay.</i>'}))
add_fieldsets = (
(None,
{'fields': ('username', 'password1', 'password2', 'email'),
@ -162,7 +164,7 @@ class PlayerDBAdmin(BaseUserAdmin):
#uname, passwd, email = str(request.POST.get(u"username")), \
# str(request.POST.get(u"password1")), str(request.POST.get(u"email"))
typeclass = str(request.POST.get(u"playerdb_set-0-db_typeclass_path"))
create.create_player("","","",
create.create_player("", "", "",
user=userobj,
typeclass=typeclass,
player_dbobj=userobj)

View file

@ -4,11 +4,12 @@ The managers for the custom Player object and permissions.
import datetime
from django.contrib.auth.models import UserManager
from functools import update_wrapper
#from functools import update_wrapper
from src.typeclasses.managers import returns_typeclass_list, returns_typeclass, TypedObjectManager
from src.utils import logger
#from src.utils import logger
__all__ = ("PlayerManager",)
#
# Player Manager
#
@ -118,8 +119,8 @@ class PlayerManager(TypedObjectManager, UserManager):
def swap_character(self, player, new_character, delete_old_character=False):
"""
This disconnects a player from the current character (if any) and connects
to a new character object.
This disconnects a player from the current character (if any) and
connects to a new character object.
"""

View file

@ -23,15 +23,16 @@ from django.utils.encoding import smart_str
from src.players import manager
from src.scripts.models import ScriptDB
from src.typeclasses.models import TypedObject, TagHandler, NickHandler, AliasHandler, AttributeHandler
from src.typeclasses.models import (TypedObject, TagHandler, NickHandler,
AliasHandler, AttributeHandler)
from src.commands.cmdsethandler import CmdSetHandler
from src.commands import cmdhandler
from src.utils import utils
from src.utils import utils, logger
from src.utils.utils import to_str, make_iter
from django.utils.translation import ugettext as _
__all__ = ("PlayerDB",)
__all__ = ("PlayerDB",)
_ME = _("me")
_SELF = _("self")
@ -47,14 +48,12 @@ _DA = object.__delattr__
_TYPECLASS = None
#------------------------------------------------------------
#
# PlayerDB
#
#------------------------------------------------------------
class PlayerDB(TypedObject, AbstractUser):
"""
This is a special model using Django's 'profile' functionality
@ -90,7 +89,9 @@ class PlayerDB(TypedObject, AbstractUser):
# store a connected flag here too, not just in sessionhandler.
# This makes it easier to track from various out-of-process locations
db_is_connected = models.BooleanField(default=False, verbose_name="is_connected", help_text="If player is connected to game or not")
db_is_connected = models.BooleanField(default=False,
verbose_name="is_connected",
help_text="If player is connected to game or not")
# database storage of persistant cmdsets.
db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True,
help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.")
@ -118,24 +119,36 @@ class PlayerDB(TypedObject, AbstractUser):
_SA(self, "nicks", NickHandler(self))
# alias to the objs property
def __characters_get(self): return self.objs
def __characters_set(self, value): self.objs = value
def __characters_del(self): raise Exception("Cannot delete name")
def __characters_get(self):
return self.objs
def __characters_set(self, value):
self.objs = value
def __characters_del(self):
raise Exception("Cannot delete name")
characters = property(__characters_get, __characters_set, __characters_del)
# cmdset_storage property
# This seems very sensitive to caching, so leaving it be for now /Griatch
#@property
def cmdset_storage_get(self):
"Getter. Allows for value = self.name. Returns a list of cmdset_storage."
"""
Getter. Allows for value = self.name. Returns a list of cmdset_storage.
"""
storage = _GA(self, "db_cmdset_storage")
# we need to check so storage is not None
return [path.strip() for path in storage.split(',')] if storage else []
return [path.strip() for path in storage.split(',')] if storage else []
#@cmdset_storage.setter
def cmdset_storage_set(self, value):
"Setter. Allows for self.name = value. Stores as a comma-separated string."
"""
Setter. Allows for self.name = value. Stores as a comma-separated
string.
"""
_SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value)))
_GA(self, "save")()
#@cmdset_storage.deleter
def cmdset_storage_del(self):
"Deleter. Allows for del self.name"
@ -161,10 +174,12 @@ class PlayerDB(TypedObject, AbstractUser):
#@property
def __username_get(self):
return _GA(self, "username")
def __username_set(self, value):
_SA(self, "username", value)
def __username_del(self):
_DA(self, "username", value)
_DA(self, "username")
# aliases
name = property(__username_get, __username_set, __username_del)
key = property(__username_get, __username_set, __username_del)
@ -173,8 +188,10 @@ class PlayerDB(TypedObject, AbstractUser):
def __uid_get(self):
"Getter. Retrieves the user id"
return self.id
def __uid_set(self, value):
raise Exception("User id cannot be set!")
def __uid_del(self):
raise Exception("User id cannot be deleted!")
uid = property(__uid_get, __uid_set, __uid_del)
@ -197,21 +214,21 @@ class PlayerDB(TypedObject, AbstractUser):
def msg(self, text=None, from_obj=None, sessid=None, **kwargs):
"""
Evennia -> User
This is the main route for sending data back to the user from the server.
This is the main route for sending data back to the user from the
server.
outgoing_string (string) - text data to send
from_obj (Object/Player) - source object of message to send. Its at_msg_send
hook will be called.
sessid - the session id of the session to send to. If not given, return to
all sessions connected to this player. This is usually only
from_obj (Object/Player) - source object of message to send. Its
at_msg_send() hook will be called.
sessid - the session id of the session to send to. If not given, return
to all sessions connected to this player. This is usually only
relevant when using msg() directly from a player-command (from
a command on a Character, the character automatically stores and
handles the sessid).
a command on a Character, the character automatically stores
and handles the sessid).
kwargs (dict) - All other keywords are parsed as extra data.
"""
if "data" in kwargs:
# deprecation warning
from src.utils import logger
logger.log_depmsg("PlayerDB:msg() 'data'-dict keyword is deprecated. Use **kwargs instead.")
data = kwargs.pop("data")
if isinstance(data, dict):
@ -253,16 +270,17 @@ class PlayerDB(TypedObject, AbstractUser):
if not _SESSIONS:
from src.server.sessionhandler import SESSIONS as _SESSIONS
return _SESSIONS.sessions_from_player(self)
sessions = property(get_all_sessions) # alias shortcut
sessions = property(get_all_sessions) # alias shortcut
def disconnect_session_from_player(self, sessid):
"""
Access method for disconnecting a given session from the player
(connection happens automatically in the sessionhandler)
"""
# this should only be one value, loop just to make sure to clean everything
sessions = (session for session in self.get_all_sessions() if session.sessid == sessid)
# this should only be one value, loop just to make sure to
# clean everything
sessions = (session for session in self.get_all_sessions()
if session.sessid == sessid)
for session in sessions:
# this will also trigger unpuppeting
session.sessionhandler.disconnect(session)
@ -292,8 +310,9 @@ class PlayerDB(TypedObject, AbstractUser):
# we don't allow to puppet an object already controlled by an active
# player. To kick a player, call unpuppet_object on them explicitly.
return
# if we get to this point the character is ready to puppet or it was left
# with a lingering player/sessid reference from an unclean server kill or similar
# if we get to this point the character is ready to puppet or it
# was left with a lingering player/sessid reference from an unclean
# server kill or similar
if normal_mode:
_GA(obj.typeclass, "at_pre_puppet")(self.typeclass, sessid=sessid)
@ -341,8 +360,9 @@ class PlayerDB(TypedObject, AbstractUser):
def get_puppet(self, sessid, return_dbobj=False):
"""
Get an object puppeted by this session through this player. This is the main
method for retrieving the puppeted object from the player's end.
Get an object puppeted by this session through this player. This is
the main method for retrieving the puppeted object from the
player's end.
sessid - return character connected to this sessid,
character - return character if connected to this player, else None.
@ -359,7 +379,8 @@ class PlayerDB(TypedObject, AbstractUser):
"""
Get all currently puppeted objects as a list
"""
puppets = [session.puppet for session in self.get_all_sessions() if session.puppet]
puppets = [session.puppet for session in self.get_all_sessions()
if session.puppet]
if return_dbobj:
return puppets
return [puppet.typeclass for puppet in puppets]
@ -379,30 +400,25 @@ class PlayerDB(TypedObject, AbstractUser):
# utility methods
def delete(self, *args, **kwargs):
"""
Deletes the player permanently.
Makes sure to delete user also when deleting player - the two may never exist separately.
"""
for session in self.get_all_sessions():
# unpuppeting all objects and disconnecting the user, if any
# sessions remain (should usually be handled from the deleting command)
# sessions remain (should usually be handled from the
# deleting command)
self.unpuppet_object(session.sessid)
session.sessionhandler.disconnect(session, reason=_("Player being deleted."))
#try:
# if _GA(self, "user"):
# _GA(_GA(self, "user"), "delete")()
#except AssertionError:
# pass
super(PlayerDB, self).delete(*args, **kwargs)
def execute_cmd(self, raw_string, sessid=None):
"""
Do something as this player. This method is never called normally,
but only when the player object itself is supposed to execute the command. It
does not take nicks on eventual puppets into account.
but only when the player object itself is supposed to execute the
command. It does not take nicks on eventual puppets into account.
raw_string - raw command input coming from the command line.
"""
# nick replacement - we require full-word matching.
@ -410,8 +426,9 @@ class PlayerDB(TypedObject, AbstractUser):
raw_string = utils.to_unicode(raw_string)
raw_list = raw_string.split(None)
raw_list = [" ".join(raw_list[:i+1]) for i in range(len(raw_list)) if raw_list[:i+1]]
# get the nick replacement data directly from the database to be able to use db_category__in
raw_list = [" ".join(raw_list[:i + 1]) for i in range(len(raw_list)) if raw_list[:i + 1]]
# get the nick replacement data directly from the database to be
# able to use db_category__in
nicks = self.db_attributes.filter(db_category__in=("nick_inputline", "nick_channel"))
for nick in nicks:
if nick.db_key in raw_list:
@ -419,22 +436,30 @@ class PlayerDB(TypedObject, AbstractUser):
break
if not sessid and _MULTISESSION_MODE in (0, 1):
# in this case, we should either have only one sessid, or the sessid
# should not matter (since the return goes to all of them we can just
# use the first one as the source)
# should not matter (since the return goes to all of them we can
# just use the first one as the source)
sessid = self.get_all_sessions()[0].sessid
return cmdhandler.cmdhandler(self.typeclass, raw_string, callertype="player", sessid=sessid)
return cmdhandler.cmdhandler(self.typeclass, raw_string,
callertype="player", sessid=sessid)
def search(self, ostring, return_character=False, **kwargs):
def search(self, ostring, return_puppet=False,
return_character=False, **kwargs):
"""
This is similar to the ObjectDB search method but will search for Players only. Errors
will be echoed, and None returned if no Player is found.
This is similar to the ObjectDB search method but will search for
Players only. Errors will be echoed, and None returned if no Player
is found.
return_character - will try to return the character the player controls instead of
the Player object itself. If no Character exists (since Player is
OOC), None will be returned.
Extra keywords are ignored, but are allowed in call in order to make API more consistent
with objects.models.TypedObject.search.
return_character - will try to return the character the player controls
instead of the Player object itself. If no
Character exists (since Player is OOC), None will
be returned.
Extra keywords are ignored, but are allowed in call in order to make
API more consistent with objects.models.
TypedObject.search.
"""
if return_character:
logger.log_depmsg("Player.search's 'return_character' keyword is deprecated. Use the return_puppet keyword instead.")
#return_puppet = return_character
# handle me, self
if ostring in (_ME, _SELF, '*' + _ME, '*' + _SELF):
return self

View file

@ -41,23 +41,29 @@ class Player(TypeClass):
key (string) - name of player
name (string)- wrapper for user.username
aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings.
aliases (list of strings) - aliases to the object. Will be saved to
database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
dbobj (Player, read-only) - link to database model. dbobj.typeclass points back to this class
typeclass (Player, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch.
dbobj (Player, read-only) - link to database model. dbobj.typeclass
points back to this class
typeclass (Player, read-only) - this links back to this class as an
identified only. Use self.swap_typeclass() to switch.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
user (User, read-only) - django User authorization object
obj (Object) - game object controlled by player. 'character' can also be used.
obj (Object) - game object controlled by player. 'character' can also
be used.
sessions (list of Sessions) - sessions connected to this player
is_superuser (bool, read-only) - if the connected user is a superuser
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
db - attribute-handler: store/retrieve database attributes on this
self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not
create a database entry when storing data
scripts - script-handler. Add new scripts to object with scripts.add()
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
nicks - nick-handler. New nicks with nicks.add().
@ -67,7 +73,9 @@ class Player(TypeClass):
msg(outgoing_string, from_obj=None, **kwargs)
swap_character(new_character, delete_old_character=False)
execute_cmd(raw_string)
search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, player=False)
search(ostring, global_search=False, attribute_name=None,
use_nicks=False, location=None,
ignore_errors=False, player=False)
is_typeclass(typeclass, exact=False)
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
access(accessing_obj, access_type='read', default=False)
@ -99,15 +107,16 @@ class Player(TypeClass):
def msg(self, text=None, from_obj=None, sessid=None, **kwargs):
"""
Evennia -> User
This is the main route for sending data back to the user from the server.
This is the main route for sending data back to the user from
the server.
text (string) - text data to send
from_obj (Object/Player) - source object of message to send
sessid - the session id of the session to send to. If not given, return to
all sessions connected to this player. This is usually only
relevant when using msg() directly from a player-command (from
a command on a Character, the character automatically stores and
handles the sessid).
sessid - the session id of the session to send to. If not given,
return to all sessions connected to this player. This is usually only
relevant when using msg() directly from a player-command (from
a command on a Character, the character automatically stores and
handles the sessid).
kwargs - extra data to send through protocol
"""
self.dbobj.msg(text=text, from_obj=from_obj, sessid=sessid, **kwargs)
@ -131,16 +140,18 @@ class Player(TypeClass):
Argument:
raw_string (string) - raw command input
sessid (int) - id of session executing the command. This sets the sessid property on the command
sessid (int) - id of session executing the command. This sets the
sessid property on the command
Returns Deferred - this is an asynchronous Twisted object that will
not fire until the command has actually finished executing. To overload
this one needs to attach callback functions to it, with addCallback(function).
This function will be called with an eventual return value from the command
execution.
not fire until the command has actually finished executing. To
overload this one needs to attach callback functions to it, with
addCallback(function). This function will be called with an
eventual return value from the command execution.
This return is not used at all by Evennia by default, but might be useful
for coders intending to implement some sort of nested command structure.
This return is not used at all by Evennia by default, but might
be useful for coders intending to implement some sort of nested
command structure.
"""
return self.dbobj.execute_cmd(raw_string, sessid=sessid)
@ -204,11 +215,13 @@ class Player(TypeClass):
"""
self.dbobj.swap_typeclass(new_typeclass, clean_attributes=clean_attributes, no_default=no_default)
self.dbobj.swap_typeclass(new_typeclass,
clean_attributes=clean_attributes, no_default=no_default)
def access(self, accessing_obj, access_type='read', default=False):
"""
Determines if another object has permission to access this object in whatever way.
Determines if another object has permission to access this object
in whatever way.
accessing_obj (Object)- object trying to access this one
access_type (string) - type of access sought
@ -221,8 +234,8 @@ class Player(TypeClass):
This explicitly checks the given string against this object's
'permissions' property without involving any locks.
permstring (string) - permission string that need to match a permission on the object.
(example: 'Builders')
permstring (string) - permission string that need to match a permission
on the object. (example: 'Builders')
"""
return self.dbobj.check_permstring(permstring)
@ -307,7 +320,8 @@ class Player(TypeClass):
except Exception:
logger.log_trace()
now = datetime.datetime.now()
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month,
now.day, now.hour, now.minute)
if _CONNECT_CHANNEL:
_CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message))
else:
@ -361,15 +375,15 @@ class Player(TypeClass):
def at_server_reload(self):
"""
This hook is called whenever the server is shutting down for restart/reboot.
If you want to, for example, save non-persistent properties across a restart,
this is the place to do it.
This hook is called whenever the server is shutting down for
restart/reboot. If you want to, for example, save non-persistent
properties across a restart, this is the place to do it.
"""
pass
def at_server_shutdown(self):
"""
This hook is called whenever the server is shutting down fully (i.e. not for
a restart).
This hook is called whenever the server is shutting down fully
(i.e. not for a restart).
"""
pass

View file

@ -1,12 +1,13 @@
"""
Makes it easier to import by grouping all relevant things already at this level.
Makes it easier to import by grouping all relevant things already at this
level.
You can henceforth import most things directly from src.scripts
Also, the initiated object manager is available as src.scripts.manager.
"""
from src.scripts.scripts import *
from src.scripts.scripts import *
from src.scripts.models import ScriptDB
manager = ScriptDB.objects

View file

@ -7,14 +7,17 @@ from src.typeclasses.models import Attribute
from src.scripts.models import ScriptDB
from django.contrib import admin
class AttributeInline(admin.TabularInline):
model = Attribute
fields = ('db_key', 'db_value')
max_num = 1
class ScriptDBAdmin(admin.ModelAdmin):
list_display = ('id', 'db_key', 'db_typeclass_path', 'db_obj', 'db_interval', 'db_repeats', 'db_persistent')
list_display = ('id', 'db_key', 'db_typeclass_path',
'db_obj', 'db_interval', 'db_repeats', 'db_persistent')
list_display_links = ('id', 'db_key')
ordering = ['db_obj', 'db_typeclass_path']
search_fields = ['^db_key', 'db_typeclass_path']
@ -25,7 +28,9 @@ class ScriptDBAdmin(admin.ModelAdmin):
fieldsets = (
(None, {
'fields':(('db_key', 'db_typeclass_path'), 'db_interval', 'db_repeats', 'db_start_delay', 'db_persistent', 'db_obj')}),
'fields': (('db_key', 'db_typeclass_path'), 'db_interval',
'db_repeats', 'db_start_delay', 'db_persistent',
'db_obj')}),
)
#inlines = [AttributeInline]

View file

@ -10,6 +10,7 @@ __all__ = ("ScriptManager",)
VALIDATE_ITERATION = 0
class ScriptManager(TypedObjectManager):
"""
This Scriptmanager implements methods for searching
@ -82,8 +83,8 @@ class ScriptManager(TypedObjectManager):
def remove_non_persistent(self, obj=None):
"""
This cleans up the script database of all non-persistent
scripts, or only those on obj. It is called every time the server restarts
and
scripts, or only those on obj. It is called every time the server
restarts.
"""
if obj:
to_stop = self.filter(db_obj=obj, db_persistent=False, db_is_active=True)
@ -211,10 +212,11 @@ class ScriptManager(TypedObjectManager):
Make an identical copy of the original_script
"""
typeclass = original_script.typeclass_path
new_key = new_key if new_key!=None else original_script.key
new_obj = new_obj if new_obj!=None else original_script.obj
new_locks = new_locks if new_locks!=None else original_script.db_lock_storage
new_key = new_key if new_key is not None else original_script.key
new_obj = new_obj if new_obj is not None else original_script.obj
new_locks = new_locks if new_locks is not None else original_script.db_lock_storage
from src.utils import create
new_script = create.create_script(typeclass, key=new_key, obj=new_obj, locks=new_locks, autostart=True)
new_script = create.create_script(typeclass, key=new_key, obj=new_obj,
locks=new_locks, autostart=True)
return new_script

View file

@ -26,10 +26,9 @@ Common examples of uses of Scripts:
"""
from django.conf import settings
from django.db import models
from django.db.models.signals import post_init, pre_delete
from src.typeclasses.models import Attribute, TypedObject, TagHandler, AttributeHandler#, AliasHandler, NickHandler
from django.contrib.contenttypes.models import ContentType
from src.typeclasses.models import (TypedObject, TagHandler,
AttributeHandler)
from src.scripts.manager import ScriptManager
__all__ = ("ScriptDB",)

View file

@ -38,10 +38,13 @@ class ScriptHandler(object):
interval = script.interval
if script.repeats:
repeats = script.repeats
try: next_repeat = script.time_until_next_repeat()
except: next_repeat = "?"
try:
next_repeat = script.time_until_next_repeat()
except:
next_repeat = "?"
string += _("\n '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s") % \
{"key":script.key, "next_repeat":next_repeat, "interval":interval,"repeats":repeats,"desc":script.desc}
{"key": script.key, "next_repeat": next_repeat,
"interval": interval, "repeats": repeats, "desc": script.desc}
return string.strip()
def add(self, scriptclass, key=None, autostart=True):
@ -51,10 +54,12 @@ class ScriptHandler(object):
scriptclass - either a class object
inheriting from Script, an instantiated script object
or a python path to such a class object.
key - optional identifier for the script (often set in script definition)
key - optional identifier for the script (often set in script
definition)
autostart - start the script upon adding it
"""
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=autostart)
script = create.create_script(scriptclass, key=key, obj=self.obj,
autostart=autostart)
if not script:
logger.log_errmsg("Script %s could not be created and/or started." % scriptclass)
return False

View file

@ -5,30 +5,31 @@ scripts are inheriting from.
It also defines a few common scripts.
"""
from sys import getsizeof
from time import time
from collections import defaultdict
from twisted.internet.defer import maybeDeferred
from twisted.internet.task import LoopingCall
from django.conf import settings
from src.server import caches
from django.utils.translation import ugettext as _
from src.typeclasses.typeclass import TypeClass
from src.scripts.models import ScriptDB
from src.comms import channelhandler
from src.utils import logger, is_pypy
from django.utils.translation import ugettext as _
from src.utils import logger
__all__ = ["Script", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler"]
__all__ = ["Script", "DoNothing", "CheckSessions",
"ValidateScripts", "ValidateChannelHandler"]
_SESSIONS = None
_ATTRIBUTE_CACHE_MAXSIZE = settings.ATTRIBUTE_CACHE_MAXSIZE # attr-cache size in MB.
# attr-cache size in MB
_ATTRIBUTE_CACHE_MAXSIZE = settings.ATTRIBUTE_CACHE_MAXSIZE
#
# Base script, inherit from Script below instead.
#
class ScriptClass(TypeClass):
"""
Base class for scripts. Don't inherit from this, inherit from Script instead.
Base class for scripts. Don't inherit from this, inherit
from the class 'Script' instead.
"""
# private methods
@ -53,7 +54,8 @@ class ScriptClass(TypeClass):
else:
# starting script anew.
#print "_start_task: self.interval:", self.key, self.dbobj.interval
self.ndb.twisted_task.start(self.dbobj.interval, now=start_now and not self.start_delay)
self.ndb.twisted_task.start(self.dbobj.interval,
now=start_now and not self.start_delay)
self.ndb.time_last_called = int(time())
def _stop_task(self):
@ -64,16 +66,19 @@ class ScriptClass(TypeClass):
self.ndb.twisted_task.stop()
except Exception:
logger.log_trace()
def _step_err_callback(self, e):
"callback for runner errors"
cname = self.__class__.__name__
estring = _("Script %(key)s(#%(dbid)i) of type '%(cname)s': at_repeat() error '%(err)s'.") % \
{"key":self.key, "dbid":self.dbid, "cname":cname, "err":e.getErrorMessage()}
{"key": self.key, "dbid": self.dbid, "cname": cname,
"err": e.getErrorMessage()}
try:
self.dbobj.db_obj.msg(estring)
except Exception:
pass
logger.log_errmsg(estring)
def _step_succ_callback(self):
"step task runner. No try..except needed due to defer wrap."
if not self.is_valid():
@ -82,7 +87,7 @@ class ScriptClass(TypeClass):
self.at_repeat()
repeats = self.dbobj.db_repeats
if repeats <= 0:
pass # infinite repeat
pass # infinite repeat
elif repeats == 1:
self.stop()
return
@ -92,14 +97,14 @@ class ScriptClass(TypeClass):
self.save()
if self.ndb._paused_time:
# this means we were running an unpaused script, for the time remaining
# after the pause. Now we start a normal-running timer again.
#print "switching to normal run:", self.key
# this means we were running an unpaused script, for the
# time remaining after the pause. Now we start a normal-running
# timer again.
# print "switching to normal run:", self.key
del self.ndb._paused_time
self._stop_task()
self._start_task(start_now=False)
def _step_task(self):
"step task"
try:
@ -109,7 +114,6 @@ class ScriptClass(TypeClass):
except Exception:
logger.log_trace()
# Public methods
def time_until_next_repeat(self):
@ -136,7 +140,8 @@ class ScriptClass(TypeClass):
force_restart - if True, will always restart the script, regardless
of if it has started before.
returns 0 or 1 to indicated the script has been started or not. Used in counting.
returns 0 or 1 to indicated the script has been started or not.
Used in counting.
"""
#print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
# self.is_active, force_restart)
@ -205,7 +210,7 @@ class ScriptClass(TypeClass):
"""
#print "pausing", self.key, self.time_until_next_repeat()
dt = self.time_until_next_repeat()
if dt == None:
if dt is None:
return
self.db._paused_time = dt
self._stop_task()
@ -216,7 +221,7 @@ class ScriptClass(TypeClass):
"""
#print "unpausing", self.key, self.db._paused_time
dt = self.db._paused_time
if dt == None:
if dt is None:
return False
try:
self.dbobj.is_active = True
@ -234,18 +239,23 @@ class ScriptClass(TypeClass):
def at_script_creation(self):
"placeholder"
pass
def is_valid(self):
"placeholder"
pass
def at_start(self):
"placeholder."
pass
def at_stop(self):
"placeholder"
pass
def at_repeat(self):
"placeholder"
pass
def at_init(self):
"called when typeclass re-caches. Usually not used for scripts."
pass
@ -263,8 +273,9 @@ class Script(ScriptClass):
def __init__(self, dbobj):
"""
This is the base TypeClass for all Scripts. Scripts describe events, timers and states in game,
they can have a time component or describe a state that changes under certain conditions.
This is the base TypeClass for all Scripts. Scripts describe events,
timers and states in game, they can have a time component or describe
a state that changes under certain conditions.
Script API:
@ -272,57 +283,73 @@ class Script(ScriptClass):
key (string) - name of object
name (string)- same as key
aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings.
aliases (list of strings) - aliases to the object. Will be saved to
database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class
typeclass (Object, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch.
dbobj (Object, read-only) - link to database model. dbobj.typeclass
points back to this class
typeclass (Object, read-only) - this links back to this class as an
identified only. Use self.swap_typeclass() to switch.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
desc (string) - optional description of script, shown in listings
obj (Object) - optional object that this script is connected to and acts on (set automatically by obj.scripts.add())
interval (int) - how often script should run, in seconds. <=0 turns off ticker
start_delay (bool) - if the script should start repeating right away or wait self.interval seconds
repeats (int) - how many times the script should repeat before stopping. <=0 means infinite repeats
obj (Object) - optional object that this script is connected to
and acts on (set automatically
by obj.scripts.add())
interval (int) - how often script should run, in seconds.
<=0 turns off ticker
start_delay (bool) - if the script should start repeating right
away or wait self.interval seconds
repeats (int) - how many times the script should repeat before
stopping. <=0 means infinite repeats
persistent (bool) - if script should survive a server shutdown or not
is_active (bool) - if script is currently running
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data
db - attribute-handler: store/retrieve database attributes on this
self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not
create a database entry when storing data
* Helper methods
start() - start script (this usually happens automatically at creation and obj.script.add() etc)
start() - start script (this usually happens automatically at creation
and obj.script.add() etc)
stop() - stop script, and delete it
pause() - put the script on hold, until unpause() is called. If script is persistent, the pause state will survive a shutdown.
unpause() - restart a previously paused script. The script will continue as if it was never paused.
time_until_next_repeat() - if a timed script (interval>0), returns time until next tick
pause() - put the script on hold, until unpause() is called. If script
is persistent, the pause state will survive a shutdown.
unpause() - restart a previously paused script. The script will
continue as if it was never paused.
time_until_next_repeat() - if a timed script (interval>0), returns
time until next tick
* Hook methods
at_script_creation() - called only once, when an object of this
class is first created.
is_valid() - is called to check if the script is valid to be running
at the current time. If is_valid() returns False, the running
script is stopped and removed from the game. You can use this
to check state changes (i.e. an script tracking some combat
stats at regular intervals is only valid to run while there is
actual combat going on).
at_start() - Called every time the script is started, which for persistent
scripts is at least once every server start. Note that this is
unaffected by self.delay_start, which only delays the first call
to at_repeat().
at_repeat() - Called every self.interval seconds. It will be called immediately
upon launch unless self.delay_start is True, which will delay
the first call of this method by self.interval seconds. If
self.interval<=0, this method will never be called.
at_stop() - Called as the script object is stopped and is about to be removed from
the game, e.g. because is_valid() returned False.
at_server_reload() - Called when server reloads. Can be used to save temporary
variables you want should survive a reload.
at the current time. If is_valid() returns False, the
running script is stopped and removed from the game. You
can use this to check state changes (i.e. an script
tracking some combat stats at regular intervals is only
valid to run while there is actual combat going on).
at_start() - Called every time the script is started, which for
persistent scripts is at least once every server start.
Note that this is unaffected by self.delay_start, which
only delays the first call to at_repeat().
at_repeat() - Called every self.interval seconds. It will be called
immediately upon launch unless self.delay_start is True,
which will delay the first call of this method by
self.interval seconds. If self.interval<=0, this method
will never be called.
at_stop() - Called as the script object is stopped and is about to
be removed from the game, e.g. because is_valid()
returned False or self.stop() was called manually.
at_server_reload() - Called when server reloads. Can be used to save
temporary variables you want should survive a reload.
at_server_shutdown() - called at a full server shutdown.
@ -335,7 +362,7 @@ class Script(ScriptClass):
"""
self.key = "<unnamed>"
self.desc = ""
self.interval = 0 # infinite
self.interval = 0 # infinite
self.start_delay = False
self.repeats = 0 # infinite
self.persistent = False
@ -372,29 +399,29 @@ class Script(ScriptClass):
def at_server_reload(self):
"""
This hook is called whenever the server is shutting down for restart/reboot.
If you want to, for example, save non-persistent properties across a restart,
this is the place to do it.
This hook is called whenever the server is shutting down for
restart/reboot. If you want to, for example, save non-persistent
properties across a restart, this is the place to do it.
"""
pass
def at_server_shutdown(self):
"""
This hook is called whenever the server is shutting down fully (i.e. not for
a restart).
This hook is called whenever the server is shutting down fully
(i.e. not for a restart).
"""
pass
# Some useful default Script types used by Evennia.
class DoNothing(Script):
"An script that does nothing. Used as default fallback."
def at_script_creation(self):
"Setup the script"
self.key = "sys_do_nothing"
self.desc = _("This is an empty placeholder script.")
"Setup the script"
self.key = "sys_do_nothing"
self.desc = _("This is an empty placeholder script.")
class Store(Script):
"Simple storage script"
@ -403,6 +430,7 @@ class Store(Script):
self.key = "sys_storage"
self.desc = _("This is a generic storage container.")
class CheckSessions(Script):
"Check sessions regularly."
def at_script_creation(self):
@ -421,13 +449,14 @@ class CheckSessions(Script):
#print "ValidateSessions run"
_SESSIONS.validate_sessions()
class ValidateScripts(Script):
"Check script validation regularly"
def at_script_creation(self):
"Setup the script"
self.key = "sys_scripts_validate"
self.desc = _("Validates all scripts regularly.")
self.interval = 3600 # validate every hour.
self.interval = 3600 # validate every hour.
self.persistent = True
def at_repeat(self):
@ -435,13 +464,14 @@ class ValidateScripts(Script):
#print "ValidateScripts run."
ScriptDB.objects.validate()
class ValidateChannelHandler(Script):
"Update the channelhandler to make sure it's in sync."
def at_script_creation(self):
"Setup the script"
self.key = "sys_channels_validate"
self.desc = _("Updates the channel handler")
self.interval = 3700 # validate a little later than ValidateScripts
self.interval = 3700 # validate a little later than ValidateScripts
self.persistent = True
def at_repeat(self):

View file

@ -1,18 +1,19 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from django.contrib import admin
from src.server.models import ServerConfig
class ServerConfigAdmin(admin.ModelAdmin):
"Custom admin for server configs"
list_display = ('db_key', 'db_value')
list_display_links = ('db_key',)
ordering = ['db_key', 'db_value']
search_fields = ['db_key']
save_as = True
save_on_top = True
list_select_related = True
admin.site.register(ServerConfig, ServerConfigAdmin)
#
# This sets up how models are displayed
# in the web admin interface.
#
from django.contrib import admin
from src.server.models import ServerConfig
class ServerConfigAdmin(admin.ModelAdmin):
"Custom admin for server configs"
list_display = ('db_key', 'db_value')
list_display_links = ('db_key',)
ordering = ['db_key', 'db_value']
search_fields = ['db_key']
save_as = True
save_on_top = True
list_select_related = True
admin.site.register(ServerConfig, ServerConfigAdmin)

View file

@ -1,17 +1,18 @@
"""
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.
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 player connected. If it looses the AMP connection
it will automatically try to reconnect.
Portal - (AMP client) handles protocols. It contains a list of connected
sessions in a dictionary for identifying the respective player
connected. If it looses 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 player objects. This is synced against the portal at startup
and when a session connects/disconnects
of sessions tied to player objects. This is synced against the portal
at startup and when a session connects/disconnects
"""
@ -29,16 +30,17 @@ from src.utils.utils import to_str, variable_from_module
# communication bits
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 sessigon sync
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 sessigon sync
MAXLEN = 65535 # max allowed data length in AMP protocol
MAXLEN = 65535 # max allowed data length in AMP protocol
def get_restart_mode(restart_file):
"""
@ -49,6 +51,7 @@ def get_restart_mode(restart_file):
return flag == "True"
return False
class AmpServerFactory(protocol.ServerFactory):
"""
This factory creates the Server as a new AMPProtocol instance for accepting
@ -71,9 +74,11 @@ class AmpServerFactory(protocol.ServerFactory):
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
This factory creates an instance of the Portal, an AMPProtocol
instances to use to connect
"""
# Initial reconnect delay in seconds.
initialDelay = 1
@ -139,6 +144,7 @@ class MsgPortal2Server(amp.Command):
errors = [(Exception, 'EXCEPTION')]
response = []
class MsgServer2Portal(amp.Command):
"""
Message server -> portal
@ -151,6 +157,7 @@ class MsgServer2Portal(amp.Command):
errors = [(Exception, 'EXCEPTION')]
response = []
class ServerAdmin(amp.Command):
"""
Portal -> Server
@ -165,6 +172,7 @@ class ServerAdmin(amp.Command):
errors = [(Exception, 'EXCEPTION')]
response = []
class PortalAdmin(amp.Command):
"""
Server -> Portal
@ -178,6 +186,7 @@ class PortalAdmin(amp.Command):
errors = [(Exception, 'EXCEPTION')]
response = []
class FunctionCall(amp.Command):
"""
Bidirectional
@ -202,6 +211,7 @@ loads = lambda data: pickle.loads(to_str(data))
MSGBUFFER = defaultdict(list)
#------------------------------------------------------------
# Core AMP protocol for communication Server <-> Portal
#------------------------------------------------------------
@ -241,7 +251,8 @@ class AMPProtocol(amp.AMP):
def errback(self, e, info):
"error handler, to avoid dropping connections on server tracebacks."
f = e.trap(Exception)
print "AMP Error for %(info)s: %(e)s" % {'info': info, 'e': e.getErrorMessage()}
print "AMP Error for %(info)s: %(e)s" % {'info': info,
'e': e.getErrorMessage()}
def send_split_msg(self, sessid, msg, data, command):
"""
@ -258,14 +269,16 @@ class AMPProtocol(amp.AMP):
datastr = dumps(data)
nmsg, ndata = len(msg), len(datastr)
if nmsg > MAXLEN or ndata > MAXLEN:
msglist = [msg[i:i+MAXLEN] for i in range(0, len(msg), MAXLEN)]
datalist = [datastr[i:i+MAXLEN] for i in range(0, len(datastr), MAXLEN)]
msglist = [msg[i:i + MAXLEN] for i in range(0, len(msg), MAXLEN)]
datalist = [datastr[i:i + MAXLEN]
for i in range(0, len(datastr), MAXLEN)]
nmsglist, ndatalist = len(msglist), len(datalist)
if ndatalist < nmsglist:
datalist.extend("" for i in range(nmsglist-ndatalist))
datalist.extend("" for i in range(nmsglist - ndatalist))
if nmsglist < ndatalist:
msglist.extend("" for i in range(ndatalist-nmsglist))
# we have split the msg/data into right-size chunks. Now we send it in sequence
msglist.extend("" for i in range(ndatalist - nmsglist))
# we have split the msg/data into right-size chunks. Now we
# send it in sequence
return [self.callRemote(command,
sessid=sessid,
msg=to_str(msg),
@ -295,8 +308,8 @@ class AMPProtocol(amp.AMP):
return {}
else:
# we have all parts. Put it all together in the right order.
msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0]))
data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0]))
msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0]))
data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0]))
del MSGBUFFER[sessid]
# call session hook with the data
self.factory.server.sessions.data_in(sessid, text=msg, **loads(data))
@ -311,13 +324,14 @@ class AMPProtocol(amp.AMP):
try:
return self.callRemote(MsgPortal2Server,
sessid=sessid,
msg=to_str(msg) if msg!=None else "",
msg=to_str(msg) if msg is not None else "",
ipart=0,
nparts=1,
data=dumps(data)).addErrback(self.errback, "MsgPortal2Server")
except amp.TooLong:
# the msg (or data) was too long for AMP to send. We need to send in blocks.
return self.send_split_msg(sessid, msg, kwargs, MsgPortal2Server)
# the msg (or data) was too long for AMP to send.
# We need to send in blocks.
return self.send_split_msg(sessid, msg, data, MsgPortal2Server)
# Server -> Portal message
@ -335,8 +349,8 @@ class AMPProtocol(amp.AMP):
return {}
else:
# we have all parts. Put it all together in the right order.
msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0]))
data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0]))
msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0]))
data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0]))
del MSGBUFFER[sessid]
# call session hook with the data
self.factory.portal.sessions.data_out(sessid, text=msg, **loads(data))
@ -351,14 +365,14 @@ class AMPProtocol(amp.AMP):
try:
return self.callRemote(MsgServer2Portal,
sessid=sessid,
msg=to_str(msg) if msg!=None else "",
msg=to_str(msg) if msg is not None else "",
ipart=0,
nparts=1,
data=dumps(data)).addErrback(self.errback, "MsgServer2Portal")
except amp.TooLong:
# the msg (or data) was too long for AMP to send. We need to send in blocks.
return self.send_split_msg(sessid, msg, kwargs, MsgServer2Portal)
# the msg (or data) was too long for AMP to send.
# We need to send in blocks.
return self.send_split_msg(sessid, msg, data, MsgServer2Portal)
# Server administration from the Portal side
def amp_server_admin(self, sessid, operation, data):
@ -372,17 +386,18 @@ class AMPProtocol(amp.AMP):
#print "serveradmin (server side):", sessid, ord(operation), data
if operation == PCONN: #portal_session_connect
if operation == PCONN: # portal_session_connect
# create a new session and sync it
server_sessionhandler.portal_connect(data)
elif operation == PDISCONN: #'portal_session_disconnect'
elif operation == PDISCONN: # portal_session_disconnect
# session closed from portal side
self.factory.server.sessions.portal_disconnect(sessid)
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
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_session_sync(data)
else:
@ -414,23 +429,23 @@ class AMPProtocol(amp.AMP):
portal_sessionhandler = self.factory.portal.sessions
#print "portaladmin (portal side):", sessid, ord(operation), data
if operation == SLOGIN: # 'server_session_login'
if operation == SLOGIN: # server_session_login
# a session has authenticated; sync it.
portal_sessionhandler.server_logged_in(sessid, data)
elif operation == SDISCONN: #'server_session_disconnect'
elif operation == SDISCONN: # server_session_disconnect
# the server is ordering to disconnect the session
portal_sessionhandler.server_disconnect(sessid, reason=data)
elif operation == SDISCONNALL: #'server_session_disconnect_all'
elif operation == SDISCONNALL: # server_session_disconnect_all
# server orders all sessions to disconnect
portal_sessionhandler.server_disconnect_all(reason=data)
elif operation == SSHUTD: #server_shutdown'
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'
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(data)
@ -465,25 +480,28 @@ class AMPProtocol(amp.AMP):
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)})
# 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)}
return {'result': dumps(result)}
FunctionCall.responder(amp_function_call)
def call_remote_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).
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
*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
A deferred that fires with the return value of the remote
function call
"""
return self.callRemote(FunctionCall,
module=modulepath,

View file

@ -4,10 +4,10 @@ Central caching module.
"""
from sys import getsizeof
import os, threading
import os
import threading
from collections import defaultdict
from django.core.cache import get_cache
from src.server.models import ServerConfig
from src.utils.utils import uses_database, to_str, get_evennia_pids
@ -35,6 +35,7 @@ if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6
else:
_DATESTRING = "%Y:%m:%d-%H:%M:%S:%f"
def hashid(obj, suffix=""):
"""
Returns a per-class unique hash that combines the object's
@ -59,11 +60,15 @@ def hashid(obj, suffix=""):
# rely on memory adressing in this case.
date, idnum = "InMemory", id(obj)
if not idnum or not date:
# this will happen if setting properties on an object which is not yet saved
# this will happen if setting properties on an object which
# is not yet saved
return None
# we have to remove the class-name's space, for eventual use
# of memcached
hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum)
hid = hid.replace(" ", "") # we have to remove the class-name's space, for memcached's sake
# we cache the object part of the hashid to avoid too many object lookups
hid = hid.replace(" ", "")
# we cache the object part of the hashid to avoid too many
# object lookups
_SA(obj, "_hashid", hid)
# build the complete hashid
hid = "%s%s" % (hid, suffix)
@ -84,8 +89,9 @@ def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwarg
"""
Called at the beginning of the field save operation. The save method
must be called with the update_fields keyword in order to be most efficient.
This method should NOT save; rather it is the save() that triggers this function.
Its main purpose is to allow to plug-in a save handler and oob handlers.
This method should NOT save; rather it is the save() that triggers this
function. Its main purpose is to allow to plug-in a save handler and oob
handlers.
"""
if raw:
return
@ -102,12 +108,14 @@ def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwarg
if callable(handler):
handler()
def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs):
"""
Called at the beginning of the field save operation. The save method
must be called with the update_fields keyword in order to be most efficient.
This method should NOT save; rather it is the save() that triggers this function.
Its main purpose is to allow to plug-in a save handler and oob handlers.
This method should NOT save; rather it is the save() that triggers this
function. Its main purpose is to allow to plug-in a save handler and oob
handlers.
"""
if raw:
return
@ -127,70 +135,6 @@ def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwar
if trackerhandler:
trackerhandler.update(fieldname, _GA(instance, fieldname))
#------------------------------------------------------------
# Attr cache - caching the attribute objects related to a given object to
# avoid lookups more than necessary (this makes Attributes en par in speed
# to any property).
#------------------------------------------------------------
## connected to m2m_changed signal in respective model class
#def post_attr_update(sender, **kwargs):
# "Called when the many2many relation changes (NOT when updating the value of an Attribute!)"
# obj = kwargs['instance']
# model = kwargs['model']
# action = kwargs['action']
# if kwargs['reverse']:
# # the reverse relation changed (the Attribute itself was acted on)
# pass
# else:
# # forward relation changed (the Object holding the Attribute m2m field)
# if not kwargs["pk_set"]:
# return
# if action == "post_add":
# # cache all added objects
# for attr_id in kwargs["pk_set"]:
# attr_obj = model.objects.get(pk=attr_id)
# set_attr_cache(obj, _GA(attr_obj, "db_key"), attr_obj)
# elif action == "post_remove":
# # obj.db_attributes.remove(attr) was called
# for attr_id in kwargs["pk_set"]:
# attr_obj = model.objects.get(pk=attr_id)
# del_attr_cache(obj, _GA(attr_obj, "db_key"))
# attr_obj.delete()
# elif action == "post_clear":
# # obj.db_attributes.clear() was called
# clear_obj_attr_cache(obj)
#
#
## attr cache - this is only left as deprecated cache
#
#def get_attr_cache(obj, attrname):
# "Called by getting attribute"
# hid = hashid(obj, "-%s" % attrname)
# return _ATTR_CACHE.get(hid, None)
#
#def set_attr_cache(obj, attrname, attrobj):
# "Set the attr cache manually; this can be used to update"
# global _ATTR_CACHE
# hid = hashid(obj, "-%s" % attrname)
# _ATTR_CACHE[hid] = attrobj
#
#def del_attr_cache(obj, attrname):
# "Del attribute cache"
# global _ATTR_CACHE
# hid = hashid(obj, "-%s" % attrname)
# if hid in _ATTR_CACHE:
# del _ATTR_CACHE[hid]
#
#def flush_attr_cache():
# "Clear attribute cache"
# global _ATTR_CACHE
# _ATTR_CACHE = {}
#
#def clear_obj_attr_cache(obj):
# global _ATTR_CACHE
# hid = hashid(obj)
# _ATTR_CACHE = {key:value for key, value in _ATTR_CACHE if not key.startswith(hid)}
#------------------------------------------------------------
# Property cache - this is a generic cache for properties stored on models.
@ -203,12 +147,14 @@ def get_prop_cache(obj, propname):
hid = hashid(obj, "-%s" % propname)
return _PROP_CACHE[hid].get(propname, None) if hid else None
def set_prop_cache(obj, propname, propvalue):
"Set property cache"
hid = hashid(obj, "-%s" % propname)
if hid:
_PROP_CACHE[hid][propname] = propvalue
def del_prop_cache(obj, propname):
"Delete element from property cache"
hid = hashid(obj, "-%s" % propname)
@ -216,11 +162,12 @@ def del_prop_cache(obj, propname):
if propname in _PROP_CACHE[hid]:
del _PROP_CACHE[hid][propname]
def flush_prop_cache():
"Clear property cache"
global _PROP_CACHE
_PROP_CACHE = defaultdict(dict)
#_PROP_CACHE.clear()
def get_cache_sizes():
"""
@ -229,8 +176,8 @@ def get_cache_sizes():
global _ATTR_CACHE, _PROP_CACHE
attr_n = len(_ATTR_CACHE)
attr_mb = sum(getsizeof(obj) for obj in _ATTR_CACHE) / 1024.0
field_n = 0 #sum(len(dic) for dic in _FIELD_CACHE.values())
field_mb = 0 # sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _FIELD_CACHE.values()) / 1024.0
field_n = 0 # sum(len(dic) for dic in _FIELD_CACHE.values())
field_mb = 0 # sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _FIELD_CACHE.values()) / 1024.0
prop_n = sum(len(dic) for dic in _PROP_CACHE.values())
prop_mb = sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _PROP_CACHE.values()) / 1024.0
return (attr_n, attr_mb), (field_n, field_mb), (prop_n, prop_mb)

View file

@ -7,16 +7,13 @@ Everything starts at handle_setup()
"""
import django
from django.core import management
from django.conf import settings
from django.contrib.auth import get_user_model
from src.server.models import ServerConfig
from src.help.models import HelpEntry
from src.utils import create
from django.utils.translation import ugettext as _
def create_config_values():
"""
Creates the initial config values.
@ -24,6 +21,7 @@ def create_config_values():
ServerConfig.objects.conf("site_name", settings.SERVERNAME)
ServerConfig.objects.conf("idle_timeout", settings.IDLE_TIMEOUT)
def get_god_player():
"""
Creates the god user.
@ -32,13 +30,15 @@ def get_god_player():
try:
god_player = PlayerDB.objects.get(id=1)
except PlayerDB.DoesNotExist:
txt = "\n\nNo superuser exists yet. The superuser is the 'owner' account on the"
txt += "\nEvennia server. Create a new superuser using the command"
txt = "\n\nNo superuser exists yet. The superuser is the 'owner'"
txt += "\account on the Evennia server. Create a new superuser using"
txt += "\nthe command"
txt += "\n\n python manage.py createsuperuser"
txt += "\n\nFollow the prompts, then restart the server."
raise Exception(txt)
return god_player
def create_objects():
"""
Creates the #1 player and Limbo room.
@ -54,18 +54,23 @@ def create_objects():
# mud-specific settings for the PlayerDB object.
player_typeclass = settings.BASE_PLAYER_TYPECLASS
# run all creation hooks on god_player (we must do so manually since the manage.py command does not)
# run all creation hooks on god_player (we must do so manually
# since the manage.py command does not)
god_player.typeclass_path = player_typeclass
god_player.basetype_setup()
god_player.at_player_creation()
god_player.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all()")
god_player.permissions.add("Immortals") # this is necessary for quelling to work correctly.
# this is necessary for quelling to work correctly.
god_player.permissions.add("Immortals")
# Limbo is the default "nowhere" starting room
# Create the in-game god-character for player #1 and set it to exist in Limbo.
# Create the in-game god-character for player #1 and set
# it to exist in Limbo.
character_typeclass = settings.BASE_CHARACTER_TYPECLASS
god_character = create.create_object(character_typeclass, key=god_player.username, nohome=True)
god_character = create.create_object(character_typeclass,
key=god_player.username, nohome=True)
print "god_character:", character_typeclass, god_character, god_character.cmdset.all()
god_character.id = 1
god_character.db.desc = _('This is User #1.')
@ -81,10 +86,13 @@ def create_objects():
limbo_obj = create.create_object(room_typeclass, _('Limbo'), nohome=True)
limbo_obj.id = 2
string = " ".join([
"Welcome to your new {wEvennia{n-based game. From here you are ready to begin development.",
"Visit http://evennia.com if you should need help or would like to participate in community discussions.",
"If you are logged in as User #1 you can create a demo/tutorial area with '@batchcommand contrib.tutorial_world.build'.",
"Log out and create a new non-admin account at the login screen to play the tutorial properly."])
"Welcome to your new {wEvennia{n-based game. From here you are ready",
"to begin development. Visit http://evennia.com if you should need",
"help or would like to participate in community discussions. If you",
"are logged in as User #1 you can create a demo/tutorial area with",
"'@batchcommand contrib.tutorial_world.build'. Log out and create",
"a new non-admin account at the login screen to play the tutorial",
"properly."])
string = _(string)
limbo_obj.db.desc = string
limbo_obj.save()
@ -96,6 +104,7 @@ def create_objects():
if not god_character.home:
god_character.home = limbo_obj
def create_channels():
"""
Creates some sensible default channels.
@ -112,20 +121,21 @@ def create_channels():
key3, aliases, desc, locks = settings.CHANNEL_CONNECTINFO
cchan = create.create_channel(key3, aliases, desc, locks=locks)
# TODO: postgresql-psycopg2 has a strange error when trying to connect the user
# to the default channels. It works fine from inside the game, but not from
# the initial startup. We are temporarily bypassing the problem with the following
# fix. See Evennia Issue 151.
if ((".".join(str(i) for i in django.VERSION) < "1.2" and settings.DATABASE_ENGINE == "postgresql_psycopg2")
# TODO: postgresql-psycopg2 has a strange error when trying to
# connect the user to the default channels. It works fine from inside
# the game, but not from the initial startup. We are temporarily bypassing
# the problem with the following fix. See Evennia Issue 151.
if ((".".join(str(i) for i in django.VERSION) < "1.2"
and settings.DATABASE_ENGINE == "postgresql_psycopg2")
or (hasattr(settings, 'DATABASES')
and settings.DATABASES.get("default", {}).get('ENGINE', None)
== 'django.db.backends.postgresql_psycopg2')):
warning = """
PostgreSQL-psycopg2 compatability fix:
The in-game channels %s, %s and %s were created,
but the superuser was not yet connected to them. Please use in-game commands to
connect Player #1 to those channels when first logging in.
but the superuser was not yet connected to them. Please use in
game commands to onnect Player #1 to those channels when first
logging in.
""" % (key1, key2, key3)
print warning
return
@ -137,6 +147,7 @@ def create_channels():
PlayerChannelConnection.objects.create_connection(goduser, ichan)
PlayerChannelConnection.objects.create_connection(goduser, cchan)
def create_system_scripts():
"""
Setup the system repeat scripts. They are automatically started
@ -152,11 +163,10 @@ def create_system_scripts():
script2 = create.create_script(scripts.ValidateScripts)
# update the channel handler to make sure it's in sync
script3 = create.create_script(scripts.ValidateChannelHandler)
# clear the attribute cache regularly
#script4 = create.create_script(scripts.ClearAttributeCache)
if not script1 or not script2 or not script3:# or not script4:
if not script1 or not script2 or not script3:
print " Error creating system scripts."
def start_game_time():
"""
This starts a persistent script that keeps track of the
@ -168,6 +178,7 @@ def start_game_time():
from src.utils import gametime
gametime.init_gametime()
def create_admin_media_links():
"""
This traverses to src/web/media and tries to create a symbolic
@ -179,7 +190,8 @@ def create_admin_media_links():
since the django install may be at different locations depending
on system.
"""
import django, os
import django
import os
if django.get_version() < 1.4:
dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'media')
@ -202,6 +214,7 @@ def create_admin_media_links():
else:
print " Admin-media files should be copied manually to ADMIN_MEDIA_ROOT."
def at_initial_setup():
"""
Custom hook for users to overload some or all parts of the initial
@ -220,6 +233,7 @@ def at_initial_setup():
if mod.__dict__.get("at_initial_setup", None):
mod.at_initial_setup()
def reset_server():
"""
We end the initialization by resetting the server. This
@ -231,6 +245,7 @@ def reset_server():
print " Initial setup complete. Restarting Server once."
SESSIONS.server.shutdown(mode='reset')
def handle_setup(last_step):
"""
Main logic for the module. It allows for restarting
@ -242,7 +257,7 @@ def handle_setup(last_step):
# this means we don't need to handle setup since
# it already ran sucessfully once.
return
elif last_step == None:
elif last_step is None:
# config doesn't exist yet. First start of server
last_step = 0

View file

@ -3,6 +3,7 @@ Custom manager for ServerConfig objects.
"""
from django.db import models
class ServerConfigManager(models.Manager):
"""
This ServerConfigManager implements methods for searching
@ -24,16 +25,16 @@ class ServerConfigManager(models.Manager):
"""
if not key:
return self.all()
elif delete == True:
elif delete is True:
for conf in self.filter(db_key=key):
conf.delete()
elif value != None:
elif value is not None:
conf = self.filter(db_key=key)
if conf:
conf = conf[0]
else:
conf = self.model(db_key=key)
conf.value = value # this will pickle
conf.value = value # this will pickle
else:
conf = self.filter(db_key=key)
if not conf:
@ -42,7 +43,8 @@ class ServerConfigManager(models.Manager):
def get_mysql_db_version(self):
"""
This is a helper method for getting the version string of a mysql database.
This is a helper method for getting the version string
of a mysql database.
"""
from django.db import connection
conn = connection.cursor()

View file

@ -18,6 +18,7 @@ from src.utils.idmapper.models import SharedMemoryModel
from src.utils import logger, utils
from src.server.manager import ServerConfigManager
#------------------------------------------------------------
#
# ServerConfig
@ -61,11 +62,13 @@ class ServerConfig(SharedMemoryModel):
def __key_get(self):
"Getter. Allows for value = self.key"
return self.db_key
#@key.setter
def __key_set(self, value):
"Setter. Allows for self.key = value"
self.db_key = value
self.save()
#@key.deleter
def __key_del(self):
"Deleter. Allows for del self.key. Deletes entry."
@ -77,6 +80,7 @@ class ServerConfig(SharedMemoryModel):
def __value_get(self):
"Getter. Allows for value = self.value"
return pickle.loads(str(self.db_value))
#@value.setter
def __value_set(self, value):
"Setter. Allows for self.value = value"
@ -86,6 +90,7 @@ class ServerConfig(SharedMemoryModel):
return
self.db_value = pickle.dumps(value)
self.save()
#@value.deleter
def __value_del(self):
"Deleter. Allows for del self.value. Deletes entry."

View file

@ -8,13 +8,14 @@ from django.conf import settings
from src.utils.utils import to_str
_GA = object.__getattribute__
_SA = object.__setattr__
_NA = lambda o: (None, "N/A") # not implemented
_NA = lambda o: (None, "N/A") # not implemented
# mapper for which properties may be requested/sent to the client and how to do so.
# Each entry should define a function that returns two values - the name of the
# propertye being returned (a string) and the value. If tracking database fields,
# make sure to enter the full database field name (e.g. db_key rather than just key)
# since the db_ prefix is used by trackers to know which tracking mechanism to activate.
# mapper for which properties may be requested/sent to the client and how
# to do so. Each entry should define a function that returns two values - the
# name of the property being returned (a string) and the value. If tracking
# database fields, make sure to enter the full database field name (e.g.
# db_key rather than just key) since the db_ prefix is used by trackers
# to know which tracking mechanism to activate.
OOB_SENDABLE = {
## General
@ -97,13 +98,16 @@ class TrackerBase(object):
"""
def __init__(self, oobhandler, *args, **kwargs):
self.oobhandler = oobhandler
def update(self, *args, **kwargs):
"Called by tracked objects"
pass
def at_remove(self, *args, **kwargs):
"Called when tracker is removed"
pass
class OOBFieldTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
@ -127,7 +131,9 @@ class OOBFieldTracker(TrackerBase):
new_value = new_value.key
except AttributeError:
new_value = to_str(new_value, force_string=True)
self.oobhandler.msg(self.sessid, "report", self.fieldname, new_value, *args, **kwargs)
self.oobhandler.msg(self.sessid, "report", self.fieldname,
new_value, *args, **kwargs)
class OOBAttributeTracker(TrackerBase):
"""
@ -136,13 +142,13 @@ class OOBAttributeTracker(TrackerBase):
we instead store the name of the attribute to return.
"""
def __init__(self, oobhandler, fieldname, sessid, attrname, *args, **kwargs):
"""
attrname - name of attribute to track
sessid - sessid of session to report to
"""
self.oobhandler = oobhandler
self.attrname = attrname
self.sessid = sessid
"""
attrname - name of attribute to track
sessid - sessid of session to report to
"""
self.oobhandler = oobhandler
self.attrname = attrname
self.sessid = sessid
def update(self, new_value, *args, **kwargs):
"Called by cache when attribute's db_value field updates"
@ -152,6 +158,7 @@ class OOBAttributeTracker(TrackerBase):
new_value = to_str(new_value, force_string=True)
self.oobhandler.msg(self.sessid, "report", self.attrname, new_value, *args, **kwargs)
#------------------------------------------------------------
# OOB commands
# This defines which internal server commands the OOB handler
@ -173,31 +180,51 @@ def oob_error(oobhandler, session, errmsg, *args, **kwargs):
occurs already at the execution stage (such as the oob function
not being recognized or having the wrong args etc).
"""
session.msg(oob=("send", {"ERROR":errmsg}))
session.msg(oob=("send", {"ERROR": errmsg}))
def LIST(oobhandler, session, mode, *args, **kwargs):
"""
List available properties. Mode is the type of information
desired:
"COMMANDS" Request an array of commands supported by the server.
"LISTS" Request an array of lists supported by the server.
"CONFIGURABLE_VARIABLES" Request an array of variables the client can configure.
"REPORTABLE_VARIABLES" Request an array of variables the server will report.
"REPORTED_VARIABLES" Request an array of variables currently being reported.
"SENDABLE_VARIABLES" Request an array of variables the server will send.
"COMMANDS" Request an array of commands supported
by the server.
"LISTS" Request an array of lists supported
by the server.
"CONFIGURABLE_VARIABLES" Request an array of variables the client
can configure.
"REPORTABLE_VARIABLES" Request an array of variables the server
will report.
"REPORTED_VARIABLES" Request an array of variables currently
being reported.
"SENDABLE_VARIABLES" Request an array of variables the server
will send.
"""
mode = mode.upper()
# the first return argument is treated by the msdp protocol as the name of the msdp array to return
# the first return argument is treated by the msdp protocol as the
# name of the msdp array to return
if mode == "COMMANDS":
session.msg(oob=("list", ("COMMANDS", "LIST", "REPORT", "UNREPORT", "SEND"))) # RESET
session.msg(oob=("list", ("COMMANDS",
"LIST",
"REPORT",
"UNREPORT",
# "RESET",
"SEND")))
elif mode == "LISTS":
session.msg(oob=("list", ("LISTS", "REPORTABLE_VARIABLES","REPORTED_VARIABLES", "SENDABLE_VARIABLES"))) #CONFIGURABLE_VARIABLES
session.msg(oob=("list", ("LISTS",
"REPORTABLE_VARIABLES",
"REPORTED_VARIABLES",
# "CONFIGURABLE_VARIABLES",
"SENDABLE_VARIABLES")))
elif mode == "REPORTABLE_VARIABLES":
session.msg(oob=("list", ("REPORTABLE_VARIABLES",) + tuple(key for key in OOB_REPORTABLE.keys())))
session.msg(oob=("list", ("REPORTABLE_VARIABLES",) +
tuple(key for key in OOB_REPORTABLE.keys())))
elif mode == "REPORTED_VARIABLES":
session.msg(oob=("list", ("REPORTED_VARIABLES",) + tuple(oobhandler.get_all_tracked(session))))
session.msg(oob=("list", ("REPORTED_VARIABLES",) +
tuple(oobhandler.get_all_tracked(session))))
elif mode == "SENDABLE_VARIABLES":
session.msg(oob=("list", ("SENDABLE_VARIABLES",) + tuple(key for key in OOB_REPORTABLE.keys())))
session.msg(oob=("list", ("SENDABLE_VARIABLES",) +
tuple(key for key in OOB_REPORTABLE.keys())))
#elif mode == "CONFIGURABLE_VARIABLES":
# pass
else:
@ -221,6 +248,7 @@ def send(oobhandler, session, *args, **kwargs):
# return result
session.msg(oob=("send", ret))
def report(oobhandler, session, *args, **kwargs):
"""
This creates a tracker instance to track the data given in *args.
@ -232,9 +260,12 @@ def report(oobhandler, session, *args, **kwargs):
key, val = OOB_REPORTABLE.get(name, _NA)(obj)
if key:
if key.startswith("db_"):
oobhandler.track_field(obj, session.sessid, key, OOBFieldTracker)
else: # assume attribute
oobhandler.track_attribute(obj, session.sessid, key, OOBAttributeTracker)
oobhandler.track_field(obj, session.sessid,
key, OOBFieldTracker)
else: # assume attribute
oobhandler.track_attribute(obj, session.sessid,
key, OOBAttributeTracker)
def unreport(oobhandler, session, vartype="prop", *args, **kwargs):
"""
@ -248,6 +279,6 @@ def unreport(oobhandler, session, vartype="prop", *args, **kwargs):
if key:
if key.startswith("db_"):
oobhandler.untrack_field(obj, session.sessid, key)
else: # assume attribute
else: # assume attribute
oobhandler.untrack_attribute(obj, session.sessid, key)

View file

@ -5,14 +5,16 @@ The OOBHandler is called directly by out-of-band protocols. It supplies three
pieces of functionality:
function execution - the oob protocol can execute a function directly on
the server. Only functions specified in settings.OOB_PLUGIN_MODULE.OOB_FUNCS
are valid for this use.
repeat func execution - the oob protocol can request a given function be executed repeatedly
at a regular interval.
tracking - the oob protocol can request Evennia to track changes to fields on
objects, as well as changes in Attributes. This is done by dynamically adding
tracker-objects on entities. The behaviour of those objects can be customized
via settings.OOB_PLUGIN_MODULE
the server. Only functions specified in
settings.OOB_PLUGIN_MODULE.OOB_FUNCS are valid
for this use.
repeat func execution - the oob protocol can request a given function be
executed repeatedly at a regular interval.
tracking - the oob protocol can request Evennia to track changes to
fields on objects, as well as changes in Attributes. This is
done by dynamically adding tracker-objects on entities. The
behaviour of those objects can be customized via
settings.OOB_PLUGIN_MODULE
oob functions have the following call signature:
function(caller, *args, **kwargs)
@ -30,7 +32,7 @@ from src.scripts.scripts import Script
from src.utils.create import create_script
from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
from src.utils import logger
from src.utils.utils import all_from_module, to_str, is_iter, make_iter
from src.utils.utils import all_from_module
_SA = object.__setattr__
_GA = object.__getattribute__
@ -50,14 +52,18 @@ class TrackerHandler(object):
"""
def __init__(self, obj):
"""
This is initiated and stored on the object as a property _trackerhandler.
This is initiated and stored on the object as a
property _trackerhandler.
"""
try: obj = obj.dbobj
except AttributeError: pass
try:
obj = obj.dbobj
except AttributeError:
pass
self.obj = obj
self.ntrackers = 0
# initiate store only with valid on-object fieldnames
self.tracktargets = dict((key, {}) for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")())
self.tracktargets = dict((key, {})
for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")())
def add(self, fieldname, tracker):
"""
@ -95,19 +101,23 @@ class TrackerHandler(object):
except Exception:
logger.log_trace()
class TrackerBase(object):
"""
Base class for OOB Tracker objects.
"""
def __init__(self, *args, **kwargs):
pass
def update(self, *args, **kwargs):
"Called by tracked objects"
pass
def at_remove(self, *args, **kwargs):
"Called when tracker is removed"
pass
class _RepeaterScript(Script):
"""
Repeating and subscription-enabled script for triggering OOB
@ -117,7 +127,7 @@ class _RepeaterScript(Script):
"Called when script is initialized"
self.key = "oob_func"
self.desc = "OOB functionality script"
self.persistent = False #oob scripts should always be non-persistent
self.persistent = False # oob scripts should always be non-persistent
self.ndb.subscriptions = {}
def at_repeat(self):
@ -142,11 +152,12 @@ class _RepeaterScript(Script):
"""
self.ndb.subscriptions.pop(store_key, None)
class _RepeaterPool(object):
"""
This maintains a pool of _RepeaterScript scripts, ordered one per interval. It
will automatically cull itself once a given interval's script has no more
subscriptions.
This maintains a pool of _RepeaterScript scripts, ordered one per
interval. It will automatically cull itself once a given interval's
script has no more subscriptions.
This is used and accessed from oobhandler.repeat/unrepeat
"""
@ -160,9 +171,11 @@ class _RepeaterPool(object):
"""
if interval not in self.scripts:
# if no existing interval exists, create new script to fill the gap
new_tracker = create_script(_RepeaterScript, key="oob_repeater_%is" % interval, interval=interval)
new_tracker = create_script(_RepeaterScript,
key="oob_repeater_%is" % interval, interval=interval)
self.scripts[interval] = new_tracker
self.scripts[interval].subscribe(store_key, sessid, func_key, interval, *args, **kwargs)
self.scripts[interval].subscribe(store_key, sessid, func_key,
interval, *args, **kwargs)
def remove(self, store_key, interval):
"""
@ -176,8 +189,8 @@ class _RepeaterPool(object):
def stop(self):
"""
Stop all scripts in pool. This is done at server reload since restoring the pool
will automatically re-populate the pool.
Stop all scripts in pool. This is done at server reload since
restoring the pool will automatically re-populate the pool.
"""
for script in self.scripts.values():
script.stop()
@ -188,8 +201,8 @@ class _RepeaterPool(object):
class OOBHandler(object):
"""
The OOBHandler maintains all dynamic on-object oob hooks. It will store the
creation instructions and and re-apply them at a server reload (but not after
a server shutdown)
creation instructions and and re-apply them at a server reload (but
not after a server shutdown)
"""
def __init__(self):
"""
@ -207,10 +220,12 @@ class OOBHandler(object):
"""
if self.oob_tracker_storage:
#print "saved tracker_storage:", self.oob_tracker_storage
ServerConfig.objects.conf(key="oob_tracker_storage", value=dbserialize(self.oob_tracker_storage))
ServerConfig.objects.conf(key="oob_tracker_storage",
value=dbserialize(self.oob_tracker_storage))
if self.oob_repeat_storage:
#print "saved repeat_storage:", self.oob_repeat_storage
ServerConfig.objects.conf(key="oob_repeat_storage", value=dbserialize(self.oob_repeat_storage))
ServerConfig.objects.conf(key="oob_repeat_storage",
value=dbserialize(self.oob_repeat_storage))
self.oob_tracker_pool.stop()
def restore(self):
@ -242,12 +257,14 @@ class OOBHandler(object):
Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args,
kwargs will be used to initialize the OOB hook before adding
it to obj.
If property_key is not given, but the OOB has a class property property_name, this
will be used as the property name when assigning the OOB to
obj, otherwise tracker_key is used as the property name.
If property_key is not given, but the OOB has a class property
property_name, this will be used as the property name when assigning
the OOB to obj, otherwise tracker_key is used as the property name.
"""
try: obj = obj.dbobj
except AttributeError: pass
try:
obj = obj.dbobj
except AttributeError:
pass
if not "_trackerhandler" in _GA(obj, "__dict__"):
# assign trackerhandler to object
@ -266,8 +283,10 @@ class OOBHandler(object):
Remove the OOB from obj. If oob implements an
at_delete hook, this will be called with args, kwargs
"""
try: obj = obj.dbobj
except AttributeError: pass
try:
obj = obj.dbobj
except AttributeError:
pass
try:
# call at_delete hook
@ -278,7 +297,7 @@ class OOBHandler(object):
store_key = (pack_dbobj(obj), sessid, fieldname)
self.oob_tracker_storage.pop(store_key, None)
def get_all_tracked(session):
def get_all_tracked(self, session):
"""
Get the names of all variables this session is tracking.
"""
@ -304,12 +323,14 @@ class OOBHandler(object):
def track_attribute(self, obj, sessid, attr_name, trackerclass):
"""
Shortcut wrapper method for specifically tracking the changes of an
Attribute on an object. Will create a tracker on the Attribute Object and
name in a way the Attribute expects.
Attribute on an object. Will create a tracker on the Attribute
Object and name in a way the Attribute expects.
"""
# get the attribute object if we can
try: obj = obj.dbobj
except AttributeError: pass
try:
obj = obj.dbobj
except AttributeError:
pass
attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True)
if attrobj:
self.track(attrobj, sessid, "db_value", trackerclass, attr_name)
@ -318,8 +339,10 @@ class OOBHandler(object):
"""
Shortcut for deactivating tracking for a given attribute.
"""
try: obj = obj.dbobj
except AttributeError: pass
try:
obj = obj.dbobj
except AttributeError:
pass
attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True)
if attrobj:
self.untrack(attrobj, sessid, attr_name, trackerclass)
@ -335,7 +358,7 @@ class OOBHandler(object):
try:
obj = obj.dbobj
except AttributeError:
passj
pass
store_obj = pack_dbobj(obj)
store_key = (store_obj, sessid, func_key, interval)
# prepare to store
@ -363,7 +386,6 @@ class OOBHandler(object):
# access method - called from session.msg()
def execute_cmd(self, session, func_key, *args, **kwargs):
"""
Retrieve oobfunc from OOB_FUNCS and execute it immediately
@ -371,7 +393,7 @@ class OOBHandler(object):
"""
try:
#print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
oobfunc(self, session, *args, **kwargs)
except KeyError,e:
errmsg = "OOB Error: function '%s' not recognized: %s" % (func_key, e)

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -20,12 +20,14 @@ import zlib
MCCP = chr(86)
FLUSH = zlib.Z_SYNC_FLUSH
def mccp_compress(protocol, data):
"Handles zlib compression, if applicable"
if hasattr(protocol, 'zlib'):
return protocol.zlib.compress(data) + protocol.zlib.flush(FLUSH)
return data
class Mccp(object):
"""
Implements the MCCP protocol. Add this to a

View file

@ -9,9 +9,7 @@ etc.
"""
import re
from django.conf import settings
from src.utils.utils import make_iter, mod_import, to_str
from src.utils import logger
from src.utils.utils import to_str
# MSDP-relevant telnet cmd/opt-codes
MSDP = chr(69)
@ -29,11 +27,18 @@ SE = chr(240)
force_str = lambda inp: to_str(inp, force_string=True)
# pre-compiled regexes
regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY_OPEN, MSDP_ARRAY_CLOSE)) # return 2-tuple
regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE_OPEN, MSDP_TABLE_CLOSE)) # return 2-tuple (may be nested)
# returns 2-tuple
regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_ARRAY_OPEN,
MSDP_ARRAY_CLOSE))
# returns 2-tuple (may be nested)
regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_TABLE_OPEN,
MSDP_TABLE_CLOSE))
regex_var = re.compile(MSDP_VAR)
regex_val = re.compile(MSDP_VAL)
# Msdp object handler
class Msdp(object):
@ -90,7 +95,7 @@ class Msdp(object):
else:
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
string += MSDP_TABLE_CLOSE
return stringk
return string
def make_array(name, *args):
"build a array. Arrays may not nest tables by definition."
@ -169,7 +174,7 @@ class Msdp(object):
tables[key] = {}
for varval in regex_var.split(table):
parts = regex_val.split(varval)
tables[key].expand({parts[0] : tuple(parts[1:]) if len(parts)>1 else ("",)})
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)})
for key, array in regex_array.findall(data):
arrays[key] = []
for val in regex_val.split(array):
@ -178,16 +183,18 @@ class Msdp(object):
for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))):
# get remaining varvals after cleaning away tables/arrays
parts = regex_val.split(varval)
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts)>1 else ("", )
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", )
#print "MSDP: table, array, variables:", tables, arrays, variables
# all variables sent through msdp to Evennia are considered commands with arguments.
# there are three forms of commands possible through msdp:
# all variables sent through msdp to Evennia are considered commands
# with arguments. There are three forms of commands possible
# through msdp:
#
# VARNAME VAR -> varname(var)
# ARRAYNAME VAR VAL VAR VAL VAR VAL ENDARRAY -> arrayname(val,val,val)
# TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE -> tablename(varname=val, varname=val)
# TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE ->
# tablename(varname=val, varname=val)
#
# default MSDP functions
@ -232,82 +239,4 @@ class Msdp(object):
Send oob data to Evennia
"""
#print "msdp data_in:", funcname, args, kwargs
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))
# # MSDP Commands
# # Some given MSDP (varname, value) pairs can also be treated as command + argument.
# # Generic msdp command map. The argument will be sent to the given command.
# # See http://tintin.sourceforge.net/msdp/ for definitions of each command.
# # These are client->server commands.
# def msdp_cmd_list(self, arg):
# """
# The List command allows for retrieving various info about the server/client
# """
# if arg == 'COMMANDS':
# return self.evennia_to_msdp(arg, MSDP_COMMANDS)
# elif arg == 'LISTS':
# return self.evennia_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES",
# "REPORTED_VARIABLES", "SENDABLE_VARIABLES"))
# elif arg == 'CONFIGURABLE_VARIABLES':
# return self.evennia_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID"))
# elif arg == 'REPORTABLE_VARIABLES':
# return self.evennia_to_msdp(arg, MSDP_REPORTABLE.keys())
# elif arg == 'REPORTED_VARIABLES':
# # the dynamically set items to report
# return self.evennia_to_msdp(arg, self.msdp_reported.keys())
# elif arg == 'SENDABLE_VARIABLES':
# return self.evennia_to_msdp(arg, MSDP_SENDABLE.keys())
# else:
# return self.evennia_to_msdp("LIST", arg)
# # default msdp commands
# def msdp_cmd_report(self, *arg):
# """
# The report command instructs the server to start reporting a
# reportable variable to the client.
# """
# try:
# return MSDP_REPORTABLE[arg](report=True)
# except Exception:
# logger.log_trace()
# def msdp_cmd_unreport(self, arg):
# """
# Unreport a previously reported variable
# """
# try:
# MSDP_REPORTABLE[arg](report=False)
# except Exception:
# self.logger.log_trace()
# def msdp_cmd_reset(self, arg):
# """
# The reset command resets a variable to its initial state.
# """
# try:
# MSDP_REPORTABLE[arg](reset=True)
# except Exception:
# logger.log_trace()
# def msdp_cmd_send(self, *args):
# """
# Request the server to send a particular variable
# to the client.
# arg - this is a list of variables the client wants.
# """
# ret = []
# for var in make_iter(arg)
# for var in make_iter(arg):
# try:
# ret.append(MSDP_REPORTABLE[var.upper()])# (send=True))
# except Exception:
# ret.append("ERROR")#logger.log_trace()
# return ret
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))

View file

@ -83,7 +83,7 @@ class Portal(object):
# create a store of services
self.services = service.IServiceCollection(application)
self.amp_protocol = None # set by amp factory
self.amp_protocol = None # set by amp factory
self.sessions = PORTAL_SESSIONS
self.sessions.portal = self
@ -99,7 +99,7 @@ class Portal(object):
be restarted or is shutting down. Valid modes are True/False and None.
If mode is None, no change will be done to the flag file.
"""
if mode == None:
if mode is None:
return
f = open(PORTAL_RESTART, 'w')
print "writing mode=%(mode)s to %(portal_restart)s" % {'mode': mode, 'portal_restart': PORTAL_RESTART}
@ -211,17 +211,20 @@ if SSL_ENABLED:
factory = protocol.ServerFactory()
factory.sessionhandler = PORTAL_SESSIONS
factory.protocol = ssl.SSLProtocol
ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface)
ssl_service = internet.SSLServer(port,
factory,
ssl.getSSLContext(),
interface=interface)
ssl_service.setName('EvenniaSSL%s' % pstring)
PORTAL.services.addService(ssl_service)
print " ssl%s: %s" % (ifacestr, port)
if SSH_ENABLED:
# Start SSH game connections. Will create a keypair in evennia/game if necessary.
# Start SSH game connections. Will create a keypair in
# evennia/game if necessary.
from src.server.portal import ssh
@ -234,9 +237,9 @@ if SSH_ENABLED:
ifacestr = "-%s" % interface
for port in SSH_PORTS:
pstring = "%s:%s" % (ifacestr, port)
factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol,
'protocolArgs':(),
'sessions':PORTAL_SESSIONS})
factory = ssh.makeFactory({'protocolFactory': ssh.SshProtocol,
'protocolArgs': (),
'sessions': PORTAL_SESSIONS})
ssh_service = internet.TCPServer(port, factory, interface=interface)
ssh_service.setName('EvenniaSSH%s' % pstring)
PORTAL.services.addService(ssh_service)
@ -247,8 +250,6 @@ if WEBSERVER_ENABLED:
# Start a reverse proxy to relay data to the Server-side webserver
from twisted.web import proxy
for interface in WEBSERVER_INTERFACES:
if ":" in interface:
print " iPv6 interfaces not yet supported"
@ -269,7 +270,9 @@ if WEBSERVER_ENABLED:
webclientstr = "/client"
web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
proxy_service = internet.TCPServer(proxyport, web_root, interface=interface)
proxy_service = internet.TCPServer(proxyport,
web_root,
interface=interface)
proxy_service.setName('EvenniaWebProxy%s' % pstring)
PORTAL.services.addService(proxy_service)
print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport)
@ -278,7 +281,7 @@ for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES:
# external plugin services to start
plugin_module.start_plugin_services(PORTAL)
print '-' * 50 # end of terminal output
print '-' * 50 # end of terminal output
if os.name == 'nt':
# Windows only: Set PID file manually

View file

@ -4,6 +4,7 @@ Sessionhandler for portal sessions
import time
from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN
#------------------------------------------------------------
# Portal-SessionHandler class
#------------------------------------------------------------
@ -39,8 +40,8 @@ class PortalSessionHandler(SessionHandler):
def connect(self, session):
"""
Called by protocol at first connect. This adds a not-yet authenticated session
using an ever-increasing counter for sessid.
Called by protocol at first connect. This adds a not-yet
authenticated session using an ever-increasing counter for sessid.
"""
self.latest_sessid += 1
sessid = self.latest_sessid
@ -48,13 +49,15 @@ class PortalSessionHandler(SessionHandler):
sessdata = session.get_sync_data()
self.sessions[sessid] = session
# sync with server-side
if self.portal.amp_protocol: # this is a timing issue
if self.portal.amp_protocol: # this is a timing issue
self.portal.amp_protocol.call_remote_ServerAdmin(sessid,
operation=PCONN,
data=sessdata)
def disconnect(self, session):
"""
Called from portal side when the connection is closed from the portal side.
Called from portal side when the connection is closed
from the portal side.
"""
sessid = session.sessid
if sessid in self.sessions:
@ -86,18 +89,22 @@ class PortalSessionHandler(SessionHandler):
self.sessions = {}
def server_logged_in(self, sessid, data):
"The server tells us that the session has been authenticated. Updated it."
"""
The server tells us that the session has been
authenticated. Updated it.
"""
sess = self.get_session(sessid)
sess.load_sync_data(data)
def server_session_sync(self, serversessions):
"""
Server wants to save data to the portal, maybe because it's about to shut down.
We don't overwrite any sessions here, just update them in-place and remove
any that are out of sync (which should normally not be the case)
Server wants to save data to the portal, maybe because it's about
to shut down. We don't overwrite any sessions here, just update
them in-place and remove any that are out of sync (which should
normally not be the case)
serversessions - dictionary {sessid:{property:value},...} describing the properties
to sync on all sessions
serversessions - dictionary {sessid:{property:value},...} describing
the properties to sync on all sessions
"""
to_save = [sessid for sessid in serversessions if sessid in self.sessions]
to_delete = [sessid for sessid in self.sessions if sessid not in to_save]
@ -131,6 +138,7 @@ class PortalSessionHandler(SessionHandler):
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
msg=text,
data=kwargs)
def announce_all(self, message):
"""
Send message to all connection sessions
@ -138,7 +146,6 @@ class PortalSessionHandler(SessionHandler):
for session in self.sessions.values():
session.data_out(message)
def data_out(self, sessid, text=None, **kwargs):
"""
Called by server for having the portal relay messages and data

View file

@ -26,7 +26,7 @@ from twisted.python import components
from django.conf import settings
from src.server import session
from src.players.models import PlayerDB
from src.utils import ansi, utils, logger
from src.utils import ansi, utils
ENCODINGS = settings.ENCODINGS
@ -35,6 +35,7 @@ CTRL_D = '\x04'
CTRL_BACKSLASH = '\x1c'
CTRL_L = '\x0c'
class SshProtocol(Manhole, session.Session):
"""
Each player connecting over ssh gets this protocol assigned to
@ -47,7 +48,8 @@ class SshProtocol(Manhole, session.Session):
login automatically.
"""
self.authenticated_player = starttuple[0]
self.cfactory = starttuple[1] # obs may not be called self.factory, it gets overwritten!
# obs must not be called self.factory, that gets overwritten!
self.cfactory = starttuple[1]
def terminalSize(self, width, height):
"""
@ -95,7 +97,6 @@ class SshProtocol(Manhole, session.Session):
self.terminal.write("KeyboardInterrupt")
self.terminal.nextLine()
def handle_EOF(self):
"""
Handles EOF generally used to exit.
@ -105,7 +106,6 @@ class SshProtocol(Manhole, session.Session):
else:
self.handle_QUIT()
def handle_FF(self):
"""
Handle a 'form feed' byte - generally used to request a screen
@ -114,14 +114,12 @@ class SshProtocol(Manhole, session.Session):
self.terminal.eraseDisplay()
self.terminal.cursorHome()
def handle_QUIT(self):
"""
Quit, end, and lose the connection.
"""
self.terminal.loseConnection()
def connectionLost(self, reason=None):
"""
This is executed when the connection is lost for
@ -140,9 +138,7 @@ class SshProtocol(Manhole, session.Session):
"""
return self.terminal.transport.getPeer()
def lineReceived(self, string):
"""
Communication Player -> Evennia. Any line return indicates a
command for the purpose of the MUD. So we take the user input
@ -159,10 +155,10 @@ class SshProtocol(Manhole, session.Session):
"""
for line in string.split('\n'):
self.terminal.write(line) #this is the telnet-specific method for sending
#this is the telnet-specific method for sending
self.terminal.write(line)
self.terminal.nextLine()
# session-general method hooks
def disconnect(self, reason="Connection closed. Goodbye for now."):
@ -175,7 +171,8 @@ class SshProtocol(Manhole, session.Session):
def data_out(self, text=None, **kwargs):
"""
Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings.
Data Evennia -> Player access hook. 'data' argument is a dict
parsed for string settings.
ssh flags:
raw=True - leave all ansi markup and tokens unparsed
@ -190,9 +187,9 @@ class SshProtocol(Manhole, session.Session):
raw = kwargs.get("raw", False)
nomarkup = kwargs.get("nomarkup", False)
if raw:
self.lineSend(string)
self.lineSend(text)
else:
self.lineSend(ansi.parse_ansi(string.strip("{r") + "{r", strip_ansi=nomarkup))
self.lineSend(ansi.parse_ansi(text.strip("{r") + "{r", strip_ansi=nomarkup))
class ExtraInfoAuthServer(SSHUserAuthServer):
@ -209,6 +206,7 @@ class ExtraInfoAuthServer(SSHUserAuthServer):
return self.portal.login(c, None, IConchUser).addErrback(
self._ebPassword)
class PlayerDBPasswordChecker(object):
"""
Checks the django db for the correct credentials for
@ -232,6 +230,7 @@ class PlayerDBPasswordChecker(object):
res = (player, self.factory)
return defer.succeed(res)
class PassAvatarIdTerminalRealm(TerminalRealm):
"""
Returns an avatar that passes the avatarId through to the
@ -244,7 +243,7 @@ class PassAvatarIdTerminalRealm(TerminalRealm):
sess = self.sessionFactory(comp)
sess.transportFactory = self.transportFactory
sess.chainedProtocolFactory = lambda : self.chainedProtocolFactory(avatarId)
sess.chainedProtocolFactory = lambda: self.chainedProtocolFactory(avatarId)
comp.setComponent(iconch.IConchUser, user)
comp.setComponent(iconch.ISession, sess)
@ -252,7 +251,6 @@ class PassAvatarIdTerminalRealm(TerminalRealm):
return user
class TerminalSessionTransport_getPeer:
"""
Taken from twisted's TerminalSessionTransport which doesn't
@ -345,4 +343,4 @@ def makeFactory(configdict):
factory.portal.registerChecker(PlayerDBPasswordChecker(factory))
return factory
return factory

View file

@ -3,7 +3,8 @@ This is a simple context factory for auto-creating
SSL keys and certificates.
"""
import os, sys
import os
import sys
from twisted.internet import ssl as twisted_ssl
try:
import OpenSSL
@ -13,6 +14,7 @@ except ImportError:
from src.server.portal.telnet import TelnetProtocol
class SSLProtocol(TelnetProtocol):
"""
Communication is the same as telnet, except data transfer
@ -20,6 +22,7 @@ class SSLProtocol(TelnetProtocol):
"""
pass
def verify_SSL_key_and_cert(keyfile, certfile):
"""
This function looks for RSA key and certificate in the current
@ -41,24 +44,27 @@ def verify_SSL_key_and_cert(keyfile, certfile):
rsaKey = Key(RSA.generate(KEY_LENGTH))
keyString = rsaKey.toString(type="OPENSSH")
file(keyfile, 'w+b').write(keyString)
except Exception,e:
except Exception, e:
print "rsaKey error: %(e)s\n WARNING: Evennia could not auto-generate SSL private key." % {'e': e}
print "If this error persists, create game/%(keyfile)s yourself using third-party tools." % {'keyfile': keyfile}
sys.exit(5)
# try to create the certificate
CERT_EXPIRE = 365 * 20 # twenty years validity
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)
#print "exestring:", exestring
try:
err = subprocess.call(exestring)#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
subprocess.call(exestring)
except OSError, e:
string = "\n".join([
" %s\n" % e,
" Evennia's SSL context factory could not automatically create an SSL certificate game/%(cert)s." % {'cert': certfile},
" A private key 'ssl.key' was already created. Please create %(cert)s manually using the commands valid" % {'cert': certfile},
" Evennia's SSL context factory could not automatically",
" create an SSL certificate game/%(cert)s." % {'cert': certfile},
" A private key 'ssl.key' was already created. Please",
" create %(cert)s manually using the commands valid" % {'cert': certfile},
" for your operating system.",
" Example (linux, using the openssl program): ",
" %s" % exestring])
@ -66,6 +72,7 @@ def verify_SSL_key_and_cert(keyfile, certfile):
sys.exit(5)
print "done."
def getSSLContext():
"""
Returns an SSL context (key and certificate). This function

View file

@ -9,15 +9,14 @@ sessions etc.
import re
from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE
from twisted.internet.defer import inlineCallbacks, returnValue
from src.server.session import Session
from src.server.portal import ttype, mssp, msdp
from src.server.portal.mccp import Mccp, mccp_compress, MCCP
from src.utils import utils, ansi, logger
from src.utils.utils import make_iter, is_iter
_RE_N = re.compile(r"\{n$")
class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
"""
Each player connecting over telnet (ie using most traditional mud
@ -127,7 +126,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
def _write(self, data):
"hook overloading the one used in plain telnet"
#print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in data))
# print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in data))
data = data.replace('\n', '\r\n').replace('\r\r\n', '\r\n')
#data = data.replace('\n', '\r\n')
super(TelnetProtocol, self)._write(mccp_compress(self, data))
@ -147,7 +146,6 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
"""
self.data_in(text=string)
# Session hooks
def disconnect(self, reason=None):
@ -172,11 +170,13 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
through the telnet connection.
valid telnet kwargs:
raw=True - pass string through without any ansi processing (i.e. include Evennia
ansi markers but do not convert them into ansi tokens)
raw=True - pass string through without any ansi
processing (i.e. include Evennia ansi markers but do
not convert them into ansi tokens)
nomarkup=True - strip all ansi markup
The telnet ttype negotiation flags, if any, are used if no kwargs are given.
The telnet ttype negotiation flags, if any, are used if no kwargs
are given.
"""
try:
text = utils.to_str(text if text else "", encoding=self.encoding)
@ -200,6 +200,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
# no processing whatsoever
self.sendLine(text)
else:
# we need to make sure to kill the color at the end in order to match the webclient output.
#print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self)
# we need to make sure to kill the color at the end in order
# to match the webclient output.
# print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self)
self.sendLine(ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=ttype.get('256 COLORS')))

View file

@ -12,12 +12,12 @@ under the 'TTYPE' key.
"""
# telnet option codes
TTYPE = chr(24)
TTYPE = chr(24)
IS = chr(0)
SEND = chr(1)
# terminal capabilities and their codes
MTTS = [(128,'PROXY'),
MTTS = [(128, 'PROXY'),
(64, 'SCREEN READER'),
(32, 'OSC COLOR PALETTE'),
(16, 'MOUSE TRACKING'),
@ -27,8 +27,8 @@ MTTS = [(128,'PROXY'),
(1, 'ANSI')]
# some clients sends erroneous strings instead
# of capability numbers. We try to convert back.
MTTS_invert = {"PROXY":128,
"SCREEN COLOR PALETTE":64,
MTTS_invert = {"PROXY": 128,
"SCREEN COLOR PALETTE": 64,
"OSC COLOR PALETTE": 32,
"MOUSE TRACKING": 16,
"256 COLORS": 8,
@ -36,6 +36,7 @@ MTTS_invert = {"PROXY":128,
"VT100": 2,
"ANSI": 1}
class Ttype(object):
"""
Handles ttype negotiations. Called and initiated by the
@ -51,7 +52,7 @@ class Ttype(object):
"""
self.ttype_step = 0
self.protocol = protocol
self.protocol.protocol_flags['TTYPE'] = {"init_done":False}
self.protocol.protocol_flags['TTYPE'] = {"init_done": False}
# setup protocol to handle ttype initialization and negotiation
self.protocol.negotiationMap[TTYPE] = self.do_ttype
# ask if client will ttype, connect callback if it does.
@ -61,7 +62,7 @@ class Ttype(object):
"""
Callback if ttype is not supported by client.
"""
self.protocol.protocol_flags['TTYPE'] = {"init_done":True}
self.protocol.protocol_flags['TTYPE'] = {"init_done": True}
def do_ttype(self, option):
"""
@ -95,9 +96,9 @@ class Ttype(object):
try:
option = int(option.strip('MTTS '))
except ValueError:
# it seems some clients don't send MTTS according to protocol
# specification, but instead just sends the data as plain
# strings. We try to convert back.
# it seems some clients don't send MTTS according to
# protocol specification, but instead just sends
# the data as plain strings. We try to convert back.
option = MTTS_invert.get(option.strip('MTTS ').upper())
if not option:
# no conversion possible. Give up.

View file

@ -20,19 +20,19 @@ import time
from hashlib import md5
from twisted.web import server, resource
from twisted.internet import defer, reactor
from django.utils import simplejson
from django.utils.functional import Promise
from django.utils.encoding import force_unicode
from django.conf import settings
from src.utils import utils, logger, ansi
from src.utils import utils, logger
from src.utils.text2html import parse_html
from src.server import session
SERVERNAME = settings.SERVERNAME
ENCODINGS = settings.ENCODINGS
# defining a simple json encoder for returning
# django data to the client. Might need to
# extend this if one wants to send more
@ -43,6 +43,8 @@ class LazyEncoder(simplejson.JSONEncoder):
if isinstance(obj, Promise):
return force_unicode(obj)
return super(LazyEncoder, self).default(obj)
def jsonify(obj):
return utils.to_str(simplejson.dumps(obj, ensure_ascii=False, cls=LazyEncoder))
@ -84,23 +86,23 @@ class WebClient(resource.Resource):
request = self.requests.get(suid)
if request:
# we have a request waiting. Return immediately.
request.write(jsonify({'msg':string, 'data':data}))
request.write(jsonify({'msg': string, 'data': data}))
request.finish()
del self.requests[suid]
else:
# no waiting request. Store data in buffer
dataentries = self.databuffer.get(suid, [])
dataentries.append(jsonify({'msg':string, 'data':data}))
dataentries.append(jsonify({'msg': string, 'data': data}))
self.databuffer[suid] = dataentries
def client_disconnect(self, suid):
"""
Disconnect session with given suid.
"""
if self.requests.has_key(suid):
if suid in self.requests:
self.requests[suid].finish()
del self.requests[suid]
if self.databuffer.has_key(suid):
if suid in self.databuffer:
del self.databuffer[suid]
def mode_init(self, request):
@ -108,7 +110,8 @@ class WebClient(resource.Resource):
This is called by render_POST when the client
requests an init mode operation (at startup)
"""
#csess = request.getSession() # obs, this is a cookie, not an evennia session!
#csess = request.getSession() # obs, this is a cookie, not
# an evennia session!
#csees.expireCallbacks.append(lambda : )
suid = request.args.get('suid', ['0'])[0]
@ -124,7 +127,7 @@ class WebClient(resource.Resource):
sess.init_session("webclient", remote_addr, self.sessionhandler)
sess.suid = suid
sess.sessionhandler.connect(sess)
return jsonify({'msg':host_string, 'suid':suid})
return jsonify({'msg': host_string, 'suid': suid})
def mode_input(self, request):
"""
@ -158,8 +161,8 @@ class WebClient(resource.Resource):
if dataentries:
return dataentries.pop(0)
request.notifyFinish().addErrback(self._responseFailed, suid, request)
if self.requests.has_key(suid):
self.requests[suid].finish() # Clear any stale request.
if suid in self.requests:
self.requests[suid].finish() # Clear any stale request.
self.requests[suid] = request
return server.NOT_DONE_YET
@ -206,6 +209,7 @@ class WebClient(resource.Resource):
# this should not happen if client sends valid data.
return ''
#
# A session type handling communication over the
# web client interface.
@ -241,7 +245,8 @@ class WebClientSession(session.Session):
if raw:
self.client.lineSend(self.suid, text)
else:
self.client.lineSend(self.suid, parse_html(text, strip_ansi=nomarkup))
self.client.lineSend(self.suid,
parse_html(text, strip_ansi=nomarkup))
return
except Exception:
logger.log_trace()

View file

@ -32,12 +32,11 @@ from src.server.sessionhandler import SESSIONS
# setting up server-side field cache
from django.db.models.signals import pre_save, post_save
from django.db.models.signals import post_save
from src.server.caches import field_pre_save
#pre_save.connect(field_pre_save, dispatch_uid="fieldcache")
post_save.connect(field_pre_save, dispatch_uid="fieldcache")
from src.typeclasses.models import TypedObject
#from src.server.caches import post_attr_update
#from django.db.models.signals import m2m_changed
@ -104,7 +103,7 @@ class Evennia(object):
# create a store of services
self.services = service.IServiceCollection(application)
self.amp_protocol = None # set by amp factory
self.amp_protocol = None # set by amp factory
self.sessions = SESSIONS
self.sessions.server = self
@ -121,7 +120,8 @@ class Evennia(object):
# set a callback if the server is killed abruptly,
# 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 = True
@ -146,38 +146,48 @@ class Evennia(object):
def update_defaults(self):
"""
We make sure to store the most important object defaults here, so we can catch if they
change and update them on-objects automatically. This allows for changing default cmdset locations
and default typeclasses in the settings file and have them auto-update all already existing
objects.
We make sure to store the most important object defaults here, so
we can catch if they change and update them on-objects automatically.
This allows for changing default cmdset locations and default
typeclasses in the settings file and have them auto-update all
already existing objects.
"""
# setting names
settings_names = ("CMDSET_CHARACTER", "CMDSET_PLAYER", "BASE_PLAYER_TYPECLASS", "BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS", "BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS")
settings_names = ("CMDSET_CHARACTER", "CMDSET_PLAYER",
"BASE_PLAYER_TYPECLASS", "BASE_OBJECT_TYPECLASS",
"BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS",
"BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS")
# get previous and current settings so they can be compared
settings_compare = zip([ServerConfig.objects.conf(name) for name in settings_names],
[settings.__getattr__(name) for name in settings_names])
mismatches = [i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]]
if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any()
# we have a changed default. Import relevant objects and run the update
if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any()
# we have a changed default. Import relevant objects and
# run the update
from src.objects.models import ObjectDB
#from src.players.models import PlayerDB
for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches):
# update the database
print " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (settings_names[i], prev, curr)
if i == 0: [obj.__setattr__("cmdset_storage", curr) for obj in ObjectDB.objects.filter(db_cmdset_storage__exact=prev)]
if i == 1: [ply.__setattr__("cmdset_storage", curr) for ply in PlayerDB.objects.filter(db_cmdset_storage__exact=prev)]
if i == 2: [ply.__setattr__("typeclass_path", curr) for ply in PlayerDB.objects.filter(db_typeclass_path__exact=prev)]
if i in (3,4,5,6): [obj.__setattr__("typeclass_path",curr)
for obj in ObjectDB.objects.filter(db_typeclass_path__exact=prev)]
if i == 7: [scr.__setattr__("typeclass_path", curr) for scr in ScriptDB.objects.filter(db_typeclass_path__exact=prev)]
if i == 0:
[obj.__setattr__("cmdset_storage", curr) for obj in ObjectDB.objects.filter(db_cmdset_storage__exact=prev)]
if i == 1:
[ply.__setattr__("cmdset_storage", curr) for ply in PlayerDB.objects.filter(db_cmdset_storage__exact=prev)]
if i == 2:
[ply.__setattr__("typeclass_path", curr) for ply in PlayerDB.objects.filter(db_typeclass_path__exact=prev)]
if i in (3, 4, 5, 6):
[obj.__setattr__("typeclass_path", curr) for obj in ObjectDB.objects.filter(db_typeclass_path__exact=prev)]
if i == 7:
[scr.__setattr__("typeclass_path", curr) for scr in ScriptDB.objects.filter(db_typeclass_path__exact=prev)]
# store the new default and clean caches
ServerConfig.objects.conf(settings_names[i], curr)
ObjectDB.flush_instance_cache()
PlayerDB.flush_instance_cache()
ScriptDB.flush_instance_cache()
# if this is the first start we might not have a "previous" setup saved. Store it now.
[ServerConfig.objects.conf(settings_names[i], tup[1]) for i, tup in enumerate(settings_compare) if not tup[0]]
# if this is the first start we might not have a "previous"
# setup saved. Store it now.
[ServerConfig.objects.conf(settings_names[i], tup[1])
for i, tup in enumerate(settings_compare) if not tup[0]]
def run_initial_setup(self):
"""
@ -191,7 +201,7 @@ class Evennia(object):
# i.e. this is an empty DB that needs populating.
print ' Server started for the first time. Setting defaults.'
initial_setup.handle_setup(0)
print '-'*50
print '-' * 50
elif int(last_initial_setup_step) >= 0:
# a positive value means the setup crashed on one of its
# modules and setup will resume from this step, retrying
@ -200,7 +210,7 @@ class Evennia(object):
print ' Resuming initial setup from step %(last)s.' % \
{'last': last_initial_setup_step}
initial_setup.handle_setup(int(last_initial_setup_step))
print '-'*50
print '-' * 50
def run_init_hooks(self):
"""
@ -244,7 +254,7 @@ class Evennia(object):
Either way, the active restart setting (Restart=True/False) is
returned so the server knows which more it's in.
"""
if mode == None:
if mode is None:
with open(SERVER_RESTART, 'r') as f:
# mode is either shutdown, reset or reload
mode = f.read()
@ -259,16 +269,20 @@ class Evennia(object):
Shuts down the server from inside it.
mode - sets the server restart mode.
'reload' - server restarts, no "persistent" scripts are stopped, at_reload hooks called.
'reset' - server restarts, non-persistent scripts stopped, at_shutdown hooks called.
'reload' - server restarts, no "persistent" scripts
are stopped, at_reload hooks called.
'reset' - server restarts, non-persistent scripts stopped,
at_shutdown hooks called.
'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 command OR this method was already called
once - in both cases the reactor is dead/stopping already.
_reactor_stopping - this is set if server is stopped by a kill
command OR this method was already called
once - in both cases the reactor is
dead/stopping already.
"""
if _reactor_stopping and hasattr(self, "shutdown_complete"):
# this means we have already passed through this method once; we don't need
# to run the shutdown procedure again.
# this means we have already passed through this method
# once; we don't need to run the shutdown procedure again.
defer.returnValue(None)
mode = self.set_restart_mode(mode)
@ -280,9 +294,12 @@ class Evennia(object):
if mode == 'reload':
# call restart hooks
yield [(o.typeclass, o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()]
yield [(p.typeclass, p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()]
yield [(s.typeclass, s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()]
yield [(o.typeclass, o.at_server_reload())
for o in ObjectDB.get_all_cached_instances()]
yield [(p.typeclass, p.at_server_reload())
for p in PlayerDB.get_all_cached_instances()]
yield [(s.typeclass, s.pause(), s.at_server_reload())
for s in ScriptDB.get_all_cached_instances()]
yield self.sessions.all_sessions_portal_sync()
ServerConfig.objects.conf("server_restart_mode", "reload")
@ -294,14 +311,20 @@ class Evennia(object):
else:
if mode == 'reset':
# don't unset the is_connected flag on reset, otherwise same as shutdown
yield [(o.typeclass, o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
else: # shutdown
yield [_SA(p, "is_connected", False) for p in PlayerDB.get_all_cached_instances()]
yield [(o.typeclass, o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()]
# don't unset the is_connected flag on reset, otherwise
# same as shutdown
yield [(o.typeclass, o.at_server_shutdown())
for o in ObjectDB.get_all_cached_instances()]
else: # shutdown
yield [_SA(p, "is_connected", False)
for p in PlayerDB.get_all_cached_instances()]
yield [(o.typeclass, o.at_server_shutdown())
for o in ObjectDB.get_all_cached_instances()]
yield [(p.typeclass, p.unpuppet_all(), p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()]
yield [(s.typeclass, s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()]
yield [(p.typeclass, p.unpuppet_all(), p.at_server_shutdown())
for p in PlayerDB.get_all_cached_instances()]
yield [(s.typeclass, s.at_server_shutdown())
for s in ScriptDB.get_all_cached_instances()]
yield ObjectDB.objects.clear_all_sessids()
ServerConfig.objects.conf("server_restart_mode", "reset")
@ -310,12 +333,14 @@ class Evennia(object):
if SERVER_STARTSTOP_MODULE:
SERVER_STARTSTOP_MODULE.at_server_stop()
# if _reactor_stopping is true, reactor does not need to be stopped again.
# if _reactor_stopping is true, reactor does not need to
# be stopped again.
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
# for Windows we need to remove pid files manually
os.remove(SERVER_PIDFILE)
if not _reactor_stopping:
# this will also send a reactor.stop signal, so we set a flag to avoid loops.
# this will also send a reactor.stop signal, so we set a
# flag to avoid loops.
self.shutdown_complete = True
reactor.callLater(0, reactor.stop)
@ -415,7 +440,7 @@ for plugin_module in SERVER_SERVICES_PLUGIN_MODULES:
# external plugin protocols
plugin_module.start_plugin_services(EVENNIA)
print '-' * 50 # end of terminal output
print '-' * 50 # end of terminal output
# clear server startup mode
ServerConfig.objects.conf("server_starting_mode", delete=True)

View file

@ -10,7 +10,7 @@ are stored on the Portal side)
import time
from datetime import datetime
from django.conf import settings
from src.scripts.models import ScriptDB
#from src.scripts.models import ScriptDB
from src.comms.models import ChannelDB
from src.utils import logger, utils
from src.utils.utils import make_iter, to_str
@ -53,7 +53,8 @@ class ServerSession(Session):
self.cmdset = cmdsethandler.CmdSetHandler(self)
def __cmdset_storage_get(self):
return [path.strip() for path in self.cmdset_storage_string.split(',')]
return [path.strip() for path in self.cmdset_storage_string.split(',')]
def __cmdset_storage_set(self, value):
self.cmdset_storage_string = ",".join(str(val).strip() for val in make_iter(value))
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set)
@ -61,8 +62,8 @@ class ServerSession(Session):
def at_sync(self):
"""
This is called whenever a session has been resynced with the portal.
At this point all relevant attributes have already been set and self.player
been assigned (if applicable).
At this point all relevant attributes have already been set and
self.player been assigned (if applicable).
Since this is often called after a server restart we need to set up
the session as it was.
@ -78,7 +79,8 @@ class ServerSession(Session):
self.cmdset.update(init_mode=True)
if self.puid:
# reconnect puppet (puid is only set if we are coming back from a server reload)
# reconnect puppet (puid is only set if we are coming
# back from a server reload)
obj = _ObjectDB.objects.get(id=self.puid)
self.player.puppet_object(self.sessid, obj, normal_mode=False)
@ -139,7 +141,8 @@ class ServerSession(Session):
def get_puppet_or_player(self):
"""
Returns session if not logged in; puppet if one exists, otherwise return the player.
Returns session if not logged in; puppet if one exists,
otherwise return the player.
"""
if self.logged_in:
return self.puppet if self.puppet else self.player
@ -192,7 +195,8 @@ class ServerSession(Session):
# merge, give prio to the lowest level (puppet)
nicks = list(puppet.db_attributes.filter(db_category__in=("nick_inputline", "nick_channel"))) + list(nicks)
raw_list = text.split(None)
raw_list = [" ".join(raw_list[:i+1]) for i in range(len(raw_list)) if raw_list[:i+1]]
raw_list = [" ".join(raw_list[:i + 1])
for i in range(len(raw_list)) if raw_list[:i + 1]]
for nick in nicks:
if nick.db_key in raw_list:
text = text.replace(nick.db_key, nick.db_strvalue, 1)
@ -209,7 +213,7 @@ class ServerSession(Session):
if funcname:
_OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs)
execute_cmd = data_in # alias
execute_cmd = data_in # alias
def data_out(self, text=None, **kwargs):
"""
@ -255,7 +259,6 @@ class ServerSession(Session):
"alias for at_data_out"
self.data_out(text=text, **kwargs)
# Dummy API hooks for use during non-loggedin operation
def at_cmdset_get(self):
@ -282,6 +285,7 @@ class ServerSession(Session):
def all(self):
return [val for val in self.__dict__.keys()
if not val.startswith['_']]
def __getattribute__(self, key):
# return None if no matching attribute was found.
try:
@ -290,12 +294,14 @@ class ServerSession(Session):
return None
self._ndb_holder = NdbHolder()
return self._ndb_holder
#@ndb.setter
def ndb_set(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to ndb object! "
string = "Use ndb.attr=value instead."
raise Exception(string)
#@ndb.deleter
def ndb_del(self):
"Stop accidental deletion."

View file

@ -6,6 +6,7 @@ on Portal and Server side) should inherit from this class.
import time
#------------------------------------------------------------
# Server Session
#------------------------------------------------------------
@ -18,23 +19,24 @@ class Session(object):
Each connection will see two session instances created:
1) A Portal session. This is customized for the respective connection
protocols that Evennia supports, like Telnet, SSH etc. The Portal session
must call init_session() as part of its initialization. The respective
hook methods should be connected to the methods unique for the respective
protocol so that there is a unified interface to Evennia.
2) A Server session. This is the same for all connected players, regardless
of how they connect.
protocols that Evennia supports, like Telnet, SSH etc. The Portal
session must call init_session() as part of its initialization. The
respective hook methods should be connected to the methods unique
for the respective protocol so that there is a unified interface
to Evennia.
2) A Server session. This is the same for all connected players,
regardless of how they connect.
The Portal and Server have their own respective sessionhandlers. These are synced
whenever new connections happen or the Server restarts etc, which means much of the
same information must be stored in both places e.g. the portal can re-sync with the
server when the server reboots.
The Portal and Server have their own respective sessionhandlers. These
are synced whenever new connections happen or the Server restarts etc,
which means much of the same information must be stored in both places
e.g. the portal can re-sync with the server when the server reboots.
"""
# names of attributes that should be affected by syncing.
_attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname',
'logged_in', 'puid', 'encoding',
_attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid',
'uname', 'logged_in', 'puid', 'encoding',
'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total',
'protocol_flags', 'server_data', "cmdset_storage_string")
@ -55,7 +57,7 @@ class Session(object):
self.suid = None
# unique id for this session
self.sessid = 0 # no sessid yet
self.sessid = 0 # no sessid yet
# database id for the user connected to this session
self.uid = None
# user name, for easier tracking of sessions
@ -84,7 +86,8 @@ class Session(object):
"""
Return all data relevant to sync the session
"""
return dict((key, value) for key, value in self.__dict__.items() if key in self._attrs_to_sync)
return dict((key, value) for key, value in self.__dict__.items()
if key in self._attrs_to_sync)
def load_sync_data(self, sessdata):
"""
@ -124,4 +127,3 @@ class Session(object):
hook for protocols to send incoming data to the engine.
"""
pass

View file

@ -15,7 +15,7 @@ There are two similar but separate stores of sessions:
import time
from django.conf import settings
from src.commands.cmdhandler import CMD_LOGINSTART
from src.utils.utils import variable_from_module, to_str
from src.utils.utils import variable_from_module
try:
import cPickle as pickle
except ImportError:
@ -29,14 +29,14 @@ _ScriptDB = None
# AMP signals
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
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
# i18n
from django.utils.translation import ugettext as _
@ -45,6 +45,7 @@ SERVERNAME = settings.SERVERNAME
MULTISESSION_MODE = settings.MULTISESSION_MODE
IDLE_TIMEOUT = settings.IDLE_TIMEOUT
def delayed_import():
"Helper method for delayed import of all needed entities"
global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
@ -61,6 +62,7 @@ def delayed_import():
# including once to avoid warnings in Python syntax checkers
_ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
#-----------------------------------------------------------
# SessionHandler base class
#------------------------------------------------------------
@ -110,7 +112,8 @@ class SessionHandler(object):
(cmdname, (args,), {kwargs})
((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,)))
outputs an ordered structure on the form
((cmdname, (args,), {kwargs}), ...), where the two last parts of each tuple may be empty
((cmdname, (args,), {kwargs}), ...), where the two last
parts of each tuple may be empty
"""
def _parse(oobstruct):
slen = len(oobstruct)
@ -119,8 +122,9 @@ class SessionHandler(object):
elif not hasattr(oobstruct, "__iter__"):
# a singular command name, without arguments or kwargs
return (oobstruct.lower(), (), {})
# regardless of number of args/kwargs, the first element must be the function name.
# we will not catch this error if not, but allow it to propagate.
# regardless of number of args/kwargs, the first element must be
# the function name. We will not catch this error if not, but
# allow it to propagate.
if slen == 1:
return (oobstruct[0].lower(), (), {})
elif slen == 2:
@ -135,7 +139,9 @@ class SessionHandler(object):
return (oobstruct[0].lower(), tuple(oobstruct[1]), dict(oobstruct[2]))
if hasattr(oobstruct, "__iter__"):
# differentiate between (cmdname, cmdname), (cmdname, args, kwargs) and ((cmdname,args,kwargs), (cmdname,args,kwargs), ...)
# differentiate between (cmdname, cmdname),
# (cmdname, args, kwargs) and ((cmdname,args,kwargs),
# (cmdname,args,kwargs), ...)
if oobstruct and isinstance(oobstruct[0], basestring):
return (tuple(_parse(oobstruct)),)
@ -146,6 +152,7 @@ class SessionHandler(object):
return (tuple(out),)
return (_parse(oobstruct),)
#------------------------------------------------------------
# Server-SessionHandler class
#------------------------------------------------------------
@ -171,7 +178,7 @@ class ServerSessionHandler(SessionHandler):
"""
self.sessions = {}
self.server = None
self.server_data = {"servername":SERVERNAME}
self.server_data = {"servername": SERVERNAME}
def portal_connect(self, portalsession):
"""
@ -189,7 +196,8 @@ class ServerSessionHandler(SessionHandler):
sess.sessionhandler = self
sess.load_sync_data(portalsession)
if sess.logged_in and sess.uid:
# this can happen in the case of auto-authenticating protocols like SSH
# this can happen in the case of auto-authenticating
# protocols like SSH
sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid)
sess.at_sync()
# validate all script
@ -216,17 +224,19 @@ class ServerSessionHandler(SessionHandler):
def portal_session_sync(self, portalsessions):
"""
Syncing all session ids of the portal with the ones of the server. This is instantiated
by the portal when reconnecting.
Syncing all session ids of the portal with the ones of the
server. This is instantiated by the portal when reconnecting.
portalsessions is a dictionary {sessid: {property:value},...} defining
each session and the properties in it which should be synced.
each session and the properties in it which should
be synced.
"""
delayed_import()
global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB
for sess in self.sessions.values():
# we delete the old session to make sure to catch eventual lingering references.
# we delete the old session to make sure to catch eventual
# lingering references.
del sess
for sessid, sessdict in portalsessions.items():
@ -238,7 +248,8 @@ class ServerSessionHandler(SessionHandler):
self.sessions[sessid] = sess
sess.at_sync()
# after sync is complete we force-validate all scripts (this also starts them)
# after sync is complete we force-validate all scripts
# (this also starts them)
init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None)
_ScriptDB.objects.validate(init_mode=init_mode)
_ServerConfig.objects.conf("server_restart_mode", delete=True)
@ -333,7 +344,6 @@ class ServerSessionHandler(SessionHandler):
operation=SSYNC,
data=sessdata)
def disconnect_all_sessions(self, reason=_("You have been disconnected.")):
"""
Cleanly disconnect all of the connected sessions.
@ -346,7 +356,8 @@ class ServerSessionHandler(SessionHandler):
operation=SDISCONNALL,
data=reason)
def disconnect_duplicate_sessions(self, curr_session, reason = _("Logged in from elsewhere. Disconnecting.") ):
def disconnect_duplicate_sessions(self, curr_session,
reason=_("Logged in from elsewhere. Disconnecting.")):
"""
Disconnects any existing sessions with the same user.
"""
@ -364,7 +375,7 @@ class ServerSessionHandler(SessionHandler):
and see if any are dead.
"""
tcurr = time.time()
reason= _("Idle timeout exceeded, disconnecting.")
reason = _("Idle timeout exceeded, disconnecting.")
for session in (session for session in self.sessions.values()
if session.logged_in and IDLE_TIMEOUT > 0
and (tcurr - session.cmd_last) > IDLE_TIMEOUT):
@ -407,7 +418,6 @@ class ServerSessionHandler(SessionHandler):
return self.sessions.get(sessid)
return None
def announce_all(self, message):
"""
Send message to all connected sessions
@ -422,6 +432,7 @@ class ServerSessionHandler(SessionHandler):
self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid,
msg=text,
data=kwargs)
def data_in(self, sessid, text="", **kwargs):
"""
Data Portal -> Server

View file

@ -15,7 +15,7 @@ import urlparse
from urllib import quote as urlquote
from twisted.web import resource, http
from twisted.internet import reactor
from twisted.application import service, internet
from twisted.application import internet
from twisted.web.proxy import ReverseProxyResource
from twisted.web.server import NOT_DONE_YET
@ -24,6 +24,7 @@ from django.core.handlers.wsgi import WSGIHandler
from settings import UPSTREAM_IPS
#
# X-Forwarded-For Handler
#
@ -40,13 +41,14 @@ class HTTPChannelWithXForwardedFor(http.HTTPChannel):
proxy_chain = req.getHeader('X-FORWARDED-FOR')
if proxy_chain and client_ip in UPSTREAM_IPS:
forwarded = proxy_chain.split(', ', 1)[CLIENT]
self.transport.client = (forwarded, port)
self.transport.client = (forwarded, port)
# Monkey-patch Twisted to handle X-Forwarded-For.
http.HTTPFactory.protocol = HTTPChannelWithXForwardedFor
class EvenniaReverseProxyResource(ReverseProxyResource):
def getChild(self, path, request):
"""
@ -58,7 +60,6 @@ class EvenniaReverseProxyResource(ReverseProxyResource):
self.host, self.port, self.path + '/' + urlquote(path, safe=""),
self.reactor)
def render(self, request):
"""
Render a request by forwarding it to the proxied server.
@ -77,6 +78,7 @@ class EvenniaReverseProxyResource(ReverseProxyResource):
self.reactor.connectTCP(self.host, self.port, clientFactory)
return NOT_DONE_YET
#
# Website server resource
#
@ -92,7 +94,7 @@ class DjangoWebRoot(resource.Resource):
Setup the django+twisted resource
"""
resource.Resource.__init__(self)
self.wsgi_resource = WSGIResource(reactor, pool , WSGIHandler())
self.wsgi_resource = WSGIResource(reactor, pool, WSGIHandler())
def getChild(self, path, request):
"""
@ -102,6 +104,8 @@ class DjangoWebRoot(resource.Resource):
path0 = request.prepath.pop(0)
request.postpath.insert(0, path0)
return self.wsgi_resource
#
# Threaded Webserver
#
@ -114,14 +118,16 @@ class WSGIWebServer(internet.TCPServer):
call with WSGIWebServer(threadpool, port, wsgi_resource)
"""
def __init__(self, pool, *args, **kwargs ):
def __init__(self, pool, *args, **kwargs):
"This just stores the threadpool"
self.pool = pool
internet.TCPServer.__init__(self, *args, **kwargs)
def startService(self):
"Start the pool after the service"
internet.TCPServer.startService(self)
self.pool.start()
def stopService(self):
"Safely stop the pool after service stop."
internet.TCPServer.stopService(self)

View file

@ -34,7 +34,7 @@ TELNET_INTERFACES = ['0.0.0.0']
# full use of OOB, you need to prepare functions to handle the data
# server-side (see OOB_FUNC_MODULE). TELNET_ENABLED is required for this
# to work.
TELNET_OOB_ENABLED = False # OBS - currently not fully implemented - do not use!
TELNET_OOB_ENABLED = False
# Start the evennia django+twisted webserver so you can
# browse the evennia website and the admin interface
# (Obs - further web configuration can be found below
@ -85,8 +85,9 @@ LOG_DIR = os.path.join(GAME_DIR, 'logs')
SERVER_LOG_FILE = os.path.join(LOG_DIR, 'server.log')
PORTAL_LOG_FILE = os.path.join(LOG_DIR, 'portal.log')
HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log')
# Rotate log files when server and/or portal stops. This will keep log file sizes down.
# Turn off to get ever growing log files and never loose log info.
# Rotate log files when server and/or portal stops. This will keep log
# file sizes down. Turn off to get ever growing log files and never
# loose log info.
CYCLE_LOGFILES = True
# Local time zone for this installation. All choices can be found here:
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
@ -114,20 +115,22 @@ ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"]
# The game server opens an AMP port so that the portal can
# communicate with it. This is an internal functionality of Evennia, usually
# operating between two processes on the same machine. You usually don't need to
# change this unless you cannot use the default AMP port/host for whatever reason.
# change this unless you cannot use the default AMP port/host for
# whatever reason.
AMP_HOST = 'localhost'
AMP_PORT = 5000
AMP_INTERFACE = '127.0.0.1'
# Caching speeds up all forms of database access, often considerably. There
# are (currently) only two settings, "local" or None, the latter of which turns
# off all caching completely. Local caching stores data in the process. It's very
# fast but will go out of sync if more than one process writes to the database (such
# as when using procpool or an extensice web precense).
# off all caching completely. Local caching stores data in the process. It's
# very fast but will go out of sync if more than one process writes to the
# database (such as when using procpool or an extensice web precense).
GAME_CACHE_TYPE = "local"
# Attributes on objects are cached aggressively for speed. If the number of
# objects is large (and their attributes are often accessed) this can use up a lot of
# memory. So every now and then Evennia checks the size of this cache and resets
# it if it's too big. This variable sets the maximum size (in MB).
# objects is large (and their attributes are often accessed) this can use up
# a lot of memory. So every now and then Evennia checks the size of this
# cache and resets it if it's too big. This variable sets the maximum
# size (in MB).
ATTRIBUTE_CACHE_MAXSIZE = 100
######################################################################
@ -146,13 +149,13 @@ ATTRIBUTE_CACHE_MAXSIZE = 100
# HOST - empty string is localhost (unused in sqlite3)
# PORT - empty string defaults to localhost (unused in sqlite3)
DATABASES = {
'default':{
'ENGINE':'django.db.backends.sqlite3',
'NAME':os.path.join(GAME_DIR, 'evennia.db3'),
'USER':'',
'PASSWORD':'',
'HOST':'',
'PORT':''
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(GAME_DIR, 'evennia.db3'),
'USER': '',
'PASSWORD': '',
'HOST': '',
'PORT': ''
}}
######################################################################
@ -185,12 +188,14 @@ AT_INITIAL_SETUP_HOOK_MODULE = ""
# at_server_stop() methods. These methods will be called every time
# the server starts, reloads and resets/stops respectively.
AT_SERVER_STARTSTOP_MODULE = ""
# List of one or more module paths to modules containing a function start_plugin_services(application). This module
# will be called with the main Evennia Server application when the Server is initiated.
# List of one or more module paths to modules containing a function start_
# plugin_services(application). This module will be called with the main
# Evennia Server application when the Server is initiated.
# It will be called last in the startup sequence.
SERVER_SERVICES_PLUGIN_MODULES = []
# List of one or more module paths to modules containing a function start_plugin_services(application). This module
# will be called with the main Evennia Portal application when the Portal is initiated.
# List of one or more module paths to modules containing a function
# start_plugin_services(application). This module will be called with the
# main Evennia Portal application when the Portal is initiated.
# It will be called last in the startup sequence.
PORTAL_SERVICES_PLUGIN_MODULES = []
# Module holding MSSP meta data. This is used by MUD-crawlers to determine
@ -208,9 +213,9 @@ OOB_PLUGIN_MODULE = "src.server.oob_msdp"
# Default command sets
######################################################################
# Note that with the exception of the unloggedin set (which is not
# stored anywhere in the databse), changing these paths will only affect NEW created
# characters/objects, not those already in play. So if you plan to change
# this, it's recommended you do it before having created a lot of objects
# stored anywhere in the databse), changing these paths will only affect
# NEW created characters/objects, not those already in play. So if you plan to
# change this, it's recommended you do it before having created a lot of objects
# (or simply reset the database after the change for simplicity). Remember
# that you should never edit things in src/. Instead copy out the examples
# in game/gamesrc/commands/examples up one level and re-point these settings
@ -236,8 +241,12 @@ SERVER_SESSION_CLASS = "src.server.serversession.ServerSession"
# Base paths for typeclassed object classes. These paths must be
# defined relative evennia's root directory. They will be searched in
# order to find relative typeclass paths.
OBJECT_TYPECLASS_PATHS = ["game.gamesrc.objects", "game.gamesrc.objects.examples", "contrib"]
SCRIPT_TYPECLASS_PATHS = ["game.gamesrc.scripts", "game.gamesrc.scripts.examples", "contrib"]
OBJECT_TYPECLASS_PATHS = ["game.gamesrc.objects",
"game.gamesrc.objects.examples",
"contrib"]
SCRIPT_TYPECLASS_PATHS = ["game.gamesrc.scripts",
"game.gamesrc.scripts.examples",
"contrib"]
PLAYER_TYPECLASS_PATHS = ["game.gamesrc.objects", "contrib"]
COMM_TYPECLASS_PATHS = ["game.gamesrc.objects", "contrib"]
@ -304,20 +313,31 @@ TIME_MONTH_PER_YEAR = 12
# Default Player setup and access
######################################################################
# Different Multisession modes allow a player (=account) to connect to the game simultaneously
# with multiple clients (=sessions). In modes 0,1 there is only one character created to the same
# name as the account at first login. In modes 1,2 no default character will be created and
# the MAX_NR_CHARACTERS value (below) defines how many characters are allowed.
# 0 - single session, one player, one character, when a new session is connected, the old one is disconnected
# 1 - multiple sessions, one player, one character, each session getting the same data
# 2 - multiple sessions, one player, many characters, each session getting data from different characters
# Different Multisession modes allow a player (=account) to connect to the
# game simultaneously with multiple clients (=sessions). In modes 0,1 there is
# only one character created to the same name as the account at first login.
# In modes 1,2 no default character will be created and the MAX_NR_CHARACTERS
# value (below) defines how many characters are allowed.
# 0 - single session, one player, one character, when a new session is
# connected, the old one is disconnected
# 1 - multiple sessions, one player, one character, each session getting
# the same data
# 2 - multiple sessions, one player, many characters, each session getting
# data from different characters
MULTISESSION_MODE = 0
# The maximum number of characters allowed for MULTISESSION_MODE 2. This is checked
# by the default ooc char-creation command. Forced to 1 for MULTISESSION_MODE 0 and 1.
# The maximum number of characters allowed for MULTISESSION_MODE 2. This is
# checked
# by the default ooc char-creation command. Forced to 1 for
# MULTISESSION_MODE 0 and 1.
MAX_NR_CHARACTERS = 1
# The access hiearchy, in climbing order. A higher permission in the
# hierarchy includes access of all levels below it. Used by the perm()/pperm() lock functions.
PERMISSION_HIERARCHY = ("Players","PlayerHelpers","Builders", "Wizards", "Immortals")
# hierarchy includes access of all levels below it. Used by the perm()/pperm()
# lock functions.
PERMISSION_HIERARCHY = ("Players",
"PlayerHelpers",
"Builders",
"Wizards",
"Immortals")
# The default permission given to all new players
PERMISSION_PLAYER_DEFAULT = "Players"
@ -334,10 +354,10 @@ CHANNEL_PUBLIC = ("Public", ('ooc',), 'Public discussion',
"control:perm(Wizards);listen:all();send:all()")
# General info about the server
CHANNEL_MUDINFO = ("MUDinfo", '', 'Informative messages',
"control:perm(Immortals);listen:perm(Immortals);send:false()")
"control:perm(Immortals);listen:perm(Immortals);send:false()")
# Channel showing when new people connecting
CHANNEL_CONNECTINFO = ("MUDconnections", '', 'Connection log',
"control:perm(Immortals);listen:perm(Wizards);send:false()")
"control:perm(Immortals);listen:perm(Wizards);send:false()")
######################################################################
# External Channel connections
@ -461,7 +481,7 @@ TEMPLATE_LOADERS = (
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', # 1.4?
'django.contrib.messages.middleware.MessageMiddleware', # 1.4?
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.middleware.doc.XViewMiddleware',

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View file

@ -13,6 +13,7 @@ from src.utils.dbserialize import to_pickle
__all__ = ("AttributeManager", "TypedObjectManager")
_GA = object.__getattribute__
# Managers
def _attr_pickled(method):
@ -29,24 +30,30 @@ def _attr_pickled(method):
return method(self, *args, **kwargs)
return update_wrapper(wrapper, method)
class AttributeManager(models.Manager):
"Manager for handling Attributes."
@_attr_pickled
def get(self, *args, **kwargs):
return super(AttributeManager, self).get(*args, **kwargs)
@_attr_pickled
def filter(self,*args, **kwargs):
return super(AttributeManager, self).filter(*args, **kwargs)
@_attr_pickled
def exclude(self,*args, **kwargs):
return super(AttributeManager, self).exclude(*args, **kwargs)
@_attr_pickled
def values(self,*args, **kwargs):
return super(AttributeManager, self).values(*args, **kwargs)
@_attr_pickled
def values_list(self,*args, **kwargs):
return super(AttributeManager, self).values_list(*args, **kwargs)
@_attr_pickled
def exists(self,*args, **kwargs):
return super(AttributeManager, self).exists(*args, **kwargs)
@ -113,11 +120,11 @@ class TagManager(models.Manager):
search_key (string) - the tag identifier
category (string) - the tag category
Returns a single Tag (or None) if both key and category is given, otherwise
it will return a list.
Returns a single Tag (or None) if both key and category is given,
otherwise it will return a list.
"""
key_cands = Q(db_key__iexact=key.lower().strip()) if key!=None else Q()
cat_cands = Q(db_category__iexact=category.lower().strip()) if category!=None else Q()
key_cands = Q(db_key__iexact=key.lower().strip()) if key is not None else Q()
cat_cands = Q(db_category__iexact=category.lower().strip()) if category is not None else Q()
tags = self.filter(key_cands & cat_cands)
if key and category:
return tags[0] if tags else None
@ -132,8 +139,8 @@ class TagManager(models.Manager):
key (string) - the tag identifier
category (string) - the tag category
"""
key_cands = Q(db_tags__db_key__iexact=key.lower().strip()) if search_key!=None else Q()
cat_cands = Q(db_tags__db_category__iexact=category.lower().strip()) if category!=None else Q()
key_cands = Q(db_tags__db_key__iexact=key.lower().strip()) if key is not None else Q()
cat_cands = Q(db_tags__db_category__iexact=category.lower().strip()) if category is not None else Q()
return objclass.objects.filter(key_cands & cat_cands)
def create_tag(self, key=None, category=None, data=None):
@ -144,19 +151,21 @@ class TagManager(models.Manager):
data on a tag (it is not part of what makes the tag unique).
"""
data = str(data) if data!=None else None
data = str(data) if data is not None else None
tag = self.get_tag(key=key, category=category)
if tag and data != None:
if tag and data is not None:
tag.db_data = data
tag.save()
elif not tag:
tag = self.create(db_key=key.lower().strip() if key!=None else None,
db_category=category.lower().strip() if category and key!=None else None,
db_data=str(data) if data!=None else None)
tag = self.create(db_key=key.lower().strip() if key is not None else None,
db_category=category.lower().strip()
if category and key is not None else None,
db_data=str(data) if data is not None else None)
tag.save()
return make_iter(tag)[0]
#
# helper functions for the TypedObjectManager.
#
@ -171,9 +180,11 @@ def returns_typeclass_list(method):
"decorator. Returns a list."
self.__doc__ = method.__doc__
matches = make_iter(method(self, *args, **kwargs))
return [(hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj for dbobj in make_iter(matches)]
return [(hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj
for dbobj in make_iter(matches)]
return update_wrapper(func, method)
def returns_typeclass(method):
"""
Decorator: Will always return a single typeclassed result or None.
@ -184,7 +195,7 @@ def returns_typeclass(method):
matches = method(self, *args, **kwargs)
dbobj = matches and make_iter(matches)[0] or None
if dbobj:
return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj
return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj
return None
return update_wrapper(func, method)
@ -240,9 +251,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
given boundaries.
"""
retval = super(TypedObjectManager, self).all()
if min_dbref != None:
if min_dbref is not None:
retval = retval.filter(id__gte=self.dbref(min_dbref, reqhash=False))
if max_dbref != None:
if max_dbref is not None:
retval = retval.filter(id__lte=self.dbref(max_dbref, reqhash=False))
return retval

View file

@ -147,6 +147,7 @@ class Attribute(SharedMemoryModel):
# self.cached_value = value
# self.no_cache = False
#return self.cached_value
#@value.setter
def __value_set(self, new_value):
"""
@ -168,13 +169,13 @@ class Attribute(SharedMemoryModel):
# self._track_db_value_change.update(self.cached_value)
#except AttributeError:
# pass
#@value.deleter
def __value_del(self):
"Deleter. Allows for del attr.value. This removes the entire attribute."
self.delete()
value = property(__value_get, __value_set, __value_del)
#
#
# Attribute methods
@ -202,6 +203,7 @@ class Attribute(SharedMemoryModel):
"""
pass
#
# Handlers making use of the Attribute model
#
@ -226,19 +228,21 @@ class AttributeHandler(object):
def has(self, key, category=None):
"""
Checks if the given Attribute (or list of Attributes) exists on the object.
Checks if the given Attribute (or list of Attributes) exists on
the object.
If an iterable is given, returns list of booleans.
"""
if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE:
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
catkey = to_str(category, force_string=True).lower()
searchkeys = ["%s_%s" % (k.lower(), catkey) for k in make_iter(key)]
ret = [self._cache[skey] for skey in searchkeys if skey in self._cache]
return ret[0] if len(ret) == 1 else ret
def get(self, key=None, category=None, default=None, return_obj=False, strattr=False,
raise_exception=False, accessing_obj=None, default_access=True):
def get(self, key=None, category=None, default=None, return_obj=False,
strattr=False, raise_exception=False, accessing_obj=None,
default_access=True):
"""
Returns the value of the given Attribute or list of Attributes.
strattr will cause the string-only value field instead of the normal
@ -253,7 +257,7 @@ class AttributeHandler(object):
checked before displaying each looked-after Attribute. If no
accessing_obj is given, no check will be done.
"""
if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE:
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
ret = []
catkey = to_str(category, force_string=True).lower()
@ -280,32 +284,36 @@ class AttributeHandler(object):
ret = ret if return_obj else [attr.value if attr else None for attr in ret]
return ret[0] if len(ret)==1 else ret
def add(self, key, value, category=None, lockstring="", strattr=False, accessing_obj=None, default_access=True):
def add(self, key, value, category=None, lockstring="",
strattr=False, accessing_obj=None, default_access=True):
"""
Add attribute to object, with optional lockstring.
If strattr is set, the db_strvalue field will be used (no pickling). Use the get() method
with the strattr keyword to get it back.
If strattr is set, the db_strvalue field will be used (no pickling).
Use the get() method with the strattr keyword to get it back.
If accessing_obj is given, self.obj's 'attrcreate' lock access
will be checked against it. If no accessing_obj is given, no check will be done.
will be checked against it. If no accessing_obj is given, no check
will be done.
"""
if accessing_obj and not self.obj.access(accessing_obj, self._attrcreate, default=default_access):
if accessing_obj and not self.obj.access(accessing_obj,
self._attrcreate, default=default_access):
# check create access
return
if self._cache == None:
if self._cache is None:
self._recache()
cachekey = "%s_%s" % (key.lower(), to_str(category, force_string=True).lower())
attr_obj = self._cache.get(cachekey)
if not attr_obj:
# no old attr available; create new.
attr_obj = Attribute(db_key=key, db_category=category)
attr_obj.save() # important
attr_obj.save() # important
_GA(self.obj, self._m2m_fieldname).add(attr_obj)
self._cache[cachekey] = attr_obj
if lockstring:
attr_obj.locks.add(lockstring)
# we shouldn't need to fear stale objects, the field signalling should catch all cases
# we shouldn't need to fear stale objects, the field signalling
# should catch all cases
if strattr:
# store as a simple string
attr_obj.strvalue = value
@ -315,18 +323,21 @@ class AttributeHandler(object):
attr_obj.value = value
attr_obj.strvalue = None
def remove(self, key, raise_exception=False, category=None, accessing_obj=None, default_access=True):
def remove(self, key, raise_exception=False, category=None,
accessing_obj=None, default_access=True):
"""Remove attribute or a list of attributes from object.
If accessing_obj is given, will check against the 'attredit' lock. If not given, this check is skipped.
If accessing_obj is given, will check against the 'attredit' lock.
If not given, this check is skipped.
"""
if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE:
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
catkey = to_str(category, force_string=True).lower()
for keystr in ("%s_%s" % (k.lower(), catkey) for k in make_iter(key)):
attr_obj = self._cache.get(keystr)
if attr_obj:
if accessing_obj and not attr_obj.access(accessing_obj, self._attredit, default=default_access):
if accessing_obj and not attr_obj.access(accessing_obj,
self._attredit, default=default_access):
continue
attr_obj.delete()
elif not attr_obj and raise_exception:
@ -353,7 +364,7 @@ class AttributeHandler(object):
each attribute before returning them. If not given, this
check is skipped.
"""
if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE:
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
catkey = "_%s" % to_str(category, force_string=True).lower()
return [attr for key, attr in self._cache.items() if key.endswith(catkey)]
@ -367,6 +378,7 @@ class AttributeHandler(object):
#else:
# return list(all_attrs)
class NickHandler(AttributeHandler):
"""
Handles the addition and removal of Nicks
@ -401,27 +413,33 @@ class NickHandler(AttributeHandler):
return super(NickHandler, self).all(category=category)
return _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith="nick_")
class NAttributeHandler(object):
"""
This stand-alone handler manages non-database saved properties by storing them
as properties on obj.ndb. It has the same methods as AttributeHandler, but they
are much simplified.
This stand-alone handler manages non-database saved properties by storing
them as properties on obj.ndb. It has the same methods as AttributeHandler,
but they are much simplified.
"""
def __init__(self, obj):
"initialized on the object"
self.ndb = _GA(obj, "ndb")
def has(self, key):
"Check if object has this attribute or not"
return _GA(self.ndb, key) # ndb returns None if not found
return _GA(self.ndb, key) # ndb returns None if not found
def get(self, key):
"Returns named key value"
return _GA(self.ndb, key)
def add(self, key, value):
"Add new key and value"
_SA(self.ndb, key, value)
def remove(self, key):
"Remove key from storage"
_DA(self.ndb, key)
def all(self):
"List all keys stored"
if callable(self.ndb.all):
@ -429,6 +447,7 @@ class NAttributeHandler(object):
else:
return [val for val in self.ndb.__dict__.keys() if not val.startswith('_')]
#------------------------------------------------------------
#
# Tags
@ -456,18 +475,25 @@ class Tag(models.Model):
this uses the 'aliases' tag category, which is also checked by the
default search functions of Evennia to allow quick searches by alias.
"""
db_key = models.CharField('key', max_length=255, null=True, help_text="tag identifier", db_index=True)
db_category = models.CharField('category', max_length=64, null=True, help_text="tag category", db_index=True)
db_data = models.TextField('data', null=True, blank=True, help_text="optional data field with extra information. This is not searched for.")
db_key = models.CharField('key', max_length=255, null=True,
help_text="tag identifier", db_index=True)
db_category = models.CharField('category', max_length=64, null=True,
help_text="tag category", db_index=True)
db_data = models.TextField('data', null=True, blank=True,
help_text="optional data field with extra information. This is not searched for.")
objects = managers.TagManager()
class Meta:
"Define Django meta options"
verbose_name = "Tag"
unique_together =(('db_key', 'db_category'),)
unique_together = (('db_key', 'db_category'),)
index_together = (('db_key', 'db_category'),)
def __unicode__(self):
return u"%s" % self.db_key
def __str__(self):
return str(self.db_key)
@ -489,7 +515,8 @@ class TagHandler(object):
using the category <category_prefix><tag_category>
"""
self.obj = obj
self.prefix = "%s%s" % (category_prefix.strip().lower() if category_prefix else "", self._base_category)
self.prefix = "%s%s" % (category_prefix.strip().lower()
if category_prefix else "", self._base_category)
self._cache = None
def _recache(self):
@ -499,38 +526,46 @@ class TagHandler(object):
def add(self, tag, category=None, data=None):
"Add a new tag to the handler. Tag is a string or a list of strings."
for tagstr in make_iter(tag):
tagstr = tagstr.strip().lower() if tagstr!=None else None
category = "%s%s" % (self.prefix, category.strip().lower() if category!=None else "")
data = str(data) if data!=None else None
# this will only create tag if no matches existed beforehand (it will overload
# data on an existing tag since that is not considered part of making the tag unique)
tagstr = tagstr.strip().lower() if tagstr is not None else None
category = "%s%s" % (self.prefix, category.strip().lower() if category is not None else "")
data = str(data) if data is not None else None
# this will only create tag if no matches existed beforehand (it
# will overload data on an existing tag since that is not
# considered part of making the tag unique)
tagobj = Tag.objects.create_tag(key=tagstr, category=category, data=data)
_GA(self.obj, self._m2m_fieldname).add(tagobj)
if self._cache == None:
if self._cache is None:
self._recache()
self._cache[tagstr] = True
def get(self, key, category="", return_obj=False):
"Get the data field for the given tag or list of tags. If return_obj=True, return the matching Tag objects instead."
if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE:
"""
Get the data field for the given tag or list of tags. If
return_obj=True, return the matching Tag objects instead.
"""
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
ret = []
category = "%s%s" % (self.prefix, category.strip().lower() if category!=None else "")
ret = [val for val in (self._cache.get(keystr.strip().lower()) for keystr in make_iter(key)) if val]
category = "%s%s" % (self.prefix, category.strip().lower()
if category is not None else "")
ret = [val for val in (self._cache.get(keystr.strip().lower())
for keystr in make_iter(key)) if val]
ret = ret if return_obj else [to_str(tag.db_data) for tag in ret if tag]
return ret[0] if len(ret)==1 else ret
return ret[0] if len(ret) == 1 else ret
def remove(self, tag, category=None):
"Remove a tag from the handler"
if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE:
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
for tag in make_iter(tag):
if not (tag or tag.strip()): # we don't allow empty tags
if not (tag or tag.strip()): # we don't allow empty tags
continue
tagstr = tag.strip().lower() if tag!=None else None
category = "%s%s" % (self.prefix, category.strip().lower() if category!=None else "")
#TODO This does not delete the tag object itself. Maybe it should do that when no
# objects reference the tag anymore?
tagstr = tag.strip().lower() if tag is not None else None
category = "%s%s" % (self.prefix, category.strip().lower()
if category is not None else "")
# This does not delete the tag object itself. Maybe it should do
# that when no objects reference the tag anymore (how to check)?
tagobj = self.obj.db_tags.filter(db_key=tagstr, db_category=category)
if tagobj:
_GA(self.obj, self._m2m_fieldname).remove(tagobj[0])
@ -543,19 +578,22 @@ class TagHandler(object):
def all(self):
"Get all tags in this handler"
if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE:
if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE:
self._recache()
return self._cache.keys()
#return [to_str(p[0]) for p in _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith=self.prefix).values_list("db_key") if p[0]]
def __str__(self):
return ",".join(self.all())
def __unicode(self):
return u",".join(self.all())
class AliasHandler(TagHandler):
_base_category = "alias"
class PermissionHandler(TagHandler):
_base_category = "permission"
@ -591,19 +629,23 @@ class TypedObject(SharedMemoryModel):
# TypedObject Database Model setup
#
#
# These databse fields are all accessed and set using their corresponding properties,
# named same as the field, but without the db_* prefix (no separate save() call is needed)
# These databse fields are all accessed and set using their corresponding
# properties, named same as the field, but without the db_* prefix
# (no separate save() call is needed)
# Main identifier of the object, for searching. Is accessed with self.key or self.name
# Main identifier of the object, for searching. Is accessed with self.key
# or self.name
db_key = models.CharField('key', max_length=255, db_index=True)
# This is the python path to the type class this object is tied to the type class is what defines what kind of Object this is)
# This is the python path to the type class this object is tied to the
# typeclass is what defines what kind of Object this is)
db_typeclass_path = models.CharField('typeclass', max_length=255, null=True,
help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.")
# Creation date. This is not changed once the object is created.
db_date_created = models.DateTimeField('creation date', editable=False, auto_now_add=True)
# Permissions (access these through the 'permissions' property)
#db_permissions = models.CharField('permissions', max_length=255, blank=True,
# help_text="a comma-separated list of text strings checked by in-game locks. They are often used for hierarchies, such as letting a Player have permission 'Wizards', 'Builders' etc. Character objects use 'Players' by default. Most other objects don't have any permissions.")
# help_text="a comma-separated list of text strings checked by
# in-game locks. They are often used for hierarchies, such as letting a Player have permission 'Wizards', 'Builders' etc. Character objects use 'Players' by default. Most other objects don't have any permissions.")
# Lock storage
db_lock_storage = models.TextField('locks', blank=True,
help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.")
@ -647,9 +689,14 @@ class TypedObject(SharedMemoryModel):
# is the object in question).
# name property (alias to self.key)
def __name_get(self): return self.key
def __name_set(self, value): self.key = value
def __name_del(self): raise Exception("Cannot delete name")
def __name_get(self):
return self.key
def __name_set(self, value):
self.key = value
def __name_del(self):
raise Exception("Cannot delete name")
name = property(__name_get, __name_set, __name_del)
#
@ -711,8 +758,10 @@ class TypedObject(SharedMemoryModel):
dbid = _GA(self, "id")
set_prop_cache(self, "_dbid", dbid)
return dbid
def __dbid_set(self, value):
raise Exception("dbid cannot be set!")
def __dbid_del(self):
raise Exception("dbid cannot be deleted!")
dbid = property(__dbid_get, __dbid_set, __dbid_del)
@ -723,13 +772,14 @@ class TypedObject(SharedMemoryModel):
Returns the object's dbref on the form #NN.
"""
return "#%s" % _GA(self, "_TypedObject__dbid_get")()
def __dbref_set(self):
raise Exception("dbref cannot be set!")
def __dbref_del(self):
raise Exception("dbref cannot be deleted!")
dbref = property(__dbref_get, __dbref_set, __dbref_del)
# typeclass property
#@property
def __typeclass_get(self):
@ -760,9 +810,10 @@ class TypedObject(SharedMemoryModel):
else:
# handle loading/importing of typeclasses, searching all paths.
# (self._typeclass_paths is a shortcut to settings.TYPECLASS_*_PATHS
# where '*' is either OBJECT, SCRIPT or PLAYER depending on the typed
# entities).
typeclass_paths = [path] + ["%s.%s" % (prefix, path) for prefix in _GA(self, '_typeclass_paths')]
# where '*' is either OBJECT, SCRIPT or PLAYER depending on the
# typed entities).
typeclass_paths = [path] + ["%s.%s" % (prefix, path)
for prefix in _GA(self, '_typeclass_paths')]
for tpath in typeclass_paths:
@ -787,8 +838,9 @@ class TypedObject(SharedMemoryModel):
errstring += " to specify the actual typeclass name inside the module too."
else:
errstring += "\n%s" % typeclass # this will hold a growing error message.
# If we reach this point we couldn't import any typeclasses. Return default. It's up to the calling
# method to use e.g. self.is_typeclass() to detect that the result is not the one asked for.
# If we reach this point we couldn't import any typeclasses. Return
# default. It's up to the calling method to use e.g. self.is_typeclass()
# to detect that the result is not the one asked for.
_GA(self, "_display_errmsg")(errstring)
_SA(self, "typeclass_lasterrmsg", errstring)
return _GA(self, "_get_default_typeclass")(cache=False, silent=False, save=False)
@ -818,12 +870,13 @@ class TypedObject(SharedMemoryModel):
return None
try:
modpath, class_name = path.rsplit('.', 1)
module = __import__(modpath, fromlist=["none"])
module = __import__(modpath, fromlist=["none"])
return module.__dict__[class_name]
except ImportError:
trc = sys.exc_traceback
if not trc.tb_next:
# we separate between not finding the module, and finding a buggy one.
# we separate between not finding the module, and finding
# a buggy one.
errstring = "Typeclass not found trying path '%s'." % path
else:
# a bug in the module is reported normally.
@ -866,7 +919,8 @@ class TypedObject(SharedMemoryModel):
if not callable(typeclass):
# if typeclass still doesn't exist at this point, we're in trouble.
# fall back to hardcoded core class which is wrong for e.g. scripts/players etc.
# fall back to hardcoded core class which is wrong for e.g.
# scripts/players etc.
failpath = defpath
defpath = "src.objects.objects.Object"
typeclass = _GA(self, "_path_import")(defpath)
@ -876,7 +930,8 @@ class TypedObject(SharedMemoryModel):
errstring += "\n Using Evennia's default class '%s'." % defpath
_GA(self, "_display_errmsg")(errstring)
if not callable(typeclass):
# if this is still giving an error, Evennia is wrongly configured or buggy
# if this is still giving an error, Evennia is wrongly
# configured or buggy
raise Exception("CRITICAL ERROR: The final fallback typeclass %s cannot load!!" % defpath)
typeclass = typeclass(self)
if save:
@ -911,15 +966,17 @@ class TypedObject(SharedMemoryModel):
typeclass = _GA(typeclass, "path")
except AttributeError:
pass
typeclasses = [typeclass] + ["%s.%s" % (path, typeclass) for path in _GA(self, "_typeclass_paths")]
typeclasses = [typeclass] + ["%s.%s" % (path, typeclass)
for path in _GA(self, "_typeclass_paths")]
if exact:
current_path = _GA(self.typeclass, "path") #"_GA(self, "_cached_db_typeclass_path")
return typeclass and any((current_path == typec for typec in typeclasses))
else:
# check parent chain
return any((cls for cls in self.typeclass.__class__.mro()
if any(("%s.%s" % (_GA(cls,"__module__"), _GA(cls,"__name__")) == typec for typec in typeclasses))))
if any(("%s.%s" % (_GA(cls, "__module__"),
_GA(cls, "__name__")) == typec
for typec in typeclasses))))
def delete(self, *args, **kwargs):
"""
@ -927,7 +984,6 @@ class TypedObject(SharedMemoryModel):
"""
super(TypedObject, self).delete(*args, **kwargs)
#
# Object manipulation methods
#
@ -1020,13 +1076,15 @@ class TypedObject(SharedMemoryModel):
def check_permstring(self, permstring):
"""
This explicitly checks if we hold particular permission without involving
any locks.
This explicitly checks if we hold particular permission without
involving any locks.
"""
if hasattr(self, "player"):
if self.player and self.player.is_superuser: return True
if self.player and self.player.is_superuser:
return True
else:
if self.is_superuser: return True
if self.is_superuser:
return True
if not permstring:
return False
@ -1048,9 +1106,9 @@ class TypedObject(SharedMemoryModel):
def flush_from_cache(self):
"""
Flush this object instance from cache, forcing an object reload. Note that this
will kill all temporary attributes on this object since it will be recreated
as a new Typeclass instance.
Flush this object instance from cache, forcing an object reload.
Note that this will kill all temporary attributes on this object
since it will be recreated as a new Typeclass instance.
"""
self.__class__.flush_cached_instance(self)
@ -1068,8 +1126,8 @@ class TypedObject(SharedMemoryModel):
and
del obj.db.attrname
and
all_attr = obj.db.all (unless there is no attribute named 'all', in which
case that will be returned instead).
all_attr = obj.db.all() (unless there is an attribute
named 'all', in which case that will be returned instead).
"""
try:
return self._db_holder
@ -1079,6 +1137,7 @@ class TypedObject(SharedMemoryModel):
def __init__(self, obj):
_SA(self, 'obj', obj)
_SA(self, "attrhandler", _GA(_GA(self, "obj"), "attributes"))
def __getattribute__(self, attrname):
if attrname == 'all':
# we allow to overload our default .all
@ -1087,21 +1146,26 @@ class TypedObject(SharedMemoryModel):
return attr
return _GA(self, 'all')
return _GA(self, "attrhandler").get(attrname)
def __setattr__(self, attrname, value):
_GA(self, "attrhandler").add(attrname, value)
def __delattr__(self, attrname):
_GA(self, "attrhandler").remove(attrname)
def get_all(self):
return _GA(self, "attrhandler").all()
all = property(get_all)
self._db_holder = DbHolder(self)
return self._db_holder
#@db.setter
def __db_set(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to db object! "
string += "Use db.attr=value instead."
raise Exception(string)
#@db.deleter
def __db_del(self):
"Stop accidental deletion."
@ -1129,24 +1193,28 @@ class TypedObject(SharedMemoryModel):
return [val for val in self.__dict__.keys()
if not val.startswith('_')]
all = property(get_all)
def __getattribute__(self, key):
# return None if no matching attribute was found.
try:
return _GA(self, key)
except AttributeError:
return None
def __setattr__(self, key, value):
# hook the oob handler here
#call_ndb_hooks(self, key, value)
_SA(self, key, value)
self._ndb_holder = NdbHolder()
return self._ndb_holder
#@ndb.setter
def __ndb_set(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to ndb object! "
string = "Use ndb.attr=value instead."
raise Exception(string)
#@ndb.deleter
def __ndb_del(self):
"Stop accidental deletion."
@ -1239,12 +1307,12 @@ class TypedObject(SharedMemoryModel):
value to None using this method. Use set_attribute.
"""
logger.log_depmsg("obj.attr() is deprecated. Use handlers obj.db or obj.attributes.")
if attribute_name == None:
if attribute_name is None:
# act as a list method
return _GA(self, "attributes").all()
elif delete == True:
elif delete is True:
_GA(self, "attributes").remove(attribute_name)
elif value == None:
elif value is None:
# act as a getter.
return _GA(self, "attributes").get(attribute_name)
else:
@ -1272,12 +1340,12 @@ class TypedObject(SharedMemoryModel):
"""
logger.log_depmsg("obj.secure_attr() is deprecated. Use obj.attributes methods, giving accessing_obj keyword.")
if attribute_name == None:
if attribute_name is None:
return _GA(self, "attributes").all(accessing_obj=accessing_object, default_access=default_access_read)
elif delete == True:
elif delete is True:
# act as deleter
_GA(self, "attributes").remove(attribute_name, accessing_obj=accessing_object, default_access=default_access_edit)
elif value == None:
elif value is None:
# act as getter
return _GA(self, "attributes").get(attribute_name, accessing_obj=accessing_object, default_access=default_access_read)
else:
@ -1296,17 +1364,17 @@ class TypedObject(SharedMemoryModel):
a method call. Will return None if trying to access a non-existing property.
"""
logger.log_depmsg("obj.nattr() is deprecated. Use obj.nattributes instead.")
if attribute_name == None:
if attribute_name is None:
# act as a list method
if callable(self.ndb.all):
return self.ndb.all()
else:
return [val for val in self.ndb.__dict__.keys()
if not val.startswith['_']]
elif delete == True:
elif delete is True:
if hasattr(self.ndb, attribute_name):
_DA(_GA(self, "ndb"), attribute_name)
elif value == None:
elif value is None:
# act as a getter.
if hasattr(self.ndb, attribute_name):
_GA(_GA(self, "ndb"), attribute_name)

View file

@ -29,6 +29,7 @@ PROTECTED = ('id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass', 'db_player',
'attr', 'save', 'delete', 'db_model_name','attribute_class',
'typeclass_paths')
# If this is true, all non-protected property assignments
# are directly stored to a database attribute
@ -49,6 +50,7 @@ class MetaTypeClass(type):
def __str__(cls):
return "%s" % cls.__name__
class TypeClass(object):
"""
This class implements a 'typeclass' object. This is connected
@ -70,8 +72,10 @@ class TypeClass(object):
def __init__(self, dbobj):
"""
Initialize the object class. There are two ways to call this class.
o = object_class(dbobj) : this is used to initialize dbobj with the class name
o = dbobj.object_class(dbobj) : this is used when dbobj.object_class is already set.
o = object_class(dbobj) : this is used to initialize dbobj with the
class name
o = dbobj.object_class(dbobj) : this is used when dbobj.object_class
is already set.
"""
# typecheck of dbobj - we can't allow it to be added here
@ -145,11 +149,10 @@ class TypeClass(object):
except AttributeError:
return id(self) == id(other)
def __delattr__(self, propname):
"""
Transparently deletes data from the typeclass or dbobj by first searching on the typeclass,
secondly on the dbobj.db.
Transparently deletes data from the typeclass or dbobj by first
searching on the typeclass, secondly on the dbobj.db.
Will not allow deletion of properties stored directly on dbobj.
"""
if propname in PROTECTED:
@ -166,7 +169,7 @@ class TypeClass(object):
dbobj = _GA(self, 'dbobj')
except AttributeError:
log_trace("This is probably due to an unsafe reload.")
return # ignore delete
return # ignore delete
try:
dbobj.del_attribute(propname, raise_exception=True)
except AttributeError:
@ -178,5 +181,6 @@ class TypeClass(object):
def __str__(self):
"represent the object"
return self.key
def __unicode__(self):
return u"%s" % self.key

View file

@ -59,6 +59,7 @@ ANSI_SPACE = " "
# Escapes
ANSI_ESCAPES = ("{{", "%%", "\\\\")
class ANSIParser(object):
"""
A class that parses ansi markup
@ -75,10 +76,11 @@ class ANSIParser(object):
# MUX-style mappings %cr %cn etc
self.mux_ansi_map = [
# commented out by default; they (especially blink) are potentially annoying
(r'%r', ANSI_RETURN),
(r'%t', ANSI_TAB),
(r'%b', ANSI_SPACE),
# commented out by default; they (especially blink) are
# potentially annoying
(r'%r', ANSI_RETURN),
(r'%t', ANSI_TAB),
(r'%b', ANSI_SPACE),
#(r'%cf', ANSI_BLINK),
#(r'%ci', ANSI_INVERSE),
(r'%cr', ANSI_RED),
@ -118,18 +120,18 @@ class ANSIParser(object):
(r'{M', normal + ANSI_MAGENTA),
(r'{c', hilite + ANSI_CYAN),
(r'{C', normal + ANSI_CYAN),
(r'{w', hilite + ANSI_WHITE), # pure white
(r'{W', normal + ANSI_WHITE), #light grey
(r'{x', hilite + ANSI_BLACK), #dark grey
(r'{X', normal + ANSI_BLACK), #pure black
(r'{n', normal) #reset
(r'{w', hilite + ANSI_WHITE), # pure white
(r'{W', normal + ANSI_WHITE), # light grey
(r'{x', hilite + ANSI_BLACK), # dark grey
(r'{X', normal + ANSI_BLACK), # pure black
(r'{n', normal) # reset
]
# xterm256 {123, %c134,
self.xterm256_map = [
(r'%c([0-5]{3})', self.parse_rgb), # %c123 - foreground colour
(r'%c(b[0-5]{3})', self.parse_rgb), # %cb123 - background colour
(r'%c(b[0-5]{3})', self.parse_rgb), # %cb123 - background colour
(r'{([0-5]{3})', self.parse_rgb), # {123 - foreground colour
(r'{(b[0-5]{3})', self.parse_rgb) # {b123 - background colour
]
@ -145,7 +147,8 @@ class ANSIParser(object):
# prepare matching ansi codes overall
self.ansi_regex = re.compile("\033\[[0-9;]+m")
# escapes - these double-chars will be replaced with a single instance of each
# escapes - these double-chars will be replaced with a single
# instance of each
self.ansi_escapes = re.compile(r"(%s)" % "|".join(ANSI_ESCAPES), re.DOTALL)
def parse_rgb(self, rgbmatch):
@ -174,37 +177,61 @@ class ANSIParser(object):
#print "ANSI convert:", red, green, blue
# xterm256 not supported, convert the rgb value to ansi instead
if red == green and red == blue and red < 2:
if background: return ANSI_BACK_BLACK
elif red >= 1: return ANSI_HILITE + ANSI_BLACK
else: return ANSI_NORMAL + ANSI_BLACK
if background:
return ANSI_BACK_BLACK
elif red >= 1:
return ANSI_HILITE + ANSI_BLACK
else:
return ANSI_NORMAL + ANSI_BLACK
elif red == green and red == blue:
if background: return ANSI_BACK_WHITE
elif red >= 4: return ANSI_HILITE + ANSI_WHITE
else: return ANSI_NORMAL + ANSI_WHITE
if background:
return ANSI_BACK_WHITE
elif red >= 4:
return ANSI_HILITE + ANSI_WHITE
else:
return ANSI_NORMAL + ANSI_WHITE
elif red > green and red > blue:
if background: return ANSI_BACK_RED
elif red >= 3: return ANSI_HILITE + ANSI_RED
else: return ANSI_NORMAL + ANSI_RED
if background:
return ANSI_BACK_RED
elif red >= 3:
return ANSI_HILITE + ANSI_RED
else:
return ANSI_NORMAL + ANSI_RED
elif red == green and red > blue:
if background: return ANSI_BACK_YELLOW
elif red >= 3: return ANSI_HILITE + ANSI_YELLOW
else: return ANSI_NORMAL + ANSI_YELLOW
if background:
return ANSI_BACK_YELLOW
elif red >= 3:
return ANSI_HILITE + ANSI_YELLOW
else:
return ANSI_NORMAL + ANSI_YELLOW
elif red == blue and red > green:
if background: return ANSI_BACK_MAGENTA
elif red >= 3: return ANSI_HILITE + ANSI_MAGENTA
else: return ANSI_NORMAL + ANSI_MAGENTA
if background:
return ANSI_BACK_MAGENTA
elif red >= 3:
return ANSI_HILITE + ANSI_MAGENTA
else:
return ANSI_NORMAL + ANSI_MAGENTA
elif green > blue:
if background: return ANSI_BACK_GREEN
elif green >= 3: return ANSI_HILITE + ANSI_GREEN
else: return ANSI_NORMAL + ANSI_GREEN
if background:
return ANSI_BACK_GREEN
elif green >= 3:
return ANSI_HILITE + ANSI_GREEN
else:
return ANSI_NORMAL + ANSI_GREEN
elif green == blue:
if background: return ANSI_BACK_CYAN
elif green >= 3: return ANSI_HILITE + ANSI_CYAN
else: return ANSI_NORMAL + ANSI_CYAN
if background:
return ANSI_BACK_CYAN
elif green >= 3:
return ANSI_HILITE + ANSI_CYAN
else:
return ANSI_NORMAL + ANSI_CYAN
else: # mostly blue
if background: return ANSI_BACK_BLUE
elif blue >= 3: return ANSI_HILITE + ANSI_BLUE
else: return ANSI_NORMAL + ANSI_BLUE
if background:
return ANSI_BACK_BLUE
elif blue >= 3:
return ANSI_HILITE + ANSI_BLUE
else:
return ANSI_NORMAL + ANSI_BLUE
def parse_ansi(self, string, strip_ansi=False, xterm256=False):
"""
@ -227,13 +254,14 @@ class ANSIParser(object):
part = sub[0].sub(sub[1], part)
string += "%s%s" % (part, sep[0].strip())
if strip_ansi:
# remove all ansi codes (including those manually inserted in string)
# remove all ansi codes (including those manually
# inserted in string)
string = self.ansi_regex.sub("", string)
return string
ANSI_PARSER = ANSIParser()
#
# Access function
#
@ -245,8 +273,9 @@ def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False):
"""
return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256)
def raw(string):
"""
Escapes a string into a form which won't be colorized by the ansi parser.
"""
return string.replace('{','{{').replace('%','%%')
return string.replace('{', '{{').replace('%', '%%')

View file

@ -167,16 +167,18 @@ script = create.create_script()
import re
import codecs
import traceback, sys
from traceback import format_exc
import traceback
import sys
#from traceback import format_exc
from django.conf import settings
from src.utils import logger
from src.utils import utils
from game import settings as settings_module
#from game import settings as settings_module
ENCODINGS = settings.ENCODINGS
CODE_INFO_HEADER = re.compile(r"\(.*?\)")
#------------------------------------------------------------
# Helper function
#------------------------------------------------------------
@ -216,7 +218,7 @@ def read_batchfile(pythonpath, file_ending='.py'):
continue
load_errors = []
err =None
err = None
# We have successfully found and opened the file. Now actually
# try to decode it using the given protocol.
try:
@ -246,6 +248,7 @@ def read_batchfile(pythonpath, file_ending='.py'):
else:
return lines
#------------------------------------------------------------
#
# Batch-command processor
@ -261,9 +264,9 @@ class BatchCommandProcessor(object):
"""
This parses the lines of a batchfile according to the following
rules:
1) # at the beginning of a line marks the end of the command before it.
It is also a comment and any number of # can exist on subsequent
lines (but not inside comments).
1) # at the beginning of a line marks the end of the command before
it. It is also a comment and any number of # can exist on
subsequent lines (but not inside comments).
2) #INSERT at the beginning of a line imports another
batch-cmd file file and pastes it into the batch file as if
it was written there.
@ -323,10 +326,10 @@ class BatchCommandProcessor(object):
curr_cmd = ""
filename = line.lstrip("#INSERT").strip()
insert_commands = self.parse_file(filename)
if insert_commands == None:
if insert_commands is None:
insert_commands = ["{rINSERT ERROR: %s{n" % filename]
commands.extend(insert_commands)
else: #comment
else: #comment
if curr_cmd:
commands.append(curr_cmd.strip())
curr_cmd = ""
@ -341,6 +344,7 @@ class BatchCommandProcessor(object):
commands = [c.strip('\r\n') for c in commands]
return commands
#------------------------------------------------------------
#
# Batch-code processor
@ -350,11 +354,14 @@ class BatchCommandProcessor(object):
def tb_filename(tb):
"Helper to get filename from traceback"
return tb.tb_frame.f_code.co_filename
def tb_iter(tb):
while tb is not None:
yield tb
tb = tb.tb_next
class BatchCodeProcessor(object):
"""
This implements a batch-code processor
@ -368,7 +375,8 @@ class BatchCodeProcessor(object):
1) Lines starting with #HEADER starts a header block (ends other blocks)
2) Lines starting with #CODE begins a code block (ends other blocks)
3) #CODE headers may be of the following form: #CODE (info) objname, objname2, ...
3) #CODE headers may be of the following form:
#CODE (info) objname, objname2, ...
4) Lines starting with #INSERT are on form #INSERT filename.
3) All lines outside blocks are stripped.
4) All excess whitespace beginning/ending a block is stripped.
@ -378,8 +386,8 @@ class BatchCodeProcessor(object):
# helper function
def parse_line(line):
"""
Identifies the line type: block command, comment, empty or normal code.
Identifies the line type:
block command, comment, empty or normal code.
"""
parseline = line.strip()
@ -432,7 +440,7 @@ class BatchCodeProcessor(object):
# are not checking for cyclic imports!
in_header = False
in_code = False
inserted_codes = self.parse_file(line) or [{'objs':"", 'info':line, 'code':""}]
inserted_codes = self.parse_file(line) or [{'objs': "", 'info': line, 'code': ""}]
for codedict in inserted_codes:
codedict["inserted"] = True
codes.extend(inserted_codes)
@ -444,7 +452,7 @@ class BatchCodeProcessor(object):
in_code = True
# the line is a list of object variable names
# (or an empty list) at this point.
codedict = {'objs':line, 'info':info, 'code':""}
codedict = {'objs': line, 'info': info, 'code': ""}
codes.append(codedict)
elif mode == 'comment' and in_header:
continue
@ -485,7 +493,7 @@ class BatchCodeProcessor(object):
"""
# define the execution environment
environ = "settings_module.configure()"
environdict = {"settings_module":settings}
environdict = {"settings_module": settings}
if extra_environ:
for key, value in extra_environ.items():
environdict[key] = value

View file

@ -43,10 +43,12 @@ _channelhandler = None
# limit symbol import from API
__all__ = ("create_object", "create_script", "create_help_entry", "create_message", "create_channel", "create_player")
__all__ = ("create_object", "create_script", "create_help_entry",
"create_message", "create_channel", "create_player")
_GA = object.__getattribute__
#
# Game Object creation
#
@ -105,7 +107,8 @@ def create_object(typeclass, key=None, location=None,
new_object = new_db_object.typeclass
if not _GA(new_object, "is_typeclass")(typeclass, exact=True):
# this will fail if we gave a typeclass as input and it still gave us a default
# this will fail if we gave a typeclass as input and it still
# gave us a default
SharedMemoryModel.delete(new_db_object)
if report_to:
_GA(report_to, "msg")("Error creating %s (%s):\n%s" % (new_db_object.key, typeclass,
@ -122,14 +125,14 @@ def create_object(typeclass, key=None, location=None,
# call the hook method. This is where all at_creation
# customization happens as the typeclass stores custom
# things on its database object.
new_object.basetype_setup() # setup the basics of Exits, Characters etc.
new_object.basetype_setup() # setup the basics of Exits, Characters etc.
new_object.at_object_creation()
# custom-given perms/locks overwrite hooks
if permissions:
new_object.permissions.add(permissions)
if locks:
new_object.locks.add(locks)
new_object.locks.add(locks)
if aliases:
new_object.aliases.add(aliases)
@ -137,7 +140,7 @@ def create_object(typeclass, key=None, location=None,
if home:
new_object.home = home
else:
new_object.home = settings.CHARACTER_DEFAULT_HOME if not nohome else None
new_object.home = settings.CHARACTER_DEFAULT_HOME if not nohome else None
if location:
new_object.move_to(location, quiet=True)
@ -154,6 +157,7 @@ def create_object(typeclass, key=None, location=None,
#alias for create_object
object = create_object
#
# Script creation
#
@ -218,7 +222,8 @@ def create_script(typeclass, key=None, obj=None, locks=None,
new_script = new_db_script.typeclass
if not _GA(new_db_script, "is_typeclass")(typeclass, exact=True):
# this will fail if we gave a typeclass as input and it still gave us a default
# this will fail if we gave a typeclass as input and it still
# gave us a default
SharedMemoryModel.delete(new_db_script)
if report_to:
_GA(report_to, "msg")("Error creating %s (%s): %s" % (new_db_script.key, typeclass,
@ -243,13 +248,13 @@ def create_script(typeclass, key=None, obj=None, locks=None,
new_script.key = key
if locks:
new_script.locks.add(locks)
if interval != None:
if interval is not None:
new_script.interval = interval
if start_delay != None:
if start_delay is not None:
new_script.start_delay = start_delay
if repeats != None:
if repeats is not None:
new_script.repeats = repeats
if persistent != None:
if persistent is not None:
new_script.persistent = persistent
# a new created script should usually be started.
@ -261,6 +266,7 @@ def create_script(typeclass, key=None, obj=None, locks=None,
#alias
script = create_script
#
# Help entry creation
#
@ -296,6 +302,7 @@ def create_help_entry(key, entrytext, category="General", locks=None):
# alias
help_entry = create_help_entry
#
# Comm system methods
#
@ -343,6 +350,7 @@ def create_message(senderobj, message, channels=None,
return new_message
message = create_message
def create_channel(key, aliases=None, desc=None,
locks=None, keep_log=True,
typeclass=None):
@ -389,6 +397,7 @@ def create_channel(key, aliases=None, desc=None,
channel = create_channel
#
# Player creation methods
#
@ -456,7 +465,8 @@ def create_player(key, email, password,
new_player = new_db_player.typeclass
if not _GA(new_db_player, "is_typeclass")(typeclass, exact=True):
# this will fail if we gave a typeclass as input and it still gave us a default
# this will fail if we gave a typeclass as input
# and it still gave us a default
SharedMemoryModel.delete(new_db_player)
if report_to:
_GA(report_to, "msg")("Error creating %s (%s):\n%s" % (new_db_player.key, typeclass,
@ -465,7 +475,7 @@ def create_player(key, email, password,
else:
raise Exception(_GA(new_db_player, "typeclass_last_errmsg"))
new_player.basetype_setup() # setup the basic locks and cmdset
new_player.basetype_setup() # setup the basic locks and cmdset
# call hook method (may override default permissions)
new_player.at_player_creation()

View file

@ -50,8 +50,12 @@ if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6
else:
_DATESTRING = "%Y:%m:%d-%H:%M:%S:%f"
def _TO_DATESTRING(obj):
"this will only be called with valid database objects. Returns datestring on correct form."
"""
this will only be called with valid database objects. Returns datestring
on correct form.
"""
try:
return _GA(obj, "db_date_created").strftime(_DATESTRING)
except AttributeError:
@ -74,6 +78,7 @@ def _init_globals():
# SaverList, SaverDict, SaverSet - Attribute-specific helper classes and functions
#
def _save(method):
"method decorator that saves data to Attribute"
def save_wrapper(self, *args, **kwargs):
@ -83,6 +88,7 @@ def _save(method):
return ret
return update_wrapper(save_wrapper, method)
class _SaverMutable(object):
"""
Parent class for properly handling of nested mutables in
@ -95,6 +101,7 @@ class _SaverMutable(object):
self._parent = kwargs.pop("parent", None)
self._db_obj = kwargs.pop("db_obj", None)
self._data = None
def _save_tree(self):
"recursively traverse back up the tree, save when we reach the root"
if self._parent:
@ -103,6 +110,7 @@ class _SaverMutable(object):
self._db_obj.value = self
else:
logger.log_errmsg("_SaverMutable %s has no root Attribute to save to." % self)
def _convert_mutables(self, data):
"converts mutables to Saver* variants and assigns .parent property"
def process_tree(item, parent):
@ -127,19 +135,25 @@ class _SaverMutable(object):
def __repr__(self):
return self._data.__repr__()
def __len__(self):
return self._data.__len__()
def __iter__(self):
return self._data.__iter__()
def __getitem__(self, key):
return self._data.__getitem__(key)
@_save
def __setitem__(self, key, value):
self._data.__setitem__(key, self._convert_mutables(value))
@_save
def __delitem__(self, key):
self._data.__delitem__(key)
class _SaverList(_SaverMutable, MutableSequence):
"""
A list that saves itself to an Attribute when updated.
@ -147,14 +161,17 @@ class _SaverList(_SaverMutable, MutableSequence):
def __init__(self, *args, **kwargs):
super(_SaverList, self).__init__(*args, **kwargs)
self._data = list(*args)
@_save
def __add__(self, otherlist):
self._data = self._data.__add__(otherlist)
return self._data
@_save
def insert(self, index, value):
self._data.insert(index, self._convert_mutables(value))
class _SaverDict(_SaverMutable, MutableMapping):
"""
A dict that stores changes to an Attribute when updated
@ -163,6 +180,7 @@ class _SaverDict(_SaverMutable, MutableMapping):
super(_SaverDict, self).__init__(*args, **kwargs)
self._data = dict(*args)
class _SaverSet(_SaverMutable, MutableSet):
"""
A set that saves to an Attribute when updated
@ -170,11 +188,14 @@ class _SaverSet(_SaverMutable, MutableSet):
def __init__(self, *args, **kwargs):
super(_SaverSet, self).__init__(*args, **kwargs)
self._data = set(*args)
def __contains__(self, value):
return self._data.__contains__(value)
@_save
def add(self, value):
self._data.add(self._convert_mutables(value))
@_save
def discard(self, value):
self._data.discard(value)
@ -187,14 +208,18 @@ class _SaverSet(_SaverMutable, MutableSet):
def pack_dbobj(item):
"""
Check and convert django database objects to an internal representation.
This either returns the original input item or a tuple ("__packed_dbobj__", key, creation_time, id)
This either returns the original input item or a tuple
("__packed_dbobj__", key, creation_time, id)
"""
_init_globals()
obj = hasattr(item, 'dbobj') and item.dbobj or item
obj = hasattr(item, 'dbobj') and item.dbobj or item
natural_key = _FROM_MODEL_MAP[hasattr(obj, "id") and hasattr(obj, "db_date_created") and
hasattr(obj, '__class__') and obj.__class__.__name__.lower()]
# build the internal representation as a tuple ("__packed_dbobj__", key, creation_time, id)
return natural_key and ('__packed_dbobj__', natural_key, _TO_DATESTRING(obj), _GA(obj, "id")) or item
# build the internal representation as a tuple
# ("__packed_dbobj__", key, creation_time, id)
return natural_key and ('__packed_dbobj__', natural_key,
_TO_DATESTRING(obj), _GA(obj, "id")) or item
def unpack_dbobj(item):
"""
@ -208,21 +233,26 @@ def unpack_dbobj(item):
obj = item[3] and _TO_TYPECLASS(_TO_MODEL_MAP[item[1]].objects.get(id=item[3]))
except ObjectDoesNotExist:
return None
# even if we got back a match, check the sanity of the date (some databases may 're-use' the id)
try: dbobj = obj.dbobj
except AttributeError: dbobj = obj
# even if we got back a match, check the sanity of the date (some
# databases may 're-use' the id)
try:
dbobj = obj.dbobj
except AttributeError:
dbobj = obj
return _TO_DATESTRING(dbobj) == item[2] and obj or None
#
# Access methods
#
def to_pickle(data):
"""
This prepares data on arbitrary form to be pickled. It handles any nested structure
and returns data on a form that is safe to pickle (including having converted any
database models to their internal representation). We also convert any Saver*-type
objects back to their normal representations, they are not pickle-safe.
This prepares data on arbitrary form to be pickled. It handles any nested
structure and returns data on a form that is safe to pickle (including
having converted any database models to their internal representation).
We also convert any Saver*-type objects back to their normal
representations, they are not pickle-safe.
"""
def process_item(item):
"Recursive processor and identification of data"
@ -246,20 +276,22 @@ def to_pickle(data):
return pack_dbobj(item)
return process_item(data)
@transaction.autocommit
def from_pickle(data, db_obj=None):
"""
This should be fed a just de-pickled data object. It will be converted back
to a form that may contain database objects again. Note that if a database
object was removed (or changed in-place) in the database, None will be returned.
object was removed (or changed in-place) in the database, None will be
returned.
db_obj - this is the model instance (normally an Attribute) that _Saver*-type
iterables (_SaverList etc) will save to when they update. It must have a 'value'
property that saves assigned data to the database. Skip if not serializing onto
a given object.
db_obj - this is the model instance (normally an Attribute) that
_Saver*-type iterables (_SaverList etc) will save to when they
update. It must have a 'value' property that saves assigned data
to the database. Skip if not serializing onto a given object.
If db_obj is given, this function will convert lists, dicts and sets to their
_SaverList, _SaverDict and _SaverSet counterparts.
If db_obj is given, this function will convert lists, dicts and sets
to their _SaverList, _SaverDict and _SaverSet counterparts.
"""
def process_item(item):
@ -278,7 +310,8 @@ def from_pickle(data, db_obj=None):
return set(process_item(val) for val in item)
elif hasattr(item, '__iter__'):
try:
# we try to conserve the iterable class if it accepts an iterator
# we try to conserve the iterable class if
# it accepts an iterator
return item.__class__(process_item(val) for val in item)
except (AttributeError, TypeError):
return [process_item(val) for val in item]
@ -300,7 +333,8 @@ def from_pickle(data, db_obj=None):
return dat
elif dtype == dict:
dat = _SaverDict(parent=parent)
dat._data.update(dict((key, process_tree(val, dat)) for key, val in item.items()))
dat._data.update(dict((key, process_tree(val, dat))
for key, val in item.items()))
return dat
elif dtype == set:
dat = _SaverSet(parent=parent)
@ -308,7 +342,8 @@ def from_pickle(data, db_obj=None):
return dat
elif hasattr(item, '__iter__'):
try:
# we try to conserve the iterable class if it accepts an iterator
# we try to conserve the iterable class if it
# accepts an iterator
return item.__class__(process_tree(val, parent) for val in item)
except (AttributeError, TypeError):
dat = _SaverList(parent=parent)
@ -326,7 +361,8 @@ def from_pickle(data, db_obj=None):
return dat
elif dtype == dict:
dat = _SaverDict(db_obj=db_obj)
dat._data.update((key, process_tree(val, parent=dat)) for key, val in data.items())
dat._data.update((key, process_tree(val, parent=dat))
for key, val in data.items())
return dat
elif dtype == set:
dat = _SaverSet(db_obj=db_obj)
@ -334,17 +370,22 @@ def from_pickle(data, db_obj=None):
return dat
return process_item(data)
def do_pickle(data):
"Perform pickle to string"
return to_str(dumps(data, protocol=PICKLE_PROTOCOL))
def do_unpickle(data):
"Retrieve pickle from pickled string"
return loads(to_str(data))
def dbserialize(data):
"Serialize to pickled form in one step"
return do_pickle(to_pickle(data))
def dbunserialize(data, db_obj=None):
"Un-serialize in one step. See from_pickle for help db_obj."
return do_unpickle(from_pickle(data, db_obj=db_obj))

View file

@ -24,8 +24,8 @@ TIMEFACTOR = settings.TIME_FACTOR
# Common real-life time measures, in seconds.
# You should not change these.
REAL_TICK = max(1.0, settings.TIME_TICK) #Smallest time unit (min 1s)
REAL_MIN = 60.0 # seconds per minute in real world
REAL_TICK = max(1.0, settings.TIME_TICK) # Smallest time unit (min 1s)
REAL_MIN = 60.0 # seconds per minute in real world
# Game-time units, in real-life seconds. These are supplied as
# a convenient measure for determining the current in-game time,
@ -40,6 +40,7 @@ WEEK = DAY * settings.TIME_DAY_PER_WEEK
MONTH = WEEK * settings.TIME_WEEK_PER_MONTH
YEAR = MONTH * settings.TIME_MONTH_PER_YEAR
class GameTime(Script):
"""
This sets up an script that keeps track of the
@ -51,12 +52,12 @@ class GameTime(Script):
"""
self.key = "sys_game_time"
self.desc = "Keeps track of the game time"
self.interval = REAL_MIN # update every minute
self.interval = REAL_MIN # update every minute
self.persistent = True
self.start_delay = True
self.attributes.add("game_time", 0.0) #IC time
self.attributes.add("run_time", 0.0) #OOC time
self.attributes.add("up_time", 0.0) #OOC time
self.attributes.add("game_time", 0.0) # IC time
self.attributes.add("run_time", 0.0) # OOC time
self.attributes.add("up_time", 0.0) # OOC time
def at_repeat(self):
"""
@ -77,6 +78,7 @@ class GameTime(Script):
"""
self.attributes.add("up_time", 0.0)
# Access routines
def gametime_format(seconds):
@ -94,27 +96,29 @@ def gametime_format(seconds):
# do this or we cancel the already counted
# timefactor in the timer script...
sec = int(seconds * TIMEFACTOR)
years, sec = sec/YEAR, sec % YEAR
months, sec = sec/MONTH, sec % MONTH
weeks, sec = sec/WEEK, sec % WEEK
days, sec = sec/DAY, sec % DAY
hours, sec = sec/HOUR, sec % HOUR
minutes, sec = sec/MIN, sec % MIN
years, sec = sec / YEAR, sec % YEAR
months, sec = sec / MONTH, sec % MONTH
weeks, sec = sec / WEEK, sec % WEEK
days, sec = sec / DAY, sec % DAY
hours, sec = sec / HOUR, sec % HOUR
minutes, sec = sec / MIN, sec % MIN
return (years, months, weeks, days, hours, minutes, sec)
def realtime_format(seconds):
"""
As gametime format, but with real time units
"""
sec = int(seconds)
years, sec = sec/29030400, sec % 29030400
months, sec = sec/2419200, sec % 2419200
weeks, sec = sec/604800, sec % 604800
days, sec = sec/86400, sec % 86400
hours, sec = sec/3600, sec % 3600
minutes, sec = sec/60, sec % 60
years, sec = sec / 29030400, sec % 29030400
months, sec = sec / 2419200, sec % 2419200
weeks, sec = sec / 604800, sec % 604800
days, sec = sec / 86400, sec % 86400
hours, sec = sec / 3600, sec % 3600
minutes, sec = sec / 60, sec % 60
return (years, months, weeks, days, hours, minutes, sec)
def gametime(format=False):
"""
Find the current in-game time (in seconds) since the start of the mud.
@ -136,6 +140,7 @@ def gametime(format=False):
return gametime_format(game_time)
return game_time
def runtime(format=False):
"""
Get the total actual time the server has been running (minus downtimes)
@ -151,6 +156,7 @@ def runtime(format=False):
return realtime_format(run_time)
return run_time
def uptime(format=False):
"""
Get the actual time the server has been running since last downtime.
@ -170,31 +176,33 @@ def uptime(format=False):
def gametime_to_realtime(secs=0, mins=0, hrs=0, days=0,
weeks=0, months=0, yrs=0):
"""
This method helps to figure out the real-world time it will take until a in-game time
has passed. E.g. if an event should take place a month later in-game, you will be able
to find the number of real-world seconds this corresponds to (hint: Interval events deal
with real life seconds).
This method helps to figure out the real-world time it will take until an
in-game time has passed. E.g. if an event should take place a month later
in-game, you will be able to find the number of real-world seconds this
corresponds to (hint: Interval events deal with real life seconds).
Example:
gametime_to_realtime(days=2) -> number of seconds in real life from now after which
2 in-game days will have passed.
gametime_to_realtime(days=2) -> number of seconds in real life from
now after which 2 in-game days will have passed.
"""
real_time = secs/TIMEFACTOR + mins*MIN + hrs*HOUR + \
days*DAY + weeks*WEEK + months*MONTH + yrs*YEAR
real_time = secs / TIMEFACTOR + mins * MIN + hrs * HOUR + \
days * DAY + weeks * WEEK + months * MONTH + yrs * YEAR
return real_time
def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0,
weeks=0, months=0, yrs=0):
"""
This method calculates how large an in-game time a real-world time interval would
correspond to. This is usually a lot less interesting than the other way around.
This method calculates how large an in-game time a real-world time
interval would correspond to. This is usually a lot less interesting
than the other way around.
Example:
realtime_to_gametime(days=2) -> number of game-world seconds
corresponding to 2 real days.
"""
game_time = TIMEFACTOR * (secs + mins*60 + hrs*3600 + days*86400 + \
weeks*604800 + months*2419200 + yrs*29030400)
game_time = TIMEFACTOR * (secs + mins * 60 + hrs * 3600 + days * 86400 +
weeks * 604800 + months * 2419200 + yrs * 29030400)
return game_time

View file

@ -8,6 +8,7 @@ a higher layer module.
from traceback import format_exc
from twisted.python import log
def log_trace(errmsg=None):
"""
Log a traceback to the log. This should be called
@ -27,7 +28,8 @@ def log_trace(errmsg=None):
for line in errmsg.splitlines():
log.msg('[EE] %s' % line)
except Exception:
log.msg('[EE] %s' % errmsg )
log.msg('[EE] %s' % errmsg)
def log_errmsg(errmsg):
"""
@ -43,6 +45,7 @@ def log_errmsg(errmsg):
log.msg('[EE] %s' % line)
#log.err('ERROR: %s' % (errormsg,))
def log_warnmsg(warnmsg):
"""
Prints/logs any warnings that aren't critical but should be noted.
@ -57,6 +60,7 @@ def log_warnmsg(warnmsg):
log.msg('[WW] %s' % line)
#log.msg('WARNING: %s' % (warnmsg,))
def log_infomsg(infomsg):
"""
Prints any generic debugging/informative info that should appear in the log.
@ -70,6 +74,7 @@ def log_infomsg(infomsg):
for line in infomsg.splitlines():
log.msg('[..] %s' % line)
def log_depmsg(depmsg):
"""
Prints a deprecation message

View file

@ -30,7 +30,8 @@ Example: To reach the search method 'get_object_with_player'
from django.contrib.contenttypes.models import ContentType
# limit symbol import from API
__all__ = ("search_object", "search_player", "search_script", "search_message", "search_channel", "search_help_entry")
__all__ = ("search_object", "search_player", "search_script",
"search_message", "search_channel", "search_help_entry")
# import objects this way to avoid circular import problems
@ -54,25 +55,29 @@ HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_c
# candidates=None,
# exact=True):
#
# Search globally or in a list of candidates and return results. The result is always an Object.
# Always returns a list.
# Search globally or in a list of candidates and return results.
# The result is always a list of Objects (or the empty list)
#
# Arguments:
# ostring: (str) The string to compare names against. By default (if not attribute_name
# is set), this will search object.key and object.aliases in order. Can also
# be on the form #dbref, which will, if exact=True be matched against primary key.
# attribute_name: (str): Use this named ObjectAttribute to match ostring against, instead
# of the defaults.
# typeclass (str or TypeClass): restrict matches to objects having this typeclass. This will help
# speed up global searches.
# candidates (list obj ObjectDBs): If supplied, search will only be performed among the candidates
# in this list. A common list of candidates is the contents of the current location searched.
# exact (bool): Match names/aliases exactly or partially. Partial matching matches the
# beginning of words in the names/aliases, using a matching routine to separate
# multiple matches in names with multiple components (so "bi sw" will match
# "Big sword"). Since this is more expensive than exact matching, it is
# recommended to be used together with the objlist keyword to limit the number
# of possibilities. This value has no meaning if searching for attributes/properties.
# ostring: (str) The string to compare names against. By default (if
# not attribute_name is set), this will search object.key
# and object.aliases in order. Can also be on the form #dbref,
# which will, if exact=True be matched against primary key.
# attribute_name: (str): Use this named ObjectAttribute to match ostring
# against, instead of the defaults.
# typeclass (str or TypeClass): restrict matches to objects having
# this typeclass. This will help speed up global searches.
# candidates (list obj ObjectDBs): If supplied, search will only be
# performed among the candidates in this list. A common list
# of candidates is the contents of the current location.
# exact (bool): Match names/aliases exactly or partially. Partial
# matching matches the beginning of words in the names/aliases,
# using a matching routine to separate multiple matches in
# names with multiple components (so "bi sw" will match
# "Big sword"). Since this is more expensive than exact
# matching, it is recommended to be used together with
# the objlist keyword to limit the number of possibilities.
# This keyword has no meaning if attribute_name is set.
#
# Returns:
# A list of matching objects (or a list with one unique match)

View file

@ -1,11 +1,12 @@
"""
Test runner for Evennia test suite. Run with "game/manage.py test".
Test runner for Evennia test suite. Run with "game/manage.py test".
"""
from django.conf import settings
from django.test.simple import DjangoTestSuiteRunner
class EvenniaTestSuiteRunner(DjangoTestSuiteRunner):
"""
This test runner only runs tests on the apps specified in src/ and game/ to
@ -13,11 +14,11 @@ class EvenniaTestSuiteRunner(DjangoTestSuiteRunner):
"""
def build_suite(self, test_labels, extra_tests=None, **kwargs):
"""
Build a test suite for Evennia. test_labels is a list of apps to test.
Build a test suite for Evennia. test_labels is a list of apps to test.
If not given, a subset of settings.INSTALLED_APPS will be used.
"""
if not test_labels:
test_labels = [applabel.rsplit('.', 1)[1] for applabel in settings.INSTALLED_APPS
test_labels = [applabel.rsplit('.', 1)[1] for applabel in settings.INSTALLED_APPS
if (applabel.startswith('src.') or applabel.startswith('game.'))]
return super(EvenniaTestSuiteRunner, self).build_suite(test_labels, extra_tests=extra_tests, **kwargs)

View file

@ -13,6 +13,7 @@ import re
import cgi
from ansi import *
class TextToHTMLparser(object):
"""
This class describes a parser for converting from ansi to html.
@ -36,10 +37,10 @@ class TextToHTMLparser(object):
('purple', ANSI_MAGENTA),
('cyan', hilite + ANSI_CYAN),
('teal', ANSI_CYAN),
('white', hilite + ANSI_WHITE), # pure white
('gray', ANSI_WHITE), #light grey
('dimgray', hilite + ANSI_BLACK), #dark grey
('black', ANSI_BLACK), #pure black
('white', hilite + ANSI_WHITE), # pure white
('gray', ANSI_WHITE), # light grey
('dimgray', hilite + ANSI_BLACK), # dark grey
('black', ANSI_BLACK), # pure black
]
colorback = [
('bgred', hilite + ANSI_BACK_RED),
@ -61,8 +62,8 @@ class TextToHTMLparser(object):
]
# make sure to escape [
colorcodes = [(c, code.replace("[",r"\[")) for c, code in colorcodes]
colorback = [(c, code.replace("[",r"\[")) for c, code in colorback]
colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes]
colorback = [(c, code.replace("[", r"\[")) for c, code in colorback]
# create stop markers
fgstop = [("", c.replace("[", r"\[")) for c in (normal, hilite, underline)]
bgstop = [("", c.replace("[", r"\[")) for c in (normal,)]
@ -74,7 +75,7 @@ class TextToHTMLparser(object):
re_bgs = [(cname, re.compile("(?:%s)(.*?)(?=%s)" % (code, bgstop))) for cname, code in colorback]
re_normal = re.compile(normal.replace("[", r"\["))
re_hilite = re.compile("(?:%s)(.*)(?=%s)" % (hilite.replace("[", r"\["), fgstop))
re_uline = re.compile("(?:%s)(.*?)(?=%s)" % (ANSI_UNDERLINE.replace("[",r"\["), fgstop))
re_uline = re.compile("(?:%s)(.*?)(?=%s)" % (ANSI_UNDERLINE.replace("[", r"\["), fgstop))
re_string = re.compile(r'(?P<htmlchars>[<&>])|(?P<space>^[ \t]+)|(?P<lineend>\r\n|\r|\n)', re.S|re.M|re.I)
def re_color(self, text):
@ -126,9 +127,9 @@ class TextToHTMLparser(object):
if c['lineend']:
return '<br>'
elif c['space'] == '\t':
return ' '*self.tabstop
return ' ' * self.tabstop
elif c['space']:
t = m.group().replace('\t', '&nbsp;'*self.tabstop)
t = m.group().replace('\t', '&nbsp;' * self.tabstop)
t = t.replace(' ', '&nbsp;')
return t
@ -155,6 +156,7 @@ class TextToHTMLparser(object):
HTML_PARSER = TextToHTMLparser()
#
# Access function
#

View file

@ -6,12 +6,19 @@ be of use when designing your own game.
"""
import os, sys, imp, types, math, re
import textwrap, datetime, random, traceback, inspect
import os
import sys
import imp
import types
import math
import re
import textwrap
import datetime
import random
import traceback
from inspect import ismodule
from collections import defaultdict
from twisted.internet import threads, defer, reactor
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
try:
@ -24,6 +31,7 @@ _GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
def is_iter(iterable):
"""
Checks if an object behaves iterably. However,
@ -39,10 +47,12 @@ def is_iter(iterable):
except AttributeError:
return False
def make_iter(obj):
"Makes sure that the object is always iterable."
return not hasattr(obj, '__iter__') and [obj] or obj
def fill(text, width=78, indent=0):
"""
Safely wrap text to a certain number of characters.
@ -69,7 +79,8 @@ def crop(text, width=78, suffix="[...]"):
return text
else:
lsuffix = len(suffix)
return "%s%s" % (text[:width-lsuffix], suffix)
return "%s%s" % (text[:width - lsuffix], suffix)
def dedent(text):
"""
@ -83,6 +94,7 @@ def dedent(text):
return ""
return textwrap.dedent(text)
def list_to_string(inlist, endsep="and", addquote=False):
"""
This pretty-formats a list as string output, adding
@ -108,6 +120,7 @@ def list_to_string(inlist, endsep="and", addquote=False):
return str(inlist[0])
return ", ".join(str(v) for v in inlist[:-1]) + " %s %s" % (endsep, inlist[-1])
def wildcard_to_regexp(instring):
"""
Converts a player-supplied string that may have wildcards in it to regular
@ -123,7 +136,7 @@ def wildcard_to_regexp(instring):
regexp_string += "^"
# Replace any occurances of * or ? with the appropriate groups.
regexp_string += instring.replace("*","(.*)").replace("?", "(.{1})")
regexp_string += instring.replace("*", "(.*)").replace("?", "(.{1})")
# If there's an asterisk at the end of the string, we can't impose the
# end of string ($) limiter.
@ -132,6 +145,7 @@ def wildcard_to_regexp(instring):
return regexp_string
def time_format(seconds, style=0):
"""
Function to return a 'prettified' version of a value in seconds.
@ -146,11 +160,11 @@ def time_format(seconds, style=0):
# We'll just use integer math, no need for decimal precision.
seconds = int(seconds)
days = seconds / 86400
days = seconds / 86400
seconds -= days * 86400
hours = seconds / 3600
hours = seconds / 3600
seconds -= hours * 3600
minutes = seconds / 60
minutes = seconds / 60
seconds -= minutes * 60
if style is 0:
@ -225,6 +239,7 @@ def time_format(seconds, style=0):
return retval
def datetime_format(dtobj):
"""
Takes a datetime object instance (e.g. from django's DateTimeField)
@ -250,6 +265,7 @@ def datetime_format(dtobj):
timestring = "%02i:%02i:%02i" % (hour, minute, second)
return timestring
def host_os_is(osname):
"""
Check to see if the host OS matches the query.
@ -258,6 +274,7 @@ def host_os_is(osname):
return True
return False
def get_evennia_version():
"""
Check for the evennia version info.
@ -268,6 +285,7 @@ def get_evennia_version():
except IOError:
return "Unknown version"
def pypath_to_realpath(python_path, file_ending='.py'):
"""
Converts a path on dot python form (e.g. 'src.objects.models') to
@ -284,11 +302,13 @@ def pypath_to_realpath(python_path, file_ending='.py'):
return "%s%s" % (path, file_ending)
return path
def dbref(dbref, reqhash=True):
"""
Converts/checks if input is a valid dbref.
If reqhash is set, only input strings on the form '#N', where N is an integer
is accepted. Otherwise strings '#N', 'N' and integers N are all accepted.
If reqhash is set, only input strings on the form '#N', where N is an
integer is accepted. Otherwise strings '#N', 'N' and integers N are all
accepted.
Output is the integer part.
"""
if reqhash:
@ -301,6 +321,7 @@ def dbref(dbref, reqhash=True):
return int(dbref) if dbref.isdigit() else None
return dbref if isinstance(dbref, int) else None
def to_unicode(obj, encoding='utf-8', force_string=False):
"""
This decodes a suitable object to the unicode format. Note that
@ -335,6 +356,7 @@ def to_unicode(obj, encoding='utf-8', force_string=False):
raise Exception("Error: '%s' contains invalid character(s) not in %s." % (obj, encoding))
return obj
def to_str(obj, encoding='utf-8', force_string=False):
"""
This encodes a unicode string back to byte-representation,
@ -366,6 +388,7 @@ def to_str(obj, encoding='utf-8', force_string=False):
raise Exception("Error: Unicode could not encode unicode string '%s'(%s) to a bytestring. " % (obj, encoding))
return obj
def validate_email_address(emailaddress):
"""
Checks if an email address is syntactically correct.
@ -382,18 +405,18 @@ def validate_email_address(emailaddress):
# Email address must be more than 7 characters in total.
if len(emailaddress) < 7:
return False # Address too short.
return False # Address too short.
# Split up email address into parts.
try:
localpart, domainname = emailaddress.rsplit('@', 1)
host, toplevel = domainname.rsplit('.', 1)
except ValueError:
return False # Address does not have enough parts.
return False # Address does not have enough parts.
# Check for Country code or Generic Domain.
if len(toplevel) != 2 and toplevel not in domains:
return False # Not a domain name.
return False # Not a domain name.
for i in '-_.%+.':
localpart = localpart.replace(i, "")
@ -401,9 +424,9 @@ def validate_email_address(emailaddress):
host = host.replace(i, "")
if localpart.isalnum() and host.isalnum():
return True # Email address is fine.
return True # Email address is fine.
else:
return False # Email address has funny characters.
return False # Email address has funny characters.
def inherits_from(obj, parent):
@ -447,6 +470,7 @@ def server_services():
del SESSIONS
return server
def uses_database(name="sqlite3"):
"""
Checks if the game is currently using a given database. This is a
@ -460,13 +484,15 @@ def uses_database(name="sqlite3"):
engine = settings.DATABASE_ENGINE
return engine == "django.db.backends.%s" % name
def delay(delay=2, retval=None, callback=None):
"""
Delay the return of a value.
Inputs:
delay (int) - the delay in seconds
retval (any) - this will be returned by this function after a delay
callback (func(retval)) - if given, this will be called with retval after delay seconds
callback (func(retval)) - if given, this will be called with retval
after delay seconds
Returns:
deferred that will fire with to_return after delay seconds
"""
@ -499,14 +525,15 @@ def clean_object_caches(obj):
pass
# on-object property cache
[_DA(obj, cname) for cname in obj.__dict__.keys() if cname.startswith("_cached_db_")]
[_DA(obj, cname) for cname in obj.__dict__.keys()
if cname.startswith("_cached_db_")]
try:
hashid = _GA(obj, "hashid")
hasid = obj.hashid
_TYPECLASSMODELS._ATTRIBUTE_CACHE[hashid] = {}
except AttributeError:
pass
_PPOOL = None
_PCMD = None
_PROC_ERR = "A process has ended with a probable error condition: process ended by signal 9."
@ -574,7 +601,6 @@ def run_async(to_execute, *args, **kwargs):
deferred.addCallback(callback, **callback_kwargs)
deferred.addErrback(errback, **errback_kwargs)
#
def check_evennia_dependencies():
"""
@ -631,7 +657,7 @@ def check_evennia_dependencies():
if settings.IRC_ENABLED:
try:
import twisted.words
twisted.words # set to avoid debug info about not-used import
twisted.words # set to avoid debug info about not-used import
except ImportError:
errstring += "\n ERROR: IRC is enabled, but twisted.words is not installed. Please install it."
errstring += "\n Linux Debian/Ubuntu users should install package 'python-twisted-words', others"
@ -642,6 +668,7 @@ def check_evennia_dependencies():
print "%s\n %s\n%s" % ("-"*78, errstring, '-'*78)
return no_error
def has_parent(basepath, obj):
"Checks if basepath is somewhere in objs parent tree."
try:
@ -652,17 +679,19 @@ def has_parent(basepath, obj):
# instance. Not sure if one should defend against this.
return False
def mod_import(module):
"""
A generic Python module loader.
Args:
module - this can be either a Python path (dot-notation like src.objects.models),
an absolute path (e.g. /home/eve/evennia/src/objects.models.py)
module - this can be either a Python path (dot-notation like
src.objects.models), an absolute path
(e.g. /home/eve/evennia/src/objects.models.py)
or an already import module object (e.g. models)
Returns:
an imported module. If the input argument was already a model, this is returned as-is,
otherwise the path is parsed and imported.
an imported module. If the input argument was already a model,
this is returned as-is, otherwise the path is parsed and imported.
Error:
returns None. The error is also logged.
"""
@ -691,7 +720,7 @@ def mod_import(module):
if not module:
return None
if type(module) == types.ModuleType:
if isinstance(module, types.ModuleType):
# if this is already a module, we are done
mod = module
else:
@ -699,8 +728,9 @@ def mod_import(module):
try:
mod = __import__(module, fromlist=["None"])
except ImportError, ex:
# check just where the ImportError happened (it could have been an erroneous
# import inside the module as well). This is the trivial way to do it ...
# check just where the ImportError happened (it could have been
# an erroneous import inside the module as well). This is the
# trivial way to do it ...
if str(ex) != "Import by filename is not supported.":
#log_trace("ImportError inside module '%s': '%s'" % (module, str(ex)))
raise
@ -726,29 +756,36 @@ def mod_import(module):
result[0].close()
return mod
def all_from_module(module):
"""
Return all global-level variables from a module as a dict
"""
mod = mod_import(module)
return dict((key, val) for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val)))
return dict((key, val) for key, val in mod.__dict__.items()
if not (key.startswith("_") or ismodule(val)))
def variable_from_module(module, variable=None, default=None):
"""
Retrieve a variable or list of variables from a module. The variable(s) must be defined
globally in the module. If no variable is given (or a list entry is None), a random variable
is extracted from the module.
Retrieve a variable or list of variables from a module. The variable(s)
must be defined globally in the module. If no variable is given (or a
list entry is None), a random variable is extracted from the module.
If module cannot be imported or given variable not found, default
is returned.
Args:
module (string or module)- python path, absolute path or a module
variable (string or iterable) - single variable name or iterable of variable names to extract
default (string) - default value to use if a variable fails to be extracted.
variable (string or iterable) - single variable name or iterable of
variable names to extract
default (string) - default value to use if a variable fails
to be extracted.
Returns:
a single value or a list of values depending on the type of 'variable' argument. Errors in lists
are replaced by the 'default' argument."""
a single value or a list of values depending on the type of
'variable' argument. Errors in lists are replaced by the
'default' argument.
"""
if not module:
return default
@ -761,12 +798,14 @@ def variable_from_module(module, variable=None, default=None):
result.append(mod.__dict__.get(var, default))
else:
# random selection
mvars = [val for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))]
mvars = [val for key, val in mod.__dict__.items()
if not (key.startswith("_") or ismodule(val))]
result.append((mvars and random.choice(mvars)) or default)
if len(result) == 1:
return result[0]
return result
def string_from_module(module, variable=None, default=None):
"""
This is a wrapper for variable_from_module that requires return
@ -779,6 +818,7 @@ def string_from_module(module, variable=None, default=None):
return [(isinstance(v, basestring) and v or default) for v in val]
return default
def init_new_player(player):
"""
Helper method to call all hooks, set flags etc on a newly created
@ -790,41 +830,50 @@ def init_new_player(player):
# player.character.db.FIRST_LOGIN = True
player.db.FIRST_LOGIN = True
def string_similarity(string1, string2):
"""
This implements a "cosine-similarity" algorithm as described for example in
Proceedings of the 22nd International Conference on Computation Linguistics
(Coling 2008), pages 593-600, Manchester, August 2008
The measure-vectors used is simply a "bag of words" type histogram (but for letters).
Proceedings of the 22nd International Conference on Computation
Linguistics (Coling 2008), pages 593-600, Manchester, August 2008
The measure-vectors used is simply a "bag of words" type histogram
(but for letters).
The function returns a value 0...1 rating how similar the two strings are. The strings can
contain multiple words.
The function returns a value 0...1 rating how similar the two strings
are. The strings can contain multiple words.
"""
vocabulary = set(list(string1 + string2))
vec1 = [string1.count(v) for v in vocabulary]
vec2 = [string2.count(v) for v in vocabulary]
try:
return float(sum(vec1[i]*vec2[i] for i in range(len(vocabulary)))) / \
return float(sum(vec1[i] * vec2[i] for i in range(len(vocabulary)))) / \
(math.sqrt(sum(v1**2 for v1 in vec1)) * math.sqrt(sum(v2**2 for v2 in vec2)))
except ZeroDivisionError:
# can happen if empty-string cmdnames appear for some reason. This is a no-match.
# can happen if empty-string cmdnames appear for some reason.
# This is a no-match.
return 0
def string_suggestions(string, vocabulary, cutoff=0.6, maxnum=3):
"""
Given a string and a vocabulary, return a match or a list of suggestsion based on
string similarity.
Given a string and a vocabulary, return a match or a list of suggestsion
based on string similarity.
Args:
string (str)- a string to search for
vocabulary (iterable) - a list of available strings
cutoff (int, 0-1) - limit the similarity matches (higher, the more exact is required)
cutoff (int, 0-1) - limit the similarity matches (higher, the more
exact is required)
maxnum (int) - maximum number of suggestions to return
Returns:
list of suggestions from vocabulary (could be empty if there are no matches)
list of suggestions from vocabulary (could be empty if there are
no matches)
"""
return [tup[1] for tup in sorted([(string_similarity(string, sugg), sugg) for sugg in vocabulary],
key=lambda tup: tup[0], reverse=True) if tup[0] >= cutoff][:maxnum]
return [tup[1] for tup in sorted([(string_similarity(string, sugg), sugg)
for sugg in vocabulary],
key=lambda tup: tup[0], reverse=True)
if tup[0] >= cutoff][:maxnum]
def string_partial_matching(alternatives, inp, ret_index=True):
"""
@ -837,7 +886,8 @@ def string_partial_matching(alternatives, inp, ret_index=True):
Input:
alternatives (list of str) - list of possible strings to match
inp (str) - search criterion
ret_index (bool) - return list of indices (from alternatives array) or strings
ret_index (bool) - return list of indices (from alternatives
array) or strings
Returns:
list of matching indices or strings, or an empty list
@ -855,7 +905,8 @@ def string_partial_matching(alternatives, inp, ret_index=True):
# loop over parts, making sure only to visit each part once
# (this will invalidate input in the wrong word order)
submatch = [last_index + alt_num for alt_num, alt_word
in enumerate(alt_words[last_index:]) if alt_word.startswith(inp_word)]
in enumerate(alt_words[last_index:])
if alt_word.startswith(inp_word)]
if submatch:
last_index = min(submatch) + 1
score += 1
@ -871,6 +922,7 @@ def string_partial_matching(alternatives, inp, ret_index=True):
return matches[max(matches)]
return []
def format_table(table, extra_space=1):
"""
Note: src.utils.prettytable is more powerful than this, but this
@ -909,6 +961,7 @@ def format_table(table, extra_space=1):
for icol, col in enumerate(table)])
return ftable
def get_evennia_pids():
"""
Get the currently valids PIDs (Process IDs) of the Portal and Server