Added working report functionality for db-fields. Not tested for Attributes yet. Also working oob-repeat functionality, but still a traceback at reload.

This commit is contained in:
Griatch 2013-10-16 23:39:04 +02:00
parent bdcc8de5bc
commit 46c2e372bf
4 changed files with 144 additions and 169 deletions

View file

@ -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:

View file

@ -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):

View file

@ -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()

View file

@ -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: