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

View file

@ -409,6 +409,17 @@ class CmdSetHandler(object):
self.obj.cmdset_storage = storage self.obj.cmdset_storage = storage
self.update() 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): def all(self):
""" """
Returns all cmdsets. Returns all cmdsets.

View file

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

View file

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

View file

@ -75,9 +75,27 @@ class Object(TypeClass):
""" """
pass 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): 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 pass
@ -423,11 +441,15 @@ class Exit(Object):
""" """
Helper function for creating an exit command set + command. Helper function for creating an exit command set + command.
Note that exitdbobj is an ObjectDB instance. This is necessary for The command of this cmdset has the same name as the Exit object
handling reloads and avoid tracebacks while the typeclass system and allows the exit to react when the player enter the exit's name,
is rebooting. 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): class ExitCommand(command.Command):
""" """
This is a command that simply cause the caller This is a command that simply cause the caller
@ -476,7 +498,6 @@ class Exit(Object):
return exit_cmdset return exit_cmdset
# Command hooks # Command hooks
def basetype_setup(self): def basetype_setup(self):
""" """
Setup exit-security Setup exit-security
@ -486,20 +507,25 @@ class Exit(Object):
""" """
super(Exit, self).basetype_setup() 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() # setting default locks (overload these in at_object_creation()
self.locks.add("puppet:false()") # would be weird to puppet an exit ... 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("traverse:all()") # who can pass through exit by default
self.locks.add("get:false()") # noone can pick up the exit self.locks.add("get:false()") # noone can pick up the exit
def at_cache(self): # an exit should have a destination (this is replaced at creation time)
"Called when the typeclass is re-cached or reloaded. Should usually not be edited." if self.dbobj.location:
self.cmdset.add_default(self.create_exit_cmdset(self.dbobj), permanent=False) 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. # 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 key = validate only scripts with a particular key
dbref = validate only the single script with this particular id. dbref = validate only the single script with this particular id.
init_mode - When this mode is active, non-persistent scripts init_mode - This is used during server upstart. It causes non-persistent
will be removed and persistent scripts will be scripts to be removed and persistent scripts to be
force-restarted. force-restarted.
This method also makes sure start any scripts it validates, 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 # This deletes all non-persistent scripts from database
nr_stopped += self.remove_non_persistent(obj=obj) nr_stopped += self.remove_non_persistent(obj=obj)
# turn off the activity flag for all remaining scripts # turn off the activity flag for all remaining scripts
for script in self.all(): scripts = self.get_all_scripts()
script.is_active = False 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: if not scripts:
# no scripts available to validate
VALIDATE_ITERATION -= 1 VALIDATE_ITERATION -= 1
return None, None return None, None
#print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts)) #print "scripts to validate: [%s]" % (", ".join(script.key for script in scripts))
for script in scripts: for script in scripts:
if script.is_valid(): 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) nr_started += script.start(force_restart=init_mode)
#print "back from start." #print "back from start. nr_started=", nr_started
else: else:
script.stop() script.stop()
nr_stopped += 1 nr_stopped += 1

View file

@ -44,9 +44,10 @@ class ScriptClass(TypeClass):
def _stop_task(self): def _stop_task(self):
"stop task runner" "stop task runner"
try: try:
#print "stopping twisted task:", id(self.ndb.twisted_task), self.obj
self.ndb.twisted_task.stop() self.ndb.twisted_task.stop()
except Exception: except Exception:
pass logger.log_trace()
def _step_err_callback(self, e): def _step_err_callback(self, e):
"callback for runner errors" "callback for runner errors"
cname = self.__class__.__name__ cname = self.__class__.__name__
@ -107,30 +108,30 @@ class ScriptClass(TypeClass):
""" """
#print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj), #print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
# self.is_active, force_restart) # 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 return 0
if self.obj: obj = self.obj
if obj:
# check so the scripted object is valid and initalized # check so the scripted object is valid and initalized
try: try:
dummy = object.__getattribute__(self.obj, 'cmdset') dummy = object.__getattribute__(obj, 'cmdset')
except AttributeError: except AttributeError:
# this means the object is not initialized. # this means the object is not initialized.
self.dbobj.db_is_active = False self.dbobj.is_active = False
return 0 return 0
# try to start the script # try to start the script
try: try:
self.dbobj.db_is_active = True self.dbobj.is_active = True
self.dbobj.save()
self.at_start() self.at_start()
if self.dbobj.db_interval > 0: if self.dbobj.db_interval > 0:
self._start_task() self._start_task()
return 1 return 1
except Exception: except Exception:
logger.log_trace() logger.log_trace()
self.dbobj.db_is_active = False self.dbobj.is_active = False
self.dbobj.save()
return 0 return 0
def stop(self, kill=False): def stop(self, kill=False):
@ -141,6 +142,8 @@ class ScriptClass(TypeClass):
kill - don't call finishing hooks. kill - don't call finishing hooks.
""" """
#print "stopping script %s" % self.key #print "stopping script %s" % self.key
#import pdb
#pdb.set_trace()
if not kill: if not kill:
try: try:
self.at_stop() self.at_stop()
@ -149,11 +152,13 @@ class ScriptClass(TypeClass):
if self.dbobj.db_interval > 0: if self.dbobj.db_interval > 0:
try: try:
self._stop_task() self._stop_task()
except Exception: except Exception, e:
logger.log_trace("Stopping script %s(%s)" % (self.key, self.id))
pass pass
try: try:
self.dbobj.delete() self.dbobj.delete()
except AssertionError: except AssertionError:
logger.log_trace()
return 0 return 0
return 1 return 1
@ -175,7 +180,7 @@ class ScriptClass(TypeClass):
pass pass
# class ScriptClassOld(TypeClass): # class ScriptClass(TypeClass):
# """ # """
# Base class for all Scripts. # 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 twisted.web import server, static
from django.db import connection from django.db import connection
from django.conf import settings 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.models import ServerConfig
from src.server.sessionhandler import SESSIONS from src.server.sessionhandler import SESSIONS
from src.server import initial_setup from src.server import initial_setup
@ -99,7 +99,7 @@ class Evennia(object):
channelhandler.CHANNELHANDLER.update() channelhandler.CHANNELHANDLER.update()
# init all global scripts # init all global scripts
reloads.reload_scripts(init_mode=True) ScriptDB.objects.validate(init_mode=True)
# Make info output to the terminal. # Make info output to the terminal.
self.terminal_output() self.terminal_output()

View file

@ -182,7 +182,7 @@ class SessionBase(object):
self.log('Logged in: %s' % self) self.log('Logged in: %s' % self)
# start (persistent) scripts on this object # 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 #add session to connected list
SESSIONS.add_loggedin_session(self) SESSIONS.add_loggedin_session(self)

View file

@ -747,8 +747,8 @@ class TypedObject(SharedMemoryModel):
defpath = "src.objects.objects.Object" defpath = "src.objects.objects.Object"
typeclass = object.__getattribute__(self, "_path_import")(defpath) typeclass = object.__getattribute__(self, "_path_import")(defpath)
if not silent: if not silent:
errstring += " %s\n%s" % (typeclass, errstring) #errstring = " %s\n%s" % (typeclass, errstring)
errstring += " Default class '%s' failed to load." % failpath errstring = " Default class '%s' failed to load." % failpath
errstring += "\n Using Evennia's default class '%s'." % defpath errstring += "\n Using Evennia's default class '%s'." % defpath
object.__getattribute__(self, "_display_errmsg")(errstring) object.__getattribute__(self, "_display_errmsg")(errstring)
if not callable(typeclass): if not callable(typeclass):
@ -1083,7 +1083,12 @@ class TypedObject(SharedMemoryModel):
def all(self): def all(self):
return [val for val in self.__dict__.keys() return [val for val in self.__dict__.keys()
if not val.startswith['_']] 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() self._ndb_holder = NdbHolder()
return self._ndb_holder return self._ndb_holder
#@ndb.setter #@ndb.setter

View file

@ -98,13 +98,13 @@ def create_object(typeclass, key=None, location=None,
new_object.basetype_setup() # setup the basics of Exits, Characters etc. new_object.basetype_setup() # setup the basics of Exits, Characters etc.
new_object.at_object_creation() new_object.at_object_creation()
# custom-given variables override the hook # custom-given perms/locks overwrite hooks
if permissions: if permissions:
new_object.permissions = permissions new_object.permissions = permissions
if aliases:
new_object.aliases = aliases
if locks: if locks:
new_object.locks.add(locks) new_object.locks.add(locks)
if aliases:
new_object.aliases = aliases
# perform a move_to in order to display eventual messages. # perform a move_to in order to display eventual messages.
if home: if home:
@ -114,6 +114,10 @@ def create_object(typeclass, key=None, location=None,
else: else:
# rooms would have location=None. # rooms would have location=None.
new_object.location = None new_object.location = None
# post-hook setup (mainly used by Exits)
new_object.basetype_posthook_setup()
new_object.save() new_object.save()
return new_object return new_object

View file

@ -103,7 +103,8 @@ class SharedMemoryModel(Model):
cls.__instance_cache__[instance._get_pk_val()] = instance cls.__instance_cache__[instance._get_pk_val()] = instance
try: try:
object.__getattribute__(instance, "at_cache")() object.__getattribute__(instance, "at_cache")()
except (TypeError, AttributeError): except (TypeError, AttributeError), e:
#print e, instance._get_pk_val()
pass pass
#key = "%s-%s" % (cls, instance.pk) #key = "%s-%s" % (cls, instance.pk)

View file

@ -128,23 +128,20 @@ def reload_modules():
# run through all objects in database, forcing re-caching. # run through all objects in database, forcing re-caching.
def reload_scripts(scripts=None, obj=None, key=None, def reload_scripts(scripts=None, obj=None, key=None, dbref=None):
dbref=None, init_mode=False):
""" """
Run a validation of the script database. Run a validation of the script database.
obj - only validate scripts on this object obj - only validate scripts on this object
key - only validate scripts with this key key - only validate scripts with this key
dbref - only validate the script with this unique idref dbref - only validate the script with this unique idref
emit_to_obj - which object to receive error message 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, nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts,
obj=obj, key=key, obj=obj, key=key,
dbref=dbref, dbref=dbref,
init_mode=init_mode) init_mode=False)
if nr_started or nr_stopped: if nr_started or nr_stopped:
string = " Started %s script(s). Stopped %s invalid script(s)." % \ string = " Started %s script(s). Stopped %s invalid script(s)." % \
(nr_started, nr_stopped) (nr_started, nr_stopped)