diff --git a/game/gamesrc/scripts/examples/bodyfunctions.py b/game/gamesrc/scripts/examples/bodyfunctions.py index 605d87df41..1f2b11e3c2 100644 --- a/game/gamesrc/scripts/examples/bodyfunctions.py +++ b/game/gamesrc/scripts/examples/bodyfunctions.py @@ -19,21 +19,20 @@ class BodyFunctions(Script): This class defines the script itself """ - def at_script_creation(self): + def at_script_creation(self): self.key = "bodyfunction" self.desc = "Adds various timed events to a character." self.interval = 20 # seconds #self.repeats = 5 # repeat only a certain number of times self.start_delay = True # wait self.interval until first call - self.persistent = False + self.persistent = True def at_repeat(self): """ This gets called every self.interval seconds. We make a random check here so as to only return 33% of the time. """ - #) - if random.random() < 0.33: + if random.random() < 0.66: # no message this time return rand = random.random() diff --git a/src/objects/models.py b/src/objects/models.py index 820cbbac70..fa520321b6 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -788,6 +788,7 @@ class ObjectDB(TypedObject): objects to their respective home locations, as well as clean up all exits to/from the object. """ + if not self.at_object_delete(): # this is an extra pre-check # run before deletion mechanism diff --git a/src/scripts/models.py b/src/scripts/models.py index 5ad7534f4c..13d608c33a 100644 --- a/src/scripts/models.py +++ b/src/scripts/models.py @@ -253,3 +253,15 @@ class ScriptDB(TypedObject): default_typeclass_path = settings.DEFAULT_SCRIPT_TYPECLASS except: default_typeclass_path = "src.scripts.scripts.DoNothing" + + def at_typeclass_error(self): + """ + If this is called, it means the typeclass has a critical + error and cannot even be loaded. We don't allow a script + to be created under those circumstances. Already created, + permanent scripts are set to already be active so they + won't get activated now (next reboot the bug might be fixed) + """ + # By setting is_active=True, we trick the script not to run "again". + self.is_active = True + return super(ScriptDB, self).at_typeclass_error() diff --git a/src/scripts/scripthandler.py b/src/scripts/scripthandler.py index a5b0c5cb39..2d442f1057 100644 --- a/src/scripts/scripthandler.py +++ b/src/scripts/scripthandler.py @@ -54,10 +54,10 @@ class ScriptHandler(object): or a python path to such a class object. key - optional identifier for the script (often set in script definition) autostart - start the script upon adding it - """ + """ script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=autostart) if not script: - logger.log_errmsg("Script %s failed to be created/start." % scriptclass) + logger.log_errmsg("Script %s could not be created and/or started." % scriptclass) return False return True diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index 5364a6998f..eeeafb77ef 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -378,11 +378,11 @@ class Script(ScriptClass): # Some useful default Script types used by Evennia. class DoNothing(Script): - "An script that does nothing. Used as default." + "An script that does nothing. Used as default fallback." def at_script_creation(self): "Setup the script" self.key = "sys_do_nothing" - self.desc = "This does nothing." + self.desc = "This is a placeholder script." class CheckSessions(Script): "Check sessions regularly." diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 7a344be967..dff0621910 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -52,14 +52,6 @@ PARENTS = { "channel":"src.comms.models.Channel", "helpentry":"src.help.models.HelpEntry"} -# cached typeclasses for all typed models -TYPECLASS_CACHE = {} - -def reset(): - "Clean out the typeclass cache" - global TYPECLASS_CACHE - TYPECLASS_CACHE = {} - #------------------------------------------------------------ # # Attributes @@ -426,6 +418,10 @@ class TypedObject(SharedMemoryModel): # Database manager objects = managers.TypedObjectManager() + # object cache and flags + cached_typeclass_path = "" + cached_typeclass = None + # lock handler self.locks def __init__(self, *args, **kwargs): "We must initialize the parent first - important!" @@ -485,17 +481,22 @@ class TypedObject(SharedMemoryModel): #@property def typeclass_path_get(self): "Getter. Allows for value = self.typeclass_path" + typeclass_path = object.__getattribute__(self, 'cached_typeclass_path') + if typeclass_path: + return typeclass_path return self.db_typeclass_path #@typeclass_path.setter def typeclass_path_set(self, value): "Setter. Allows for self.typeclass_path = value" self.db_typeclass_path = value self.save() + object.__setattr__(self, "cached_typeclass_path", value) #@typeclass_path.deleter def typeclass_path_del(self): "Deleter. Allows for del self.typeclass_path" - self.db_typeclass_path = None + self.db_typeclass_path = "" self.save() + self.cached_typeclass_path = "" typeclass_path = property(typeclass_path_get, typeclass_path_set, typeclass_path_del) # date_created property @@ -613,96 +614,61 @@ class TypedObject(SharedMemoryModel): it allows for extending the Typed object for all different types of objects that the game needs. This property handles loading and initialization of the typeclass on the fly. - """ - - def errmsg(message): - """ - Helper function to display error. - """ - infochan = None - cmessage = message - try: - from src.comms.models import Channel - infochan = settings.CHANNEL_MUDINFO - infochan = Channel.objects.get_channel(infochan[0]) - if infochan: - cname = infochan.key - cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')]) - infochan.msg(message) - logger.log_errmsg(cmessage) - else: - # no mudinfo channel is found. Log instead. - cmessage = "\n".join(["[NO MUDINFO CHANNEL]: %s" % line for line in message.split('\n')]) - logger.log_errmsg(cmessage) - except Exception, e: - if ServerConfig.objects.conf("server_starting_mode"): - print cmessage - else: - logger.log_trace(cmessage) - path = object.__getattribute__(self, 'db_typeclass_path') - #print "typeclass_loading:", id(self), path + Note: The liberal use of object.__getattribute__ and __setattr__ (instead + of normal dot notation) is due to optimization: it avoids calling + the custom self.__getattribute__ more than necessary. + """ + + path = object.__getattribute__(self, "cached_typeclass_path") + if not path: + path = object.__getattribute__(self, 'db_typeclass_path') + typeclass = object.__getattribute__(self, "cached_typeclass") + try: + if typeclass and object.__getattribute__(typeclass, "path") == path: + return typeclass + except AttributeError: + pass errstring = "" if not path: - # this means we should get the default obj - # without giving errors. - defpath = object.__getattribute__(self, 'default_typeclass_path') - typeclass = object.__getattribute__(self, '_path_import')(defpath) - #typeclass = self._path_import(defpath) + # this means we should get the default obj without giving errors. + return object.__getattribute__(self, "get_default_typeclass")(cache=True, silent=True, save=True) else: # handle loading/importing of typeclasses, searching all paths. - # (self.typeclss_paths is a shortcut to settings.TYPECLASS_*_PATH + # (self.typeclass_paths is a shortcut to settings.TYPECLASS_*_PATHS # where '*' is either OBJECT, SCRIPT or PLAYER depending on the typed - # object). - typeclass_paths = [path] + ["%s.%s" % (prefix, path) for prefix in self.typeclass_paths] + # entities). + typeclass_paths = [path] + ["%s.%s" % (prefix, path) for prefix in object.__getattribute__(self, 'typeclass_paths')] + for tpath in typeclass_paths: - # try to find any matches to the typeclass path, in all possible permutations.. - typeclass = TYPECLASS_CACHE.get(tpath, None) - if typeclass: - # we've imported this before. We're done. - return typeclass - # not in cache. Try to import anew. + + # try to import and analyze the result typeclass = object.__getattribute__(self, "_path_import")(tpath) if callable(typeclass): - # don't return yet, we must cache this further down. - errstring = "" - break # leave test loop + # we succeeded to import. Cache and return. + object.__setattr__(self, 'db_typeclass_path', tpath) + object.__getattribute__(self, 'save')() + object.__setattr__(self, "cached_typeclass_path", tpath) + object.__setattr__(self, "cached_typeclass", typeclass) + return typeclass elif hasattr(typeclass, '__file__'): errstring += "\n%s seems to be just the path to a module. You need" % tpath errstring += " to specify the actual typeclass name inside the module too." else: - errstring += "\n%s" % typeclass # this will hold an error message. - - if not callable(typeclass): - # Still not a valid import. Fallback to default. - # Note that we don't save to this changed path! Fix the typeclass - # definition instead. - defpath = object.__getattribute__(self, "default_typeclass_path") - errstring += "\n\nUsing Default class '%s'." % defpath - typeclass = object.__getattribute__(self, "_path_import")(defpath) - errmsg(errstring) - if not callable(typeclass): - # if typeclass still doesn't exist at this point, we're in trouble. - # fall back to hardcoded core class which is wrong for e.g. scripts/players etc. - errstring = " %s\n%s" % (typeclass, errstring) - errstring += " Default class '%s' failed to load." % defpath - defpath = "src.objects.objects.Object" - errstring += "\n Using Evennia's default class '%s'." % defpath - typeclass = object.__getattribute__(self, "_path_import")(defpath) - errmsg(errstring) - else: - TYPECLASS_CACHE[path] = typeclass - return typeclass + errstring += "\n%s" % typeclass # this will hold a growing error message. + # If we reach this point we couldn't import any typeclasses. Return default. It's up to the calling + # method to use e.g. self.is_typeclass() to detect that the result is not the one asked for. + object.__getattribute__(self, "_display_errmsg")(errstring) + return object.__getattribute__(self, "get_default_typeclass")(cache=False, silent=False, save=False) #@typeclass.deleter def typeclass_del(self): - "Deleter. Allows for del self.typeclass (don't allow deletion)" - raise Exception("The typeclass property should never be deleted!") + "Deleter. Disallow 'del self.typeclass'" + raise Exception("The typeclass property should never be deleted, only changed in-place!") # typeclass property typeclass = property(typeclass_get, fdel=typeclass_del) - def _path_import(self, path): """ @@ -731,38 +697,95 @@ class TypedObject(SharedMemoryModel): errstring = "No class '%s' was found in module '%s'." errstring = errstring % (class_name, modpath) except Exception: - trc = traceback.format_exc() - errstring = "\n%sException importing '%s'." % (trc, path) + trc = traceback.format_exc() + errstring = "\n%sException importing '%s'." % (trc, path) # return the error. return errstring + + def _display_errmsg(self, message): + """ + Helper function to display error. + """ + infochan = None + cmessage = message + try: + from src.comms.models import Channel + infochan = settings.CHANNEL_MUDINFO + infochan = Channel.objects.get_channel(infochan[0]) + if infochan: + cname = infochan.key + cmessage = "\n".join(["[%s]: %s" % (cname, line) for line in message.split('\n')]) + infochan.msg(message) + else: + # no mudinfo channel is found. Log instead. + cmessage = "\n".join(["[NO MUDINFO CHANNEL]: %s" % line for line in message.split('\n')]) + logger.log_errmsg(cmessage) + except Exception, e: + if ServerConfig.objects.conf("server_starting_mode"): + print cmessage + else: + logger.log_trace(cmessage) - def is_typeclass(self, other_typeclass, exact=False): + def get_default_typeclass(self, cache=False, silent=False, save=False): + """ + This is called when a typeclass fails to + load for whatever reason. + Overload this in different entities. + + Default operation is to load a default typeclass. + """ + defpath = object.__getattribute__(self, "default_typeclass_path") + typeclass = object.__getattribute__(self, "_path_import")(defpath) + # if not silent: + # #errstring = "\n\nUsing Default class '%s'." % defpath + # object.__getattribute__(self, "_display_errmsg")(errstring) + + if not callable(typeclass): + # if typeclass still doesn't exist at this point, we're in trouble. + # fall back to hardcoded core class which is wrong for e.g. scripts/players etc. + failpath = defpath + defpath = "src.objects.objects.Object" + typeclass = object.__getattribute__(self, "_path_import")(defpath) + if not silent: + errstring += " %s\n%s" % (typeclass, errstring) + errstring += " Default class '%s' failed to load." % failpath + errstring += "\n Using Evennia's default class '%s'." % defpath + object.__getattribute__(self, "_display_errmsg")(errstring) + if not callable(typeclass): + # if this is still giving an error, Evennia is wrongly configured or buggy + raise Exception("CRITICAL ERROR: The final fallback typeclass %s cannot load!!" % defpath) + if save: + object.__setattr__(self, 'db_typeclass_path', defpath) + object.__getattribute__(self, 'save')() + if cache: + object.__setattr__(self, "cached_typeclass_path", defpath) + object.__setattr__(self, "cached_typeclass", typeclass) + return typeclass + + def is_typeclass(self, typeclass, exact=False): """ Returns true if this object has this type OR has a typeclass which is an subclass of the given typeclass. - other_typeclass - can be a class object or the - python path to such an object. + typeclass - can be a class object or the + python path to such an object to match against. + exact - returns true only if the object's type is exactly this typeclass, ignoring parents. - """ - if callable(other_typeclass): - # this is an actual class object. Get the path to it. - cls = other_typeclass.__class__ - other_typeclass = "%s.%s" % (cls.__module__, cls.__name__) - if not other_typeclass: - return False - if self.db_typeclass_path == other_typeclass: - return True - if not exact: - # check the parent chain. + """ + try: + typeclass = typeclass.path + except AttributeError: + pass + if exact: + current_path = object.__getattribute__(self, "cached_typeclass_path") + return typeclass and current_path == typeclass + else: + # check parent chain return any([cls for cls in self.typeclass.mro() - if other_typeclass == "%s.%s" % (cls.__module__, - cls.__name__)]) - return False - + if "%s.%s" % (cls.__module__, cls.__name__) == typeclass]) # # Object manipulation methods @@ -826,8 +849,6 @@ class TypedObject(SharedMemoryModel): new_typeclass.basetype_setup() new_typeclass.at_object_creation() - - # # Attribute handler methods # diff --git a/src/typeclasses/typeclass.py b/src/typeclasses/typeclass.py index 5dfdf8784b..83155db91d 100644 --- a/src/typeclasses/typeclass.py +++ b/src/typeclasses/typeclass.py @@ -19,7 +19,7 @@ from django.conf import settings # to *in-game* safety (if you can edit typeclasses you have # full access anyway), so no protection against changing # e.g. 'locks' or 'permissions' should go here. -PROTECTED = ['id', 'dbobj', 'db', 'objects', 'typeclass', +PROTECTED = ['id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass', 'attr', 'save', 'delete'] # If this is true, all non-protected property assignments @@ -70,8 +70,8 @@ class TypeClass(object): o = dbobj.object_class(dbobj) : this is used when dbobj.object_class is already set. """ - # typecheck of dbobj - we can't allow it to be added here unless - # unless it's really a TypedObject. + # typecheck of dbobj - we can't allow it to be added here + # unless it's really a TypedObject. dbobj_cls = object.__getattribute__(dbobj, '__class__') dbobj_mro = object.__getattribute__(dbobj_cls, '__mro__') if not any('src.typeclasses.models.TypedObject' @@ -83,16 +83,13 @@ class TypeClass(object): # store the needed things on the typeclass object.__setattr__(self, '_protected_attrs', PROTECTED) - # sync the database object to this typeclass. - cls = object.__getattribute__(self, '__class__') - db_typeclass_path = "%s.%s" % (object.__getattribute__(cls, '__module__'), - object.__getattribute__(cls, '__name__')) - if not object.__getattribute__(dbobj, "db_typeclass_path") == db_typeclass_path: - object.__setattr__(dbobj, "db_typeclass_path", db_typeclass_path) - object.__getattribute__(dbobj, "save")() - - # (The inheriting typed object classes often extend this __init__ to - # add handlers etc.) + # # sync the database object to this typeclass. + # cls = object.__getattribute__(self, '__class__') + # db_typeclass_path = "%s.%s" % (object.__getattribute__(cls, '__module__'), + # object.__getattribute__(cls, '__name__')) + # if not object.__getattribute__(dbobj, "db_typeclass_path") == db_typeclass_path: + # object.__setattr__(dbobj, "db_typeclass_path", db_typeclass_path) + # object.__getattribute__(dbobj, "save")() def __getattribute__(self, propname): """ diff --git a/src/utils/create.py b/src/utils/create.py index fe49521289..56a4cf50ac 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -21,8 +21,9 @@ Models covered: from django.conf import settings from django.contrib.auth.models import User from django.db import IntegrityError -from src.utils import logger, utils -from src.utils.utils import is_iter, has_parent +from src.utils.idmapper.models import SharedMemoryModel +from src.utils import logger, utils, idmapper +from src.utils.utils import is_iter, has_parent, inherits_from # # Game Object creation @@ -45,30 +46,28 @@ def create_object(typeclass, key=None, location=None, # deferred import to avoid loops from src.objects.objects import Object from src.objects.models import ObjectDB - #print "in create_object", typeclass + if isinstance(typeclass, ObjectDB): - # this is already an objectdb instance! - new_db_object = typeclass - typeclass = new_db_object.typeclass - elif isinstance(typeclass, Object): - # this is already an object typeclass! - new_db_object = typeclass.dbobj - typeclass = typeclass.__class__ - else: - # create database object - new_db_object = ObjectDB() - #new_db_object = ObjectDB() - if not callable(typeclass): - # this means typeclass might be a path. If not, - # the type mechanism will automatically assign - # the BASE_OBJECT_TYPE from settings. - if typeclass: - typeclass = utils.to_unicode(typeclass) - new_db_object.typeclass_path = typeclass - new_db_object.save() - # this will either load the typeclass or the default one - typeclass = new_db_object.typeclass - new_db_object.save() + # this is already an objectdb instance, extract its typeclass + typeclass = new_db_object.typeclass.path + elif isinstance(typeclass, Object) or utils.inherits_from(typeclass, Object): + # this is already an object typeclass, extract its path + typeclass = typeclass.path + + # create new database object + new_db_object = ObjectDB() + + # assign the typeclass + typeclass = utils.to_unicode(typeclass) + new_db_object.typeclass_path = typeclass + # this will either load the typeclass or the default one + typeclass = new_db_object.typeclass + + if not object.__getattribute__(new_db_object, "is_typeclass")(typeclass, exact=True): + # this will fail if we gave a typeclass as input and it still gave us a default + SharedMemoryModel.delete(new_db_object) + return None + # the name/key is often set later in the typeclass. This # is set here as a failsafe. if key: @@ -142,33 +141,37 @@ def create_script(typeclass, key=None, obj=None, locks=None, autostart=True): See src.scripts.manager for methods to manipulate existing scripts in the database. """ + # deferred import to avoid loops. from src.scripts.scripts import Script #print "in create_script", typeclass from src.scripts.models import ScriptDB + if isinstance(typeclass, ScriptDB): - #print "this is already a scriptdb instance!" - new_db_object = typeclass - typeclass = new_db_object.typeclass - elif isinstance(typeclass, Script): - #print "this is already an object typeclass!", typeclass, typeclass.__class__ - new_db_object = typeclass.dbobj - typeclass = typeclass.__class__ - else: - # create a new instance. - new_db_object = ScriptDB() - #new_db_object = ScriptDB() - if not callable(typeclass): - # try to load this in case it's a path - if typeclass: - typeclass = utils.to_unicode(typeclass) - new_db_object.db_typeclass_path = typeclass - new_db_object.save() - # this will load either the typeclass or the default one - typeclass = new_db_object.typeclass - new_db_object.save() + # this is already an objectdb instance, extract its typeclass + typeclass = new_db_object.typeclass.path + elif isinstance(typeclass, Script) or utils.inherits_from(typeclass, Script): + # this is already an object typeclass, extract its path + typeclass = typeclass.path + # create new database object + new_db_object = ScriptDB() + + # assign the typeclass + typeclass = utils.to_unicode(typeclass) + new_db_object.typeclass_path = typeclass + # this will either load the typeclass or the default one + typeclass = new_db_object.typeclass + + if not object.__getattribute__(new_db_object, "is_typeclass")(typeclass, exact=True): + # this can happen if the default was loaded (due to + # inability to load given typeclass), which we + # don't accept during creation. + SharedMemoryModel.delete(new_db_object) + return None + # the typeclass is initialized new_script = typeclass(new_db_object) + # store variables on the typeclass (which means # it's actually transparently stored on the db object) @@ -388,8 +391,8 @@ def create_player(name, email, password, new_user = User.objects.create_user(name, email, password) # create the associated Player for this User, and tie them together - new_player = PlayerDB(db_key=name, user=new_user) - new_player.save() + new_player = PlayerDB(db_key=name, user=new_user) + new_player.save() new_player.basetype_setup() # setup the basic locks and cmdset # call hook method (may override default permissions) diff --git a/src/utils/idmapper/base.py b/src/utils/idmapper/base.py index 2c363db5aa..27b75af886 100755 --- a/src/utils/idmapper/base.py +++ b/src/utils/idmapper/base.py @@ -101,8 +101,11 @@ class SharedMemoryModel(Model): """ if instance._get_pk_val() is not None: cls.__instance_cache__[instance._get_pk_val()] = instance - if hasattr(instance, 'at_cache') and callable(instance.at_cache): - instance.at_cache() + try: + object.__getattribute__(instance, "at_cache")() + except (TypeError, AttributeError): + pass + #key = "%s-%s" % (cls, instance.pk) #TCACHE[key] = instance #print "cached: %s (%s: %s) (total cached: %s)" % (instance, cls.__name__, len(cls.__instance_cache__), len(TCACHE)) diff --git a/src/utils/reloads.py b/src/utils/reloads.py index 3e1699c0a4..4c189cacf9 100644 --- a/src/utils/reloads.py +++ b/src/utils/reloads.py @@ -122,8 +122,7 @@ def reload_modules(): else: cemit_info(" No modules reloaded.") - # clean out cache dictionary of typeclasses, exits and channels - typeclassmodels.reset() + # clean out cache dictionary of typeclasses, exits and channels channelhandler.CHANNELHANDLER.update() # run through all objects in database, forcing re-caching.