Working on the OOB system, somewhat unstable at the moment.

This commit is contained in:
Griatch 2014-06-27 01:13:48 +02:00
parent d59500f574
commit 9ba212c264
6 changed files with 252 additions and 342 deletions

175
src/server/oob_cmds.py Normal file
View file

@ -0,0 +1,175 @@
"""
Out-of-band default plugin commands available for OOB handler.
This module implements commands as defined by the MSDP standard
(http://tintin.sourceforge.net/msdp/), but is independent of the
actual transfer protocol (webclient, MSDP, GMCP etc).
This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions
(not classes) defined globally in this module will be made available
to the oob mechanism.
oob functions have the following call signature:
function(oobhandler, session, *args, **kwargs)
where oobhandler is a back-reference to the central OOB_HANDLER
instance and session is the active session to get return data.
The function names are not case-sensitive (this allows for names
like "LIST" which would otherwise collide with Python builtins).
A function named _OOB_ERROR will retrieve error strings if it is
defined. It will get the error message as its 3rd argument.
"""
from django.conf import settings
_GA = object.__getattribute__
_SA = object.__setattr__
_NA_REPORT = lambda o: (None, "N/A")
_NA_SEND = lambda o: "N/A"
#------------------------------------------------------------
# All OOB commands must be on the form
# cmdname(oobhandler, session, *args, **kwargs)
#------------------------------------------------------------
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).
"""
session.msg(oob=("send", {"ERROR": errmsg}))
def ECHO(oobhandler, session, *args, **kwargs):
"Test/debug function, simply returning the args and kwargs"
session.msg(oob=("echo", args, kwargs))
def SEND(oobhandler, session, *args, **kwargs):
"""
This function directly returns the value of the given variable to the
session.
"""
print "In SEND:", oobhandler, session, args
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 result
session.msg(oob=("send", ret))
def REPORT(oobhandler, session, *args, **kwargs):
"""
This creates a tracker instance to track the data given in *args.
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):
typ, val = OOB_REPORTABLE.get(name, _NA_REPORT)(obj)
if typ == "field":
oobhandler.track_field(obj, session.sessid, name)
elif typ == "attribute":
oobhandler.track_attribute(obj, session.sessid, name)
def UNREPORT(oobhandler, session, vartype="prop", *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):
typ, val = OOB_REPORTABLE.get(name, _NA_REPORT)
if typ == "field":
oobhandler.untrack_field(obj, session.sessid, name)
else: # assume attribute
oobhandler.untrack_attribute(obj, session.sessid, name)
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.
"""
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":
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())))
#elif mode == "CONFIGURABLE_VARIABLES":
# pass
else:
session.msg(oob=("list", ("unsupported mode",)))
# 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 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",
# "OPPONENT_LEVEL", "OPPONENT_NAME", "AREA_NAME", "ROOM_EXITS", "ROOM_VNUM", "ROOM_NAME", "WORLD_TIME", "CLIENT_ID",
# "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"
OOB_SENDABLE = {
"CHARACTER_NAME": lambda o: o.key,
"SERVER_ID": lambda o: settings.SERVERNAME,
"ROOM_NAME": lambda o: o.db_location.key,
"ANSI_COLORS": lambda o: True,
"XTERM_256_COLORS": lambda o: True,
"UTF_8": lambda o: True
}
# mapping for which properties may be tracked. Each callable should return a tuple (type, value) where
# the type is one of "field" or "attribute" depending on what is being tracked.
OOB_REPORTABLE = {
"CHARACTER_NAME": lambda o: ("field", o.key),
"ROOM_NAME": lambda o: ("attribute", o.db_location.key)
}

View file

@ -1,317 +0,0 @@
"""
Out-of-band default plugin commands available for OOB handler. This
follows the standards defined by the MSDP out-of-band protocol
(http://tintin.sourceforge.net/msdp/)
This module is pointed to by settings.OOB_PLUGIN_MODULES. All functions
(not classes) defined globally in this module will be made available
to the oob mechanism.
function execution - the oob protocol can execute a function directly on
the server. The available functions must be defined
as global functions via settings.OOB_PLUGIN_MODULES.
repeat func execution - the oob protocol can request a given function be
executed repeatedly at a regular interval. This
uses an internal script pool.
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_MODULES.
What goes into the OOB_PLUGIN_MODULES is a list of modules with input
for the OOB system.
oob functions have the following call signature:
function(caller, session, *args, **kwargs)
oob trackers should build upon the OOBTracker class in this module
module and implement a minimum of the same functionality.
a global function oob_error will be used as optional error management.
"""
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
# default properties defined by the MSDP protocol. These are
# used by the SEND oob function below. Each entry should point
# to a function that takes the relevant object as input and
# returns the data it is responsible for. Most of these
# are commented out, but kept for reference for each
# game to implement.
OOB_SENDABLE = {
## General
"CHARACTER_NAME": lambda o: ("db_key", o.key),
"SERVER_ID": lambda o: ("settings.SERVERNAME", settings.SERVERNAME),
#"SERVER_TIME": _NA,
## Character
#"AFFECTS": _NA,
#"ALIGNMENT": _NA,
#"EXPERIENCE": _NA,
#"EXPERIENCE_MAX": _NA,
#"EXPERIENCE_TNL": _NA,
#"HEALTH": _NA,
#"HEALTH_MAX": _NA,
#"LEVEL": _NA,
#"RACE": _NA,
#"CLASS": _NA,
#"MANA": _NA,
#"MANA_MAX": _NA,
#"WIMPY": _NA,
#"PRACTICE": _NA,
#"MONEY": _NA,
#"MOVEMENT": _NA,
#"MOVEMENT_MAX": _NA,
#"HITROLL": _NA,
#"DAMROLL": _NA,
#"AC": _NA,
#"STR": _NA,
#"INT": _NA,
#"WIS": _NA,
#"DEX": _NA,
#"CON": _NA,
## Combat
#"OPPONENT_HEALTH": _NA,
#"OPPONENT_HEALTH_MAX": _NA,
#"OPPONENT_LEVEL": _NA,
#"OPPONENT_NAME": _NA,
## World
#"AREA_NAME": _NA,
#"ROOM_EXITS": _NA,
#"ROOM_VNUM": _NA,
"ROOM_NAME": lambda o: ("db_location", o.db_location.key),
#"WORLD_TIME": _NA,
## Configurable variables
#"CLIENT_ID": _NA,
#"CLIENT_VERSION": _NA,
#"PLUGIN_ID": _NA,
#"ANSI_COLORS": _NA,
#"XTERM_256_COLORS": _NA,
#"UTF_8": _NA,
#"SOUND": _NA,
#"MXP": _NA,
## GUI variables
#"BUTTON_1": _NA,
#"BUTTON_2": _NA,
#"BUTTON_3": _NA,
#"BUTTON_4": _NA,
#"BUTTON_5": _NA,
#"GAUGE_1": _NA,
#"GAUGE_2": _NA,
#"GAUGE_3": _NA,
#"GAUGE_4": _NA,
#"GAUGE_5": _NA
}
# mapping for which properties may be tracked
OOB_REPORTABLE = OOB_SENDABLE
#------------------------------------------------------------
# Tracker classes
#
# Trackers are added to a given object's trackerhandler and
# reports back changes when they happen. They are managed using
# the oobhandler's track/untrack mechanism
#------------------------------------------------------------
class TrackerBase(object):
"""
Base class for OOB Tracker objects.
"""
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
# Tracker objects stored on objects when using the MSDP report command
class ReportFieldTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
a named database field changes. The TrackerHandler calls this with
the correct arguments.
"""
def __init__(self, oobhandler, fieldname, sessid, *args, **kwargs):
"""
name - name of entity to track, such as "db_key"
sessid - sessid of session to report to
"""
self.oobhandler = oobhandler
self.fieldname = fieldname
self.sessid = sessid
def update(self, new_value, *args, **kwargs):
"Called by cache when updating the tracked entitiy"
# use oobhandler to relay data
try:
# we must never relay objects across the amp, only text data.
new_value = new_value.key
except AttributeError:
new_value = to_str(new_value, force_string=True)
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", self.fieldname,
new_value, *args, **kwargs)
class ReportAttributeTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
the Attribute updates. Since the field here is always "db_key",
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
def update(self, new_value, *args, **kwargs):
"Called by cache when attribute's db_value field updates"
try:
new_value = new_value.dbobj
except AttributeError:
new_value = to_str(new_value, force_string=True)
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", self.attrname, new_value, *args, **kwargs)
#------------------------------------------------------------
# OOB commands
# This defines which internal server commands the OOB handler
# makes available to the client. These commands are called
# automatically by the OOB mechanism by triggering the
# oobhandlers's execute_cmd method with the cmdname and
# eventual args/kwargs. All functions defined globally in this
# module will be made available to call by the oobhandler. Use
# _funcname if you want to exclude one. To allow for python-names
# like "list" here, these properties are case-insensitive.
#
# All OOB commands must be on the form
# cmdname(oobhandler, session, *args, **kwargs)
#------------------------------------------------------------
def oob_error(oobhandler, session, errmsg, *args, **kwargs):
"""
This is a special function 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).
"""
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.
"""
mode = mode.upper()
# 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",
# "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":
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())))
#elif mode == "CONFIGURABLE_VARIABLES":
# pass
else:
session.msg(oob=("list", ("unsupported mode",)))
def SEND(oobhandler, session, *args, **kwargs):
"""
This function directly returns the value of the given variable to the
session. vartype can be one of
"""
obj = session.get_puppet_or_player()
ret = {}
if obj:
for name in (a.upper() for a in args if a):
try:
key, value = OOB_SENDABLE.get(name, _NA)(obj)
ret[name] = value
except Exception, e:
ret[name] = str(e)
# 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.
vartype is one of "prop" (database fields) or "attr" (attributes)
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
key, val = OOB_REPORTABLE.get(name, _NA)(obj)
if key:
if key.startswith("db_"):
oobhandler.track_field(obj, session.sessid,
key, ReportFieldTracker)
else: # assume attribute
oobhandler.track_attribute(obj, session.sessid,
key, ReportAttributeTracker)
def UNREPORT(oobhandler, session, vartype="prop", *args, **kwargs):
"""
This removes tracking for the given data given in *args.
vartype is one of of "prop" or "attr".
"""
obj = session.get_puppet_or_player()
if obj:
for name in (a.upper() for a in args if a):
key, val = OOB_REPORTABLE.get(name, _NA)
if key:
if key.startswith("db_"):
oobhandler.untrack_field(obj, session.sessid, key)
else: # assume attribute
oobhandler.untrack_attribute(obj, session.sessid, key)
def ECHO(oobhandler, session, *args, **kwargs):
"Test function, returning the args, kwargs"
args = ["Return echo:"] + list(args)
session.msg(oob=("echo", args, kwargs))

View file

@ -45,7 +45,7 @@ from src.server.sessionhandler import SESSIONS
from src.scripts.tickerhandler import Ticker, TickerPool, TickerHandler
from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
from src.utils import logger
from src.utils.utils import all_from_module, make_iter
from src.utils.utils import all_from_module, make_iter, to_str
_SA = object.__setattr__
_GA = object.__getattribute__
@ -55,9 +55,9 @@ _DA = object.__delattr__
_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)))
# get custom error method or use the default
_OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
# get custom error method or use the default
_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):
@ -131,11 +131,12 @@ class TrackerHandler(object):
logger.log_trace()
# Tracker loaded by the TrackerHandler
# On-object Trackers to load with TrackerHandler
class TrackerBase(object):
"""
Base class for OOB Tracker objects.
Base class for OOB Tracker objects. Inherit from this
to define custom trackers.
"""
def __init__(self, *args, **kwargs):
pass
@ -148,6 +149,61 @@ class TrackerBase(object):
"Called when tracker is removed"
pass
class ReportFieldTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
a named database field changes. The TrackerHandler calls this with
the correct arguments.
"""
def __init__(self, oobhandler, fieldname, sessid, *args, **kwargs):
"""
name - name of entity to track, such as "db_key"
sessid - sessid of session to report to
"""
self.oobhandler = oobhandler
self.fieldname = fieldname
self.sessid = sessid
def update(self, new_value, *args, **kwargs):
"Called by cache when updating the tracked entitiy"
# use oobhandler to relay data
try:
# we must never relay objects across the amp, only text data.
new_value = new_value.key
except AttributeError:
new_value = to_str(new_value, force_string=True)
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", self.fieldname,
new_value, *args, **kwargs)
class ReportAttributeTracker(TrackerBase):
"""
Tracker that passively sends data to a stored sessid whenever
the Attribute updates. Since the field here is always "db_key",
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
def update(self, new_value, *args, **kwargs):
"Called by cache when attribute's db_value field updates"
try:
new_value = new_value.dbobj
except AttributeError:
new_value = to_str(new_value, force_string=True)
# this is a wrapper call for sending oob data back to session
self.oobhandler.msg(self.sessid, "report", self.attrname, new_value, *args, **kwargs)
# Ticker of auto-updating objects
class OOBTicker(Ticker):
@ -273,7 +329,7 @@ class OOBHandler(object):
sessid = session.sessid
return [key[2].lstrip("db_") for key in self.oob_tracker_storage.keys() if key[1] == sessid]
def track_field(self, obj, sessid, field_name, trackerclass):
def track_field(self, obj, sessid, field_name, trackerclass=ReportFieldTracker):
"""
Shortcut wrapper method for specifically tracking a database field.
Takes the tracker class as argument.
@ -289,7 +345,7 @@ class OOBHandler(object):
field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name
self._untrack(obj, sessid, field_name)
def track_attribute(self, obj, sessid, attr_name, trackerclass):
def track_attribute(self, obj, sessid, attr_name, trackerclass=ReportAttributeTracker):
"""
Shortcut wrapper method for specifically tracking the changes of an
Attribute on an object. Will create a tracker on the Attribute
@ -332,12 +388,6 @@ class OOBHandler(object):
"""
self.tickerhandler.remove(self, obj, interval)
def msg(self, sessid, funcname, *args, **kwargs):
"Shortcut to relay oob data back to portal. Used by oob functions."
session = self.sessionhandler.session_from_sessid(sessid)
#print "oobhandler msg:", sessid, session, funcname, args, kwargs
if session:
session.msg(oob=(funcname, args, kwargs))
# access method - called from session.msg()
@ -347,7 +397,7 @@ class OOBHandler(object):
using *args and **kwargs
"""
try:
print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
#print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys()
oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
oobfunc(self, session, *args, **kwargs)
except KeyError,e:
@ -364,5 +414,14 @@ class OOBHandler(object):
else:
logger.log_trace(errmsg)
raise Exception(errmsg)
def msg(self, sessid, funcname, *args, **kwargs):
"Shortcut to force-send an OOB message through the oobhandler to a session"
session = self.sessionhandler.session_from_sessid(sessid)
#print "oobhandler msg:", sessid, session, funcname, args, kwargs
if session:
session.msg(oob=(funcname, args, kwargs))
# access object
OOB_HANDLER = OOBHandler()

View file

@ -103,15 +103,9 @@ class SessionHandler(object):
def oobstruct_parser(self, oobstruct):
"""
Helper method for each session to use to parse oob structures
(The 'oob' kwarg of the msg() method)
((cmdname, (args), {}), ...)
Allowed oob structures are:
allowed oob structures are
(The 'oob' kwarg of the msg() method).
Allowed input oob structures are:
cmdname
((cmdname,), (cmdname,))
(cmdname,(arg, ))

View file

@ -230,7 +230,7 @@ LOCK_FUNC_MODULES = ("src.locks.lockfuncs",)
# Module holding OOB (Out of Band) hook objects. This allows for customization
# and expansion of which hooks OOB protocols are allowed to call on the server
# protocols for attaching tracker hooks for when various object field change
OOB_PLUGIN_MODULES = ["src.server.oob_msdp"]
OOB_PLUGIN_MODULES = ["src.server.oob_cmds"]
######################################################################
# Default command sets

View file

@ -29,8 +29,6 @@ function echo(message) {
doShow("out", "ECHO return: " + message) }
// Webclient code
function webclient_init(){
@ -95,6 +93,7 @@ function doSend(){
if (OOB_debug && outmsg.length > 4 && outmsg.substr(0, 5) == "##OOB") {
// test OOB messaging
doShow("out", "OOB input: " + outmsg.slice(5))
doOOB(JSON.parse(outmsg.slice(5))); }
else {
// normal output