Scripts and Exits updated. Fixed some deep issues with Scripts that caused object-based scripts to not properly shut down in some situations, as well as spawn multiple instances of themselves. I think this should resolve all "at_repeat doubling" issues reported. Due to optimizations in the typeclass cache loader in a previous update, the Exit cmdsets were not properly loaded (they were loaded at cache time, which now doesn't happen as often). So Exits instead rely on the new "at_cmdset_get" hook called by the cmdhandler. It allows dynamic modification of cmdsets just before they are accessed. Resolves issue173 (I hope). Resolves issue180. Resolves issue 181.

This commit is contained in:
Griatch 2011-08-11 21:16:35 +00:00
parent 16affc284b
commit 2b4e008d18
13 changed files with 135 additions and 68 deletions

View file

@ -84,6 +84,10 @@ def get_and_merge_cmdsets(caller):
that this is only relevant for logged-in callers.
"""
# The calling object's cmdset
try:
caller.at_cmdset_get()
except Exception:
logger.log_trace()
try:
caller_cmdset = caller.cmdset.current
except AttributeError:
@ -103,6 +107,12 @@ def get_and_merge_cmdsets(caller):
# Gather all cmdsets stored on objects in the room and
# also in the caller's inventory and the location itself
local_objlist = location.contents_get(exclude=caller.dbobj) + caller.contents + [location]
for obj in local_objlist:
try:
# call hook in case we need to do dynamic changing to cmdset
obj.at_cmdset_get()
except Exception:
logger.log_trace()
local_objects_cmdsets = [obj.cmdset.current for obj in local_objlist
if (obj.cmdset.current and obj.locks.check(caller, 'call', no_superuser_bypass=True))]
for cset in local_objects_cmdsets:

View file

@ -409,6 +409,17 @@ class CmdSetHandler(object):
self.obj.cmdset_storage = storage
self.update()
def has_cmdset(self, cmdset_key, must_be_default=False):
"""
checks so the cmdsethandler contains a cmdset with the given key.
must_be_default - only match against the default cmdset.
"""
if must_be_default:
return self.cmdset_stack and self.cmdset_stack[0].key == cmdset_key
else:
return any([cmdset.key == cmdset_key for cmdset in self.cmdset_stack])
def all(self):
"""
Returns all cmdsets.

View file

@ -226,6 +226,8 @@ class CmdScripts(MuxCommand):
else:
string = "Stopping script '%s'." % scripts[0].key
scripts[0].stop()
#import pdb
#pdb.set_trace()
ScriptDB.objects.validate() #just to be sure all is synced
else:
# multiple matches.

View file

@ -191,7 +191,6 @@ class ObjectDB(TypedObject):
self.cmdset = CmdSetHandler(self)
self.cmdset.update(init_mode=True)
self.scripts = ScriptHandler(self)
self.scripts.validate(init_mode=True)
self.nicks = ObjectNickHandler(self)
# Wrapper properties to easily set database fields. These are
@ -804,6 +803,9 @@ class ObjectDB(TypedObject):
if object.__getattribute__(self, 'player') and self.player:
self.player.character = None
self.player = None
for script in self.scripts.all():
script.stop()
# if self.player:
# self.player.user.is_active = False

View file

@ -75,12 +75,30 @@ class Object(TypeClass):
"""
pass
def basetype_posthook_setup(self):
"""
Called once, after basetype_setup and at_object_creation. This should generally not be overloaded unless
you are redefining how a room/exit/object works. It allows for basetype-like setup
after the object is created. An example of this is EXITs, who need to know keys, aliases, locks
etc to set up their exit-cmdsets.
"""
pass
def at_cache(self):
"""
Called whenever this object is cached or reloaded.
Called whenever this object is cached to the idmapper backend.
"""
pass
def at_cmdset_get(self):
"""
Called just before cmdsets on this object are requested by the
command handler. If changes need to be done on the fly to the cmdset
before passing them on to the cmdhandler, this is the place to do it.
This is called also if the object currently have no cmdsets.
"""
pass
def at_first_login(self):
"""
Only called once, the very first
@ -423,11 +441,15 @@ class Exit(Object):
"""
Helper function for creating an exit command set + command.
Note that exitdbobj is an ObjectDB instance. This is necessary for
handling reloads and avoid tracebacks while the typeclass system
is rebooting.
"""
The command of this cmdset has the same name as the Exit object
and allows the exit to react when the player enter the exit's name,
triggering the movement between rooms.
Note that exitdbobj is an ObjectDB instance. This is necessary
for handling reloads and avoid tracebacks if this is called while
the typeclass system is rebooting.
"""
#print "Exit:create_exit_cmdset "
class ExitCommand(command.Command):
"""
This is a command that simply cause the caller
@ -476,7 +498,6 @@ class Exit(Object):
return exit_cmdset
# Command hooks
def basetype_setup(self):
"""
Setup exit-security
@ -485,21 +506,26 @@ class Exit(Object):
overload the default locks (it is called after this one).
"""
super(Exit, self).basetype_setup()
# this is the fundamental thing for making the Exit work:
self.cmdset.add_default(self.create_exit_cmdset(self.dbobj), permanent=False)
# an exit should have a destination (this is replaced at creation time)
if self.dbobj.location:
self.destination = self.dbobj.location
# setting default locks (overload these in at_object_creation()
self.locks.add("puppet:false()") # would be weird to puppet an exit ...
self.locks.add("traverse:all()") # who can pass through exit by default
self.locks.add("get:false()") # noone can pick up the exit
def at_cache(self):
"Called when the typeclass is re-cached or reloaded. Should usually not be edited."
self.cmdset.add_default(self.create_exit_cmdset(self.dbobj), permanent=False)
self.locks.add("get:false()") # noone can pick up the exit
# an exit should have a destination (this is replaced at creation time)
if self.dbobj.location:
self.destination = self.dbobj.location
def at_cmdset_get(self):
"""
Called when the cmdset is requested from this object, just before the cmdset is
actually extracted. If no Exit-cmdset is cached, create it now.
"""
if self.ndb.exit_reset or not self.cmdset.has_cmdset("_exitset", must_be_default=True):
# we are resetting, or no exit-cmdset was set. Create one dynamically.
self.cmdset.add_default(self.create_exit_cmdset(self.dbobj), permanent=False)
self.ndb.exit_reset = False
# this and other hooks are what usually can be modified safely.

View file

@ -88,8 +88,8 @@ class ScriptManager(TypedObjectManager):
key = validate only scripts with a particular key
dbref = validate only the single script with this particular id.
init_mode - When this mode is active, non-persistent scripts
will be removed and persistent scripts will be
init_mode - This is used during server upstart. It causes non-persistent
scripts to be removed and persistent scripts to be
force-restarted.
This method also makes sure start any scripts it validates,
@ -117,26 +117,30 @@ class ScriptManager(TypedObjectManager):
# This deletes all non-persistent scripts from database
nr_stopped += self.remove_non_persistent(obj=obj)
# turn off the activity flag for all remaining scripts
for script in self.all():
script.is_active = False
scripts = self.get_all_scripts()
for script in scripts:
script.dbobj.is_active = False
elif not scripts:
# normal operation
if dbref and self.dbref(dbref):
scripts = self.get_id(dbref)
elif obj:
scripts = self.get_all_scripts_on_obj(obj, key=key)
else:
scripts = self.get_all_scripts(key=key) #self.model.get_all_cached_instances()
if dbref and self.dbref(dbref):
scripts = self.get_id(dbref)
elif scripts:
pass
elif obj:
scripts = self.get_all_scripts_on_obj(obj, key=key)
else:
scripts = self.model.get_all_cached_instances()#get_all_scripts(key=key)
if not scripts:
# no scripts available to validate
VALIDATE_ITERATION -= 1
return None, None
#print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts))
for script in scripts:
if script.is_valid():
#print "validating %s (%i)" % (script.key, id(script.dbobj))
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script.dbobj), init_mode)
nr_started += script.start(force_restart=init_mode)
#print "back from start."
#print "back from start. nr_started=", nr_started
else:
script.stop()
nr_stopped += 1

View file

@ -44,9 +44,10 @@ class ScriptClass(TypeClass):
def _stop_task(self):
"stop task runner"
try:
self.ndb.twisted_task.stop()
#print "stopping twisted task:", id(self.ndb.twisted_task), self.obj
self.ndb.twisted_task.stop()
except Exception:
pass
logger.log_trace()
def _step_err_callback(self, e):
"callback for runner errors"
cname = self.__class__.__name__
@ -74,7 +75,7 @@ class ScriptClass(TypeClass):
self.save()
def _step_task(self):
"step task"
try:
try:
d = maybeDeferred(self._step_succ_callback)
d.addErrback(self._step_err_callback)
return d
@ -107,30 +108,30 @@ class ScriptClass(TypeClass):
"""
#print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
# self.is_active, force_restart)
if self.dbobj.db_is_active and not force_restart:
# script already runs.
if self.dbobj.is_active and not force_restart:
# script already runs and should not be restarted.
return 0
if self.obj:
obj = self.obj
if obj:
# check so the scripted object is valid and initalized
try:
dummy = object.__getattribute__(self.obj, 'cmdset')
dummy = object.__getattribute__(obj, 'cmdset')
except AttributeError:
# this means the object is not initialized.
self.dbobj.db_is_active = False
self.dbobj.is_active = False
return 0
# try to start the script
try:
self.dbobj.db_is_active = True
self.dbobj.save()
self.dbobj.is_active = True
self.at_start()
if self.dbobj.db_interval > 0:
self._start_task()
return 1
except Exception:
logger.log_trace()
self.dbobj.db_is_active = False
self.dbobj.save()
self.dbobj.is_active = False
return 0
def stop(self, kill=False):
@ -141,6 +142,8 @@ class ScriptClass(TypeClass):
kill - don't call finishing hooks.
"""
#print "stopping script %s" % self.key
#import pdb
#pdb.set_trace()
if not kill:
try:
self.at_stop()
@ -149,11 +152,13 @@ class ScriptClass(TypeClass):
if self.dbobj.db_interval > 0:
try:
self._stop_task()
except Exception:
except Exception, e:
logger.log_trace("Stopping script %s(%s)" % (self.key, self.id))
pass
try:
self.dbobj.delete()
except AssertionError:
logger.log_trace()
return 0
return 1
@ -175,7 +180,7 @@ class ScriptClass(TypeClass):
pass
# class ScriptClassOld(TypeClass):
# class ScriptClass(TypeClass):
# """
# Base class for all Scripts.
# """

View file

@ -22,7 +22,7 @@ from twisted.internet import protocol, reactor, defer
from twisted.web import server, static
from django.db import connection
from django.conf import settings
from src.utils import reloads
from src.scripts.models import ScriptDB
from src.server.models import ServerConfig
from src.server.sessionhandler import SESSIONS
from src.server import initial_setup
@ -99,7 +99,7 @@ class Evennia(object):
channelhandler.CHANNELHANDLER.update()
# init all global scripts
reloads.reload_scripts(init_mode=True)
ScriptDB.objects.validate(init_mode=True)
# Make info output to the terminal.
self.terminal_output()

View file

@ -182,7 +182,7 @@ class SessionBase(object):
self.log('Logged in: %s' % self)
# start (persistent) scripts on this object
reloads.reload_scripts(obj=self.player.character, init_mode=True)
reloads.reload_scripts(obj=self.player.character)
#add session to connected list
SESSIONS.add_loggedin_session(self)

View file

@ -210,7 +210,7 @@ class Attribute(SharedMemoryModel):
"Deleter is disabled. Use the lockhandler.delete (self.lock.delete) instead"""
logger.log_errmsg("Lock_Storage (on %s) cannot be deleted. Use obj.lock.delete() instead." % self)
lock_storage = property(lock_storage_get, lock_storage_set, lock_storage_del)
#
#
@ -747,8 +747,8 @@ class TypedObject(SharedMemoryModel):
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 = " %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):
@ -1083,7 +1083,12 @@ class TypedObject(SharedMemoryModel):
def all(self):
return [val for val in self.__dict__.keys()
if not val.startswith['_']]
pass
def __getattribute__(self, key):
# return None if no matching attribute was found.
try:
return object.__getattribute__(self, key)
except AttributeError:
return None
self._ndb_holder = NdbHolder()
return self._ndb_holder
#@ndb.setter

View file

@ -91,20 +91,20 @@ def create_object(typeclass, key=None, location=None,
player.obj = new_object
new_object.destination = destination
# call the hook method. This is where all at_creation
# customization happens as the typeclass stores custom
# things on its database object.
# things on its database object.
new_object.basetype_setup() # setup the basics of Exits, Characters etc.
new_object.at_object_creation()
# custom-given variables override the hook
# custom-given perms/locks overwrite hooks
if permissions:
new_object.permissions = permissions
if aliases:
new_object.aliases = aliases
if locks:
new_object.locks.add(locks)
if aliases:
new_object.aliases = aliases
# perform a move_to in order to display eventual messages.
if home:
@ -114,6 +114,10 @@ def create_object(typeclass, key=None, location=None,
else:
# rooms would have location=None.
new_object.location = None
# post-hook setup (mainly used by Exits)
new_object.basetype_posthook_setup()
new_object.save()
return new_object

View file

@ -102,8 +102,9 @@ class SharedMemoryModel(Model):
if instance._get_pk_val() is not None:
cls.__instance_cache__[instance._get_pk_val()] = instance
try:
object.__getattribute__(instance, "at_cache")()
except (TypeError, AttributeError):
object.__getattribute__(instance, "at_cache")()
except (TypeError, AttributeError), e:
#print e, instance._get_pk_val()
pass
#key = "%s-%s" % (cls, instance.pk)
@ -118,7 +119,7 @@ class SharedMemoryModel(Model):
def _flush_cached_by_key(cls, key):
del cls.__instance_cache__[key]
del cls.__instance_cache__[key]
_flush_cached_by_key = classmethod(_flush_cached_by_key)
def flush_cached_instance(cls, instance):

View file

@ -128,23 +128,20 @@ def reload_modules():
# run through all objects in database, forcing re-caching.
def reload_scripts(scripts=None, obj=None, key=None,
dbref=None, init_mode=False):
def reload_scripts(scripts=None, obj=None, key=None, dbref=None):
"""
Run a validation of the script database.
obj - only validate scripts on this object
key - only validate scripts with this key
dbref - only validate the script with this unique idref
emit_to_obj - which object to receive error message
init_mode - during init-mode, non-persistent scripts are
cleaned out. All persistent scripts are force-started.
"""
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts,
obj=obj, key=key,
dbref=dbref,
init_mode=init_mode)
init_mode=False)
if nr_started or nr_stopped:
string = " Started %s script(s). Stopped %s invalid script(s)." % \
(nr_started, nr_stopped)