From 46c2e372bfdd49e16cfe83e7f548b47c2bfbfe8b Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 16 Oct 2013 23:39:04 +0200 Subject: [PATCH] Added working report functionality for db-fields. Not tested for Attributes yet. Also working oob-repeat functionality, but still a traceback at reload. --- src/server/oobhandler.py | 204 +++++++++++++++++++++----------------- src/server/portal/msdp.py | 92 ++++------------- src/server/server.py | 11 +- src/utils/dbserialize.py | 6 +- 4 files changed, 144 insertions(+), 169 deletions(-) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index 6286b69ccf..0a5008ff8d 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -28,7 +28,7 @@ from src.server.models import ServerConfig from src.server.sessionhandler import SESSIONS from src.scripts.scripts import Script from src.utils.create import create_script -from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj +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 @@ -57,25 +57,27 @@ class TrackerHandler(object): # initiate store only with valid on-object fieldnames self.tracktargets = dict((key, {}) for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")()) - def add(self, fieldname, trackerkey, trackerobj): + def add(self, fieldname, tracker): """ Add tracker to the handler. Raises KeyError if fieldname does not exist. """ - self.tracktargets[fieldname][trackerkey] = trackerobj + trackerkey = tracker.__class__.__name__ + self.tracktargets[fieldname][trackerkey] = tracker self.ntrackers += 1 - def remove(self, fieldname, trackerkey, *args, **kwargs): + def remove(self, fieldname, trackerclass, *args, **kwargs): """ Remove tracker from handler. Raises KeyError if tracker is not found. """ - oobobj = self.tracktargets[fieldname][trackerkey] + trackerkey = trackerclass.__name__ + tracker = self.tracktargets[fieldname][trackerkey] try: - oobobj.at_delete(*args, **kwargs) + tracker.at_delete(*args, **kwargs) except Exception: logger.log_trace() - del oobobj + del tracker self.ntrackers -= 1 if self.ntrackers <= 0: # if there are no more trackers, clean this handler @@ -85,9 +87,9 @@ class TrackerHandler(object): """ Called by the field when it updates to a new value """ - for trackerobj in self.tracktargets[fieldname].values(): + for tracker in self.tracktargets[fieldname].values(): try: - trackerobj.update(fieldname, new_value) + tracker.update(new_value) except Exception: logger.log_trace() @@ -104,82 +106,61 @@ class TrackerBase(object): "Called when tracker is removed" pass -# Default tracker OOB class - -class OOBTracker(TrackerBase): +class _RepeaterScript(Script): """ - A OOB object that passively sends data to a stored sessid whenever - a named database field changes. + Repeating and subscription-enabled script for triggering OOB + functions. Maintained in a _RepeaterPool. """ - def __init__(self, fieldname, sessid, *args, **kwargs): - """ - name - name of entity to track, such as "db_key" - track_type - one of "field", "prop" or "attr" for Database fields, - non-database Property or Attribute - sessid - sessid of session to report to - """ - self.fieldname = fieldname - self.sessid = sessid + def at_script_creation(self): + "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.ndb.subscriptions = {} - def update(self, new_value, *args, **kwargs): - "Called by cache when updating the tracked entitiy" - SESSIONS.session_from_sessid(self.sessid).msg(oob=("trackreturn", - (self.fieldname, new_value))) + def at_repeat(self): + """ + Calls subscriptions every self.interval seconds + """ + for (func_key, obj, session, interval, args, kwargs) in self.ndb.subscriptions.values(): + OOB_HANDLER.execute_cmd(session, func_key, *args, **kwargs) + + def subscribe(self, store_key, obj, session, func_key, interval, *args, **kwargs): + """ + Sign up a subscriber to this oobfunction. Subscriber is + a database object with a dbref. + """ + self.ndb.subscriptions[store_key] = (func_key, obj, session, interval, args, kwargs) + + def unsubscribe(self, store_key): + """ + Unsubscribe from oobfunction. Returns True if removal was + successful, False otherwise + """ + 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 is used and accessed from oobhandler.repeat/unrepeat """ - class _RepeaterScript(Script): - """ - Repeating script for triggering OOB functions. Maintained in the pool. - """ - def at_script_creation(self): - "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.ndb.subscriptions = {} - - def at_repeat(self): - """ - Calls subscriptions every self.interval seconds - """ - for (func_key, caller, interval, args, kwargs) in self.ndb.subscriptions.values(): - try: - _OOB_FUNCS[func_key](caller, *args, **kwargs) - except Exception: - logger.log_trace() - - def subscribe(self, store_key, caller, func_key, interval, *args, **kwargs): - """ - Sign up a subscriber to this oobfunction. Subscriber is - a database object with a dbref. - """ - self.ndb.subscriptions[store_key] = (func_key, caller, interval, args, kwargs) - - def unsubscribe(self, store_key): - """ - Unsubscribe from oobfunction. Returns True if removal was - successful, False otherwise - """ - self.ndb.subscriptions.pop(store_key, None) - def __init__(self): self.scripts = {} - def add(self, store_key, caller, func_key, interval, *args, **kwargs): + def add(self, store_key, obj, sessid, func_key, interval, *args, **kwargs): """ Add a new tracking """ if interval not in self.scripts: # if no existing interval exists, create new script to fill the gap - new_tracker = create_script(self._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, caller, func_key, interval, *args, **kwargs) + session = SESSIONS.session_from_sessid(sessid) + self.scripts[interval].subscribe(store_key, obj, session, func_key, interval, *args, **kwargs) def remove(self, store_key, interval): """ @@ -215,8 +196,10 @@ class OOBHandler(object): ServerConf field """ 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)) 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)) def restore(self): @@ -228,17 +211,23 @@ class OOBHandler(object): tracker_storage = ServerConfig.objects.conf(key="oob_tracker_storage") if tracker_storage: self.oob_tracker_storage = dbunserialize(tracker_storage) - for tracker_key, (obj, sessid, fieldname, args, kwargs) in self.oob_tracker_storage.items(): - self.track(obj, sessid, fieldname, tracker_key, *args, **kwargs) + print "recovered from tracker_storage:", self.oob_tracker_storage + for (obj, sessid, fieldname, trackerclass, args, kwargs) in self.oob_tracker_storage.values(): + self.track(unpack_dbobj(obj), sessid, fieldname, trackerclass, *args, **kwargs) + # make sure to purce the storage + ServerConfig.objects.conf(key="oob_tracker_storage", delete=True) repeat_storage = ServerConfig.objects.conf(key="oob_repeat_storage") if repeat_storage: self.oob_repeat_storage = dbunserialize(repeat_storage) - for func_key, (caller, func_key, interval, args, kwargs) in self.oob_repeat_storage.items(): - self.repeat(caller, func_key, interval, *args, **kwargs) + print "recovered from repeat_storage:", self.oob_repeat_storage + for (obj, sessid, func_key, interval, args, kwargs) in self.oob_repeat_storage.values(): + self.repeat(unpack_dbobj(obj), sessid, func_key, interval, *args, **kwargs) + # make sure to purge the storage + ServerConfig.objects.conf(key="oob_repeat_storage", delete=True) - def track(self, obj, sessid, fieldname, oobclass, *args, **kwargs): + def track(self, obj, sessid, fieldname, trackerclass, *args, **kwargs): """ Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args, kwargs will be used to initialize the OOB hook before adding @@ -247,46 +236,59 @@ class OOBHandler(object): will be used as the property name when assigning the OOB to obj, otherwise tracker_key is ysed as the property name. """ - oobclass = _OOB_TRACKERS[tracker_key] # raise traceback if not found + try: + obj = obj.dbobj + except AttributeError: + pass if not "_trackerhandler" in _GA(obj, "__dict__"): # assign trackerhandler to object _SA(obj, "_trackerhandler", TrackerHandler(obj)) # initialize object - oob = oobclass(obj, sessid, fieldname, *args, **kwargs) - _GA(obj, "_trackerhandler").add(oob, fieldname) + tracker = trackerclass(self, fieldname, sessid, *args, **kwargs) + _GA(obj, "_trackerhandler").add(fieldname, tracker) # store calling arguments as a pickle for retrieval later - storekey = (pack_dbobj(obj), sessid, fieldname) - stored = (obj, sessid, fieldname, args, kwargs) + obj_packed = pack_dbobj(obj) + storekey = (obj_packed, sessid, fieldname) + stored = (obj_packed, sessid, fieldname, trackerclass, args, kwargs) self.oob_tracker_storage[storekey] = stored - def untrack(self, obj, sessid, fieldname, tracker_key, *args, **kwargs): + def untrack(self, obj, sessid, fieldname, trackerclass, *args, **kwargs): """ 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: # call at_delete hook - _GA(obj, "_trackerhandler").remove(fieldname, tracker_key, *args, **kwargs) + _GA(obj, "_trackerhandler").remove(fieldname, trackerclass, *args, **kwargs) except AttributeError: pass # remove the pickle from storage store_key = (pack_dbobj(obj), sessid, fieldname) self.oob_tracker_storage.pop(store_key, None) - def track_field(self, obj, sessid, field_name, tracker_key="oobtracker"): + def track_field(self, obj, sessid, field_name, trackerclass): """ Shortcut wrapper method for specifically tracking a database field. - Uses OOBTracker by default (change tracker_key to redirect) - Will create a tracker with a property name that the field cache - expects + Takes the tracker class as argument. """ # all database field names starts with db_* field_name = field_name if field_name.startswith("db_") else "db_%s" % field_name - oob_tracker_name = "_track_%s_change" % field_name # field cache looks for name on this form - self.track(obj, tracker_key, field_name, sessid, property_name=oob_tracker_name) + self.track(obj, sessid, field_name, trackerclass) - def track_attribute(self, obj, sessid, attr_name, tracker_key="oobtracker"): + def untrack_field(self, obj, sessid, field_name): + """ + Shortcut for untracking a database field. Uses OOBTracker by defualt + """ + 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): """ Shortcut wrapper method for specifically tracking the changes of an Attribute on an object. Will create a tracker on the Attribute Object and @@ -295,10 +297,17 @@ class OOBHandler(object): # get the attribute object if we can attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) if attrobj: - oob_tracker_name = "_track_db_value_change" - self.track(attrobj, tracker_key, attr_name, sessid, property_name=oob_tracker_name) + self.track(attrobj, sessid, attr_name, trackerclass) - def repeat(self, caller, func_key, interval=20, *args, **kwargs): + def untrack_attribute(self, obj, sessid, attr_name, tracker_key="oobtracker"): + """ + Shortcut for deactivating tracking for a given attribute. + """ + attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) + if attrobj: + self.untrack(attrobj, sessid, attr_name) + + def repeat(self, obj, sessid, func_key, interval=20, *args, **kwargs): """ Start a repeating action. Every interval seconds, the oobfunc corresponding to func_key is called with @@ -306,22 +315,32 @@ class OOBHandler(object): """ if not func_key in _OOB_FUNCS: raise KeyError("%s is not a valid OOB function name.") - store_key = (pack_dbobj(caller), func_key, interval) + try: + obj = obj.dbobj + except AttributeError: + pass + store_obj = pack_dbobj(obj) + store_key = (store_obj, sessid, func_key, interval) # prepare to store - self.oob_repeat_storage[store_key] = (caller, func_key, interval, args, kwargs) - self.oob_tracker_pool.add(store_key, caller, func_key, interval, *args, **kwargs) + self.oob_repeat_storage[store_key] = (store_obj, sessid, func_key, interval, args, kwargs) + self.oob_tracker_pool.add(store_key, obj, sessid, func_key, interval, *args, **kwargs) - def unrepeat(self, caller, func_key, interval=20): + def unrepeat(self, obj, sessid, func_key, interval=20): """ Stop a repeating action """ - store_key = (pack_dbobj(caller), func_key, interval) + try: + obj = obj.dbobj + except AttributeError: + pass + store_key = (pack_dbobj(obj), sessid, func_key, interval) self.oob_tracker_pool.remove(store_key, interval) self.oob_repeat_storage.pop(store_key, None) def msg(self, sessid, funcname, *args, **kwargs): "Shortcut to relay oob data back to portal" session = self.sessionhandler.session_from_sessid(sessid) + print "oobhandler msg:", sessid, session, funcname, args, kwargs if session: session.msg(oob=(funcname, args, kwargs)) @@ -331,6 +350,7 @@ class OOBHandler(object): using *args and **kwargs """ try: + 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: diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py index 6c21ac2800..6aea76b570 100644 --- a/src/server/portal/msdp.py +++ b/src/server/portal/msdp.py @@ -45,69 +45,6 @@ MSDP_COMMANDS_CUSTOM = {} # this maps MSDP command names to Evennia commands found in OOB_FUNC_MODULE. It # is up to these commands to return data on proper form. This is overloaded if # OOB_REPORTABLE is defined in the custom OOB module below. -MSDP_REPORTABLE = { - # General - "CHARACTER_NAME": "get_character_name", - "SERVER_ID": "get_server_id", - "SERVER_TIME": "get_server_time", - # Character - "AFFECTS": "char_affects", - "ALIGNMENT": "char_alignment", - "EXPERIENCE": "char_experience", - "EXPERIENCE_MAX": "char_experience_max", - "EXPERIENCE_TNL": "char_experience_tnl", - "HEALTH": "char_health", - "HEALTH_MAX": "char_health_max", - "LEVEL": "char_level", - "RACE": "char_race", - "CLASS": "char_class", - "MANA": "char_mana", - "MANA_MAX": "char_mana_max", - "WIMPY": "char_wimpy", - "PRACTICE": "char_practice", - "MONEY": "char_money", - "MOVEMENT": "char_movement", - "MOVEMENT_MAX": "char_movement_max", - "HITROLL": "char_hitroll", - "DAMROLL": "char_damroll", - "AC": "char_ac", - "STR": "char_str", - "INT": "char_int", - "WIS": "char_wis", - "DEX": "char_dex", - "CON": "char_con", - # Combat - "OPPONENT_HEALTH": "opponent_health", - "OPPONENT_HEALTH_MAX":"opponent_health_max", - "OPPONENT_LEVEL": "opponent_level", - "OPPONENT_NAME": "opponent_name", - # World - "AREA_NAME": "area_name", - "ROOM_EXITS": "area_room_exits", - "ROOM_NAME": "room_name", - "ROOM_VNUM": "room_dbref", - "WORLD_TIME": "world_time", - # Configurable variables - "CLIENT_ID": "client_id", - "CLIENT_VERSION": "client_version", - "PLUGIN_ID": "plugin_id", - "ANSI_COLORS": "ansi_colours", - "XTERM_256_COLORS": "xterm_256_colors", - "UTF_8": "utf_8", - "SOUND": "sound", - "MXP": "mxp", - # GUI variables - "BUTTON_1": "button1", - "BUTTON_2": "button2", - "BUTTON_3": "button3", - "BUTTON_4": "button4", - "BUTTON_5": "button5", - "GAUGE_1": "gauge1", - "GAUGE_2": "gauge2", - "GAUGE_3": "gauge3", - "GAUGE_4": "gauge4", - "GAUGE_5": "gauge5"} -MSDP_SENDABLE = MSDP_REPORTABLE # try to load custom OOB module OOB_MODULE = None#mod_import(settings.OOB_FUNC_MODULE) @@ -156,10 +93,13 @@ class Msdp(object): converted to a string), a list (will be converted to an MSDP_ARRAY), or a dictionary (will be converted to MSDP_TABLE). - Obs - this normally only returns tables and lists (var val val ...) rather than - arrays. It will convert *args to lists and **kwargs to tables and - if both are given to this method, this will result in a list followed - by a table, both having the same names. + OBS - there is no actual use of arrays and tables in the MSDP + specification or default commands -- are returns are implemented + as simple lists or named lists (our name for them here, these + un-bounded structures are not named in the specification). So for + now, this routine will not explicitly create arrays nor tables, + although there are helper methods ready should it be needed in + the future. """ def make_table(name, **kwargs): @@ -173,7 +113,7 @@ class Msdp(object): else: string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) string += MSDP_TABLE_CLOSE - return string + return stringk def make_array(name, *args): "build a array. Arrays may not nest tables by definition." @@ -188,8 +128,17 @@ class Msdp(object): string += MSDP_VAL.join(force_str(arg) for arg in args) return string + def make_named_list(name, **kwargs): + "build a named list - a table without start/end markers" + string = MSDP_VAR + force_str(name) + for key, val in kwargs.items(): + string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) + return string + # Default MSDP commands + print "MSDP outgoing:", cmdname, args, kwargs + cupper = cmdname.upper() if cupper == "LIST": self.data_out(make_list("LIST", *args)) @@ -200,15 +149,14 @@ class Msdp(object): elif cupper == "RESET": self.data_out(make_list("RESET", *args)) elif cupper == "SEND": - self.data_out(make_list("SEND", *args)) + self.data_out(make_named_list("SEND", **kwargs)) else: - # return list or tables. If both arg/kwarg is given, return one array and one table, both - # with the same name. + # return list or named lists. msdp_string = "" if args: msdp_string += make_list(cupper, *args) if kwargs: - msdp_string += make_table(cupper, **kwargs) + msdp_string += make_named_list(cupper, **kwargs) self.data_out(msdp_string) def msdp_to_evennia(self, data): diff --git a/src/server/server.py b/src/server/server.py index 21429e4b53..e900b0694a 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -214,10 +214,14 @@ class Evennia(object): [(o.typeclass, o.at_init()) for o in ObjectDB.get_all_cached_instances()] [(p.typeclass, p.at_init()) for p in PlayerDB.get_all_cached_instances()] + with open(SERVER_RESTART, 'r') as f: + mode = f.read() + if mode in ('True', 'reload'): + from src.server.oobhandler import OOB_HANDLER + OOB_HANDLER.restore() + if SERVER_STARTSTOP_MODULE: # call correct server hook based on start file value - with open(SERVER_RESTART, 'r') as f: - mode = f.read() if mode in ('True', 'reload'): # True was the old reload flag, kept for compatibilty SERVER_STARTSTOP_MODULE.at_server_reload_start() @@ -280,6 +284,9 @@ class Evennia(object): yield self.sessions.all_sessions_portal_sync() ServerConfig.objects.conf("server_restart_mode", "reload") + from src.server.oobhandler import OOB_HANDLER + OOB_HANDLER.save() + if SERVER_STARTSTOP_MODULE: SERVER_STARTSTOP_MODULE.at_server_reload_stop() diff --git a/src/utils/dbserialize.py b/src/utils/dbserialize.py index 73308a2eeb..fa2fc9b205 100644 --- a/src/utils/dbserialize.py +++ b/src/utils/dbserialize.py @@ -196,7 +196,7 @@ def pack_dbobj(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): +def unpack_dbobj(item): """ Check and convert internal representations back to Django database models. The fact that item is a packed dbobj should be checked before this call. @@ -267,7 +267,7 @@ def from_pickle(data, db_obj=None): return item elif _IS_PACKED_DBOBJ(item): # this must be checked before tuple - return _unpack_dbobj(item) + return unpack_dbobj(item) elif dtype == tuple: return tuple(process_item(val) for val in item) elif dtype == dict: @@ -289,7 +289,7 @@ def from_pickle(data, db_obj=None): return item elif _IS_PACKED_DBOBJ(item): # this must be checked before tuple - return _unpack_dbobj(item) + return unpack_dbobj(item) elif dtype == tuple: return tuple(process_tree(val) for val in item) elif dtype == list: