diff --git a/evennia/server/oob_cmds.py b/evennia/server/oob_cmds.py index 1bee54cd2d..fbc716cabe 100644 --- a/evennia/server/oob_cmds.py +++ b/evennia/server/oob_cmds.py @@ -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 + 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 + 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 ` 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 + } diff --git a/evennia/server/oobhandler.py b/evennia/server/oobhandler.py index 276af8b1ee..c30184f928 100644 --- a/evennia/server/oobhandler.py +++ b/evennia/server/oobhandler.py @@ -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() diff --git a/evennia/utils/idmapper/base.py b/evennia/utils/idmapper/base.py index faa46c3c93..9da19f599b 100755 --- a/evennia/utils/idmapper/base.py +++ b/evennia/utils/idmapper/base.py @@ -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):