Finished refactoring all msdp+evenniacore OOB commands. Still untested

This commit is contained in:
Griatch 2015-02-13 00:11:49 +01:00
parent 156d80b7bb
commit d4b5533c20
3 changed files with 312 additions and 165 deletions

View file

@ -21,9 +21,9 @@ oob functions have the following call signature:
function(oobhandler, session, *args, **kwargs)
here, oobhandler always holds a back-reference to the central oob
handler, session is the active session and *args, **kwargs are what
is sent from the oob call.
here, oobhandler is a back-reference to the central oob handler (this
allows for deactivating itself in various ways), session is the active
session and *args, **kwargs are what is sent from the oob call.
A function called with OOB_ERROR will retrieve error strings if it is
defined. It will get the error message as its 3rd argument.
@ -40,6 +40,13 @@ oobcmdnames (like "MSDP.LISTEN" / "LISTEN" above) are case-sensitive. Note that
kwargs must be iterable. Non-iterables will be interpreted as a new
command name (you can send multiple oob commands with one msg() call))
Evennia introduces two internal extensions to MSDP, and that is the
MSDP_ARRAY and MSDP_TABLE commands. These are never sent across the
wire to the client (so this is fully compliant with the MSDP
protocol), but tells the Evennia OOB Protocol that you want to send a
"bare" array or table to the client, without prepending any command
name.
"""
from django.conf import settings
@ -53,166 +60,87 @@ _NA_SEND = lambda o: "N/A"
# cmdname(oobhandler, session, *args, **kwargs)
#------------------------------------------------------------
#
# General OOB commands
#
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
"""
A function with this name is special and is called by the oobhandler when an error
occurs already at the execution stage (such as the oob function
not being recognized or having the wrong args etc).
Error handling method. Error messages are relayed here.
Args:
oobhandler (OOBHandler): The main OOB handler.
session (Session): The session to receive the error
errmsg (str): The failure message
A function with this name is special and is also called by the
oobhandler when an error occurs already at the execution stage
(such as the oob function not being recognized or having the wrong
args etc). Call this from other oob functions to centralize error
management.
"""
session.msg(oob=("err", ("ERROR " + errmsg,)))
def oob_echo(oobhandler, session, *args, **kwargs):
"Test/debug function, simply returning the args and kwargs"
"""
Test echo function. Echoes args, kwargs sent to it.
Args:
oobhandler (OOBHandler): The main OOB handler.
session (Session): The Session to receive the echo.
args (list of str): Echo text.
kwargs (dict of str, optional): Keyed echo text
"""
session.msg(oob=("echo", args, kwargs))
# MSDP standard commands
##OOB{"SEND":"CHARACTER_NAME"} - from webclient
def oob_send(oobhandler, session, *args, **kwargs):
##OOB{"repeat":10}
def oob_repeat(oobhandler, session, oobfuncname, interval, *args, **kwargs):
"""
This function directly returns the value of the given variable to the
session.
Called as REPEAT <oobfunc> <interval>
Repeats a given OOB command with a certain frequency.
Args:
oobhandler (OOBHandler): main OOB handler.
session (Session): Session creating the repeat
oobfuncname (str): OOB function called every interval seconds
interval (int): Interval of repeat, in seconds.
Notes:
The command checks so that it cannot repeat itself.
"""
obj = session.get_puppet_or_player()
ret = {}
if obj:
for name in (a.upper() for a in args if a):
try:
value = OOB_SENDABLE.get(name, _NA_SEND)(obj)
ret[name] = value
except Exception, e:
ret[name] = str(e)
session.msg(oob=("send", ret))
else:
session.msg(oob=("err", ("You must log in first.",)))
##OOB{"REPORT":"TEST"}
def oob_report(oobhandler, session, *args, **kwargs):
"""
This creates a tracker instance to track the data given in *args.
The tracker will return with a oob structure
oob={"report":["attrfieldname", (args,), {kwargs}}
Note that the data name is assumed to be a field is it starts with db_*
and an Attribute otherwise.
"Example of tracking changes to the db_key field and the desc" Attribite:
REPORT(oobhandler, session, "CHARACTER_NAME", )
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
trackname = OOB_REPORTABLE.get(name, None)
if not trackname:
session.msg(oob=("err", ("No Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % trackname,)))
elif trackname.startswith("db_"):
oobhandler.track_field(obj, session.sessid, trackname)
else:
oobhandler.track_attribute(obj, session.sessid, trackname)
else:
session.msg(oob=("err", ("You must log in first.",)))
##OOB{"UNREPORT": "TEST"}
def oob_unreport(oobhandler, session, *args, **kwargs):
"""
This removes tracking for the given data given in *args.
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
trackname = OOB_REPORTABLE.get(name, None)
if not trackname:
session.msg(oob=("err", ("No Un-Reportable property '%s'. Use LIST REPORTED_VALUES." % name,)))
elif trackname.startswith("db_"):
oobhandler.untrack_field(obj, session.sessid, trackname)
else: # assume attribute
oobhandler.untrack_attribute(obj, session.sessid, trackname)
else:
session.msg(oob=("err", ("You must log in first.",)))
##OOB{"LIST":"COMMANDS"}
def oob_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.
"""
mode = mode.upper()
if mode == "COMMANDS":
session.msg(oob=("list", ("COMMANDS",
"LIST",
"REPORT",
"UNREPORT",
# "RESET",
"SEND")))
elif mode == "LISTS":
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())))
elif mode == "REPORTED_VARIABLES":
# we need to check so as to use the right return value depending on if it is
# an Attribute (identified by tracking the db_value field) or a normal database field
reported = oobhandler.get_all_tracked(session)
reported = [stored[2] if stored[2] != "db_value" else stored[4][0] for stored in reported]
session.msg(oob=("list", ["REPORTED_VARIABLES"] + reported))
elif mode == "SENDABLE_VARIABLES":
session.msg(oob=("list", ("SENDABLE_VARIABLES",) +
tuple(key for key in OOB_REPORTABLE.keys())))
elif mode == "CONFIGURABLE_VARIABLES":
# Not implemented (game specific)
pass
else:
session.msg(oob=("err", ("LIST", "Unsupported mode",)))
def _repeat_callback(oobhandler, session, *args, **kwargs):
"Set up by REPEAT"
session.msg(oob=("repeat", ("Repeat!",)))
##OOB{"REPEAT":10}
def oob_repeat(oobhandler, session, interval, *args, **kwargs):
"""
Test command for the repeat functionality. Note that the args/kwargs
must not be db objects (or anything else non-picklable), rather use
dbrefs if so needed. The callback must be defined globally and
will be called as
callback(oobhandler, session, *args, **kwargs)
"""
oobhandler.repeat(None, session.sessid, interval, _repeat_callback, *args, **kwargs)
if oobfuncname != "REPEAT":
oobhandler.add_repeat(None, session.sessid, oobfuncname, interval, *args, **kwargs)
##OOB{"UNREPEAT":10}
def oob_unrepeat(oobhandler, session, interval):
def oob_unrepeat(oobhandler, session, oobfuncname, interval):
"""
Disable repeating callback
Called with UNREPEAT <oobfunc> <interval>
Disable repeating callback.
Args:
oobhandler (OOBHandler): main OOB handler.
session (Session): Session controlling the repeat
oobfuncname (str): OOB function called every interval seconds
interval (int): Interval of repeat, in seconds.
Notes:
The command checks so that it cannot repeat itself.
"""
oobhandler.unrepeat(None, session.sessid, interval)
oobhandler.remove_repeat(None, session.sessid, oobfuncname, interval)
# Mapping for how to retrieve each property name.
# Each entry should point to a callable that gets the interesting object as
# input and returns the relevant value.
#
# MSDP standard commands
#
# MSDP recommends the following standard name conventions for making different properties available to the player
# MSDP recommends the following standard name mappings for general compliance:
# "CHARACTER_NAME", "SERVER_ID", "SERVER_TIME", "AFFECTS", "ALIGNMENT", "EXPERIENCE", "EXPERIENCE_MAX", "EXPERIENCE_TNL",
# "HEALTH", "HEALTH_MAX", "LEVEL", "RACE", "CLASS", "MANA", "MANA_MAX", "WIMPY", "PRACTICE", "MONEY", "MOVEMENT",
# "MOVEMENT_MAX", "HITROLL", "DAMROLL", "AC", "STR", "INT", "WIS", "DEX", "CON", "OPPONENT_HEALTH", "OPPONENT_HEALTH_MAX",
@ -220,6 +148,8 @@ def oob_unrepeat(oobhandler, session, interval):
# "CLIENT_VERSION", "PLUGIN_ID", "ANSI_COLORS", "XTERM_256_COLORS", "UTF_8", "SOUND", "MXP", "BUTTON_1", "BUTTON_2",
# "BUTTON_3", "BUTTON_4", "BUTTON_5", "GAUGE_1", "GAUGE_2","GAUGE_3", "GAUGE_4", "GAUGE_5"
# mapping from MSDP standard names to Evennia variables
OOB_SENDABLE = {
"CHARACTER_NAME": lambda o: o.key,
"SERVER_ID": lambda o: settings.SERVERNAME,
@ -229,23 +159,201 @@ OOB_SENDABLE = {
"UTF_8": lambda o: True
}
# mapping for which properties may be tracked. Each value points either to a database field
# (starting with db_*) or an Attribute name.
##OOB{"SEND":"CHARACTER_NAME"} - from webclient
def oob_send(oobhandler, session, *args, **kwargs):
"""
Called with the SEND MSDP command.
This function directly returns the value of the given variable to
the session. It assumes the object on which the variable sits
belongs to the session.
Args:
oobhandler (OOBHandler): oobhandler reference
session (Session): Session object
args (str): any number of properties to return. These
must belong to the OOB_SENDABLE dictionary.
Examples:
oob input: ("SEND", "CHARACTER_NAME", "SERVERNAME")
oob output: ("MSDP_TABLE", "CHARACTER_NAME", "Amanda",
"SERVERNAME", "Evennia")
"""
# mapping of MSDP name to a property
obj = session.get_puppet_or_player()
ret = {}
if obj:
for name in (a.upper() for a in args if a):
try:
value = OOB_SENDABLE.get(name, _NA_SEND)(obj)
ret[name] = value
except Exception, e:
ret[name] = str(e)
# return, make sure to use the right case
session.msg(oob=("MSDP_TABLE", (), ret))
else:
session.msg(oob=("err", ("You must log in first.",)))
# mapping standard MSDP keys to Evennia field names
OOB_REPORTABLE = {
"CHARACTER_NAME": "db_key",
"ROOM_NAME": "db_location",
"TEST" : "test"
}
##OOB{"REPORT":"TEST"}
def oob_report(oobhandler, session, *args, **kwargs):
"""
Called with the `REPORT PROPNAME` MSDP command.
Monitors the changes of given property name. Assumes reporting
happens on an objcet controlled by the session.
Args:
oobhandler (OOBHandler): The main OOB handler
session (Session): The Session doing the monitoring. The
property is assumed to sit on the entity currently
controlled by the Session. If puppeting, this is an
Object, otherwise the object will be the Player the
Session belongs to.
args (str or list): One or more property names to monitor changes in.
If a name starts with `db_`, the property is assumed to
be a field, otherwise an Attribute of the given name will
be monitored (if it exists).
Notes:
When the property updates, the monitor will send a MSDP_ARRAY
to the session of the form `(SEND, fieldname, new_value)`
Examples:
("REPORT", "CHARACTER_NAME")
("MSDP_TABLE", "CHARACTER_NAME", "Amanda")
"""
obj = session.get_puppet_or_player()
if obj:
for name in args:
propname = OOB_REPORTABLE.get(name, None)
if not propname:
session.msg(oob=("err", ("No Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % propname,)))
# the field_monitors require an oob function as a callback when they report a change.
elif propname.startswith("db_"):
oobhandler.add_field_monitor(obj, session.sessid, propname, "return_field_report")
else:
oobhandler.add_attribute_monitor(obj, session.sessid, propname, "return_attribute_report")
else:
session.msg(oob=("err", ("You must log in first.",)))
def oob_return_field_report(oobhandler, session, fieldname, obj, *args, **kwargs):
"""
This is a helper command called by the monitor when fieldname
changes. It is not part of the official MSDP specification but is
a callback used by the monitor to format the result before sending
it on.
"""
session.msg(oob=("MSDP_TABLE", (), {fieldname, getattr(obj, fieldname)}))
def oob_return_attribute_report(oobhandler, session, fieldname, obj, *args, **kwargs):
"""
This is a helper command called by the monitor when an Attribute
changes. We need to handle this a little differently from fields
since we are generally not interested in the field name (it's
always db_value for Attributes) but the Attribute's name.
This command is not part of the official MSDP specification but is
a callback used by the monitor to format the result before sending
it on.
"""
session.msg(oob=("MSDP_TABLE", (), {obj.db_key, getattr(obj, fieldname)}))
##OOB{"UNREPORT": "TEST"}
def oob_unreport(oobhandler, session, *args, **kwargs):
"""
This removes tracking for the given data.
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
propname = OOB_REPORTABLE.get(name, None)
if not propname:
session.msg(oob=("err", ("No Un-Reportable property '%s'. Use LIST REPORTED_VALUES." % name,)))
elif propname.startswith("db_"):
oobhandler.remove_field_monitor(obj, session.sessid, propname, "oob_return_field_report")
else: # assume attribute
oobhandler.remove_attribute_monitor(obj, session.sessid, propname, "oob_return_attribute_report")
else:
session.msg(oob=("err", ("You must log in first.",)))
##OOB{"LIST":"COMMANDS"}
def oob_list(oobhandler, session, mode, *args, **kwargs):
"""
Called with the `LIST <MODE>` MSDP command.
Args:
oobhandler (OOBHandler): The main OOB handler
session (Session): The Session asking for the information
mode (str): The available properties. One of
"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.
Examples:
oob in: LIST COMMANDS
oob out: (COMMANDS, (SEND, REPORT, LIST, ...)
"""
mode = mode.upper()
if mode == "COMMANDS":
session.msg(oob=("COMMANDS", ("LIST",
"REPORT",
"UNREPORT",
# "RESET",
"SEND")))
elif mode == "LISTS":
session.msg(oob=("LISTS",("REPORTABLE_VARIABLES",
"REPORTED_VARIABLES",
# "CONFIGURABLE_VARIABLES",
"SENDABLE_VARIABLES")))
elif mode == "REPORTABLE_VARIABLES":
session.msg(oob=("REPORTABLE_VARIABLES", tuple(key for key in OOB_REPORTABLE.keys())))
elif mode == "REPORTED_VARIABLES":
# we need to check so as to use the right return value depending on if it is
# an Attribute (identified by tracking the db_value field) or a normal database field
# reported is a list of tuples (obj, propname, args, kwargs)
reported = oobhandler.get_all_monitors(session.sessid)
reported = [rep[0].key if rep[1] == "db_value" else rep[1] for rep in reported]
session.msg(oob=("REPORTED_VARIABLES", reported))
elif mode == "SENDABLE_VARIABLES":
session.msg(oob=("SENDABLE_VARIABLES", tuple(key for key in OOB_REPORTABLE.keys())))
elif mode == "CONFIGURABLE_VARIABLES":
# Not implemented (game specific)
session.msg(oob=("err", ("LIST", "Not implemented (game specific).")))
else:
session.msg(oob=("err", ("LIST", "Unsupported mode",)))
# this maps the commands to the names available to use from
# the oob call
CMD_MAP = {"OOB_ERROR": oob_error, # will get error messages
# the oob call. The standard MSDP commands are capitalized
# as per the protocol, Evennia's own commands are not.
CMD_MAP = {"oob_error": oob_error, # will get error messages
"return_field_report": oob_return_field_report,
"return_attribute_report": oob_return_attribute_report,
"repeat": oob_repeat,
"unrepeat": oob_unrepeat,
"SEND": oob_send,
"ECHO": oob_echo,
"REPORT": oob_report,
"UNREPORT": oob_unreport,
"LIST": oob_list,
"REPEAT": oob_repeat,
"UNREPEAT": oob_unrepeat}
"LIST": oob_list
}

View file

@ -51,14 +51,18 @@ _DA = object.__delattr__
# load resources from plugin module
_OOB_FUNCS = {}
for mod in make_iter(settings.OOB_PLUGIN_MODULES):
_OOB_FUNCS.update(dict((key.lower(), func) for key, func in all_from_module(mod).items() if isfunction(func)))
_OOB_FUNCS.update(mod.CMD_MAP)
# get custom error method or use the default
# get the command to receive eventual error strings
_OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
if not _OOB_ERROR:
# create default oob error message function
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
"Error wrapper"
"""
Fallback error handler. This will be used if no custom
oob_error is defined and just echoes the error back to the
session.
"""
session.msg(oob=("err", ("ERROR ", errmsg)))
_OOB_ERROR = oob_error
@ -83,13 +87,13 @@ class OOBFieldMonitor(object):
"""
self.subscribers = defaultdict(list)
def __call__(self, new_value, obj):
def __call__(self, obj, fieldname):
"""
Called by the save() mechanism when the given
field has updated.
"""
for sessid, (oobfuncname, args, kwargs) in self.subscribers.items():
OOB_HANDLER.execute_cmd(sessid, oobfuncname, new_value, obj=obj, *args, **kwargs)
OOB_HANDLER.execute_cmd(sessid, oobfuncname, fieldname, obj, *args, **kwargs)
def add(self, sessid, oobfuncname, *args, **kwargs):
"""
@ -246,9 +250,14 @@ class OOBHandler(TickerHandler):
obj (Object) - the object on which to register the repeat
sessid (int) - session id of the session registering
oobfuncname (str) - oob function name to call every interval seconds
interval (int) - interval to call oobfunc, in seconds
*args, **kwargs - are used as arguments to the oobfunc
interval (int, optional) - interval to call oobfunc, in seconds
Notes:
*args, **kwargs are used as extra arguments to the oobfunc.
"""
# check so we didn't get a session instead of a sessid
if not isinstance(sessid, int):
sessid = sessid.sessid
hook = OOBAtRepeat()
hookname = self._get_repeat_hook_name(oobfuncname, interval, sessid)
_SA(obj, hookname, hook)
@ -265,9 +274,13 @@ class OOBHandler(TickerHandler):
Args:
obj (Object): The object on which the repeater sits
sessid (int): Session id of the Session that registered the repeat
oob
oobfuncname (str): Name of oob function to call at repeat
interval (int, optional): Number of seconds between repeats
"""
# check so we didn't get a session instead of a sessid
if not isinstance(sessid, int):
sessid = sessid.sessid
self.remove(obj, interval, idstring=oobfuncname)
hookname = self._get_repeat_hook_name(oobfuncname, interval, sessid)
try:
@ -287,9 +300,18 @@ class OOBHandler(TickerHandler):
oobfuncname (str): OOB function to call when field changes
Notes:
The optional args, and kwargs will be passed on to the
oobfunction.
When the field updates the given oobfunction will be called as
`oobfuncname(oobhandler, session, fieldname, obj, *args, **kwargs)`
where `fieldname` is the name of the monitored field and
`obj` is the object on which the field sits. From this you
can also easily get the new field value if you want.
"""
# check so we didn't get a session instead of a sessid
if not isinstance(sessid, int):
sessid = sessid.sessid
# all database field names starts with db_*
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
self._add_monitor(obj, sessid, field_name, field_name, oobfuncname=None)
@ -306,10 +328,13 @@ class OOBHandler(TickerHandler):
oobfuncname (str, optional): OOB command to call on that field
"""
# check so we didn't get a session instead of a sessid
if not isinstance(sessid, int):
sessid = sessid.sessid
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
self._remove_monitor(obj, sessid, field_name, oobfuncname=oobfuncname)
def add_attribute_track(self, obj, sessid, attr_name, oobfuncname):
def add_attribute_monitor(self, obj, sessid, attr_name, oobfuncname):
"""
Monitor the changes of an Attribute on an object. Will trigger when
the Attribute's `db_value` field updates.
@ -321,6 +346,9 @@ class OOBHandler(TickerHandler):
oobfuncname (str): OOB function to call when Attribute updates.
"""
# check so we didn't get a session instead of a sessid
if not isinstance(sessid, int):
sessid = sessid.sessid
# get the attribute object if we can
attrobj = obj.attributes.get(attr_name, return_obj=True)
if attrobj:
@ -337,6 +365,9 @@ class OOBHandler(TickerHandler):
oobfuncname (str): OOB function name called when Attribute updates.
"""
# check so we didn't get a session instead of a sessid
if not isinstance(sessid, int):
sessid = sessid.sessid
attrobj = obj.attributes.get(attr_name, return_obj=True)
if attrobj:
self._remove_monitor(attrobj, sessid, "db_value", attr_name, oobfuncname)
@ -347,9 +378,17 @@ class OOBHandler(TickerHandler):
Args:
sessid (id): Session id of monitoring Session
Returns:
stored monitors (tuple): A list of tuples
`(obj, fieldname, args, kwargs)` representing all
the monitoring the Session with the given sessid is doing.
"""
return [stored for key, stored in self.oob_monitor_storage.items() if key[1] == sessid]
# check so we didn't get a session instead of a sessid
if not isinstance(sessid, int):
sessid = sessid.sessid
# [(obj, fieldname, args, kwargs), ...]
return [(unpack_dbobj(key[0]), key[2], stored[0], stored[1])
for key, stored in self.oob_monitor_storage.items() if key[1] == sessid]
# access method - called from session.msg()

View file

@ -350,7 +350,7 @@ class SharedMemoryModel(Model):
# fieldname and the new value
fieldtracker = "_oob_at_%s_postsave" % fieldname
if hasattr(self, fieldtracker):
_GA(self, fieldtracker)(_GA(self, fieldname), self)
_GA(self, fieldtracker)(self, fieldname)
class WeakSharedMemoryModelBase(SharedMemoryModelBase):