Trunk: Merged the Devel-branch (branches/griatch) into /trunk. This constitutes a major refactoring of Evennia. Development will now continue in trunk. See the wiki and the past posts to the mailing list for info. /Griatch

This commit is contained in:
Griatch 2010-08-29 18:46:58 +00:00
parent df29defbcd
commit f83c2bddf8
222 changed files with 22304 additions and 14371 deletions

0
src/scripts/__init__.py Normal file
View file

29
src/scripts/admin.py Normal file
View file

@ -0,0 +1,29 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from src.scripts.models import ScriptAttribute, ScriptDB
from django.contrib import admin
class ScriptAttributeAdmin(admin.ModelAdmin):
list_display = ('id', 'db_key', 'db_value', 'db_mode', 'db_obj', 'db_permissions')
list_display_links = ("id", 'db_key')
ordering = ["db_obj", 'db_key']
readonly_fields = ['db_permissions']
search_fields = ['id', 'db_key', 'db_obj']
save_as = True
save_on_top = True
list_select_related = True
admin.site.register(ScriptAttribute, ScriptAttributeAdmin)
class ScriptDBAdmin(admin.ModelAdmin):
list_display = ('id', 'db_key', 'db_typeclass_path', 'db_obj', 'db_interval', 'db_repeats', 'db_persistent')
list_display_links = ('id', 'db_key')
ordering = ['db_obj', 'db_typeclass_path']
readonly_fields = ['db_permissions']
search_fields = ['^db_key', 'db_typeclass_path']
save_as = True
save_on_top = True
list_select_related = True
admin.site.register(ScriptDB, ScriptDBAdmin)

171
src/scripts/manager.py Normal file
View file

@ -0,0 +1,171 @@
"""
The custom manager for Scripts.
"""
from src.typeclasses.managers import TypedObjectManager
from src.typeclasses.managers import returns_typeclass_list
VALIDATE_ITERATION = 0
class ScriptManager(TypedObjectManager):
"""
ScriptManager get methods
"""
@returns_typeclass_list
def get_all_scripts_on_obj(self, obj, key=None):
"""
Returns as result all the Scripts related to a particular object
"""
if not obj:
return []
scripts = self.filter(db_obj=obj)
if key:
return [script for script in scripts if script.key == key]
return scripts
@returns_typeclass_list
def get_all_scripts(self, key=None):
"""
Return all scripts, alternative only
scripts with a certain key/dbref.
"""
if key:
dbref = self.dbref(key)
if dbref:
# try to see if this is infact a dbref
script = self.dbref_search(dbref)
if script:
return script
# not a dbref. Normal key search
scripts = self.filter(db_key=key)
else:
scripts = self.all()
return scripts
def delete_script(self, dbref):
"""
This stops and deletes a specific script directly
from the script database. This might be
needed for global scripts not tied to
a specific game object.
"""
scripts = self.get_id(dbref)
for script in scripts:
script.stop()
def remove_non_persistent(self):
"""
This cleans up the script database of all non-persistent
scripts. It is called every time the server restarts.
"""
nr_deleted = 0
for script in [script for script in self.get_all_scripts()
if not script.persistent]:
script.stop()
nr_deleted += 1
return nr_deleted
def validate(self, scripts=None, obj=None, key=None, dbref=None,
init_mode=False):
"""
This will step through the script database and make sure
all objects run scripts that are still valid in the context
they are in. This is called by the game engine at regular
intervals but can also be initiated by player scripts.
If key and/or obj is given, only update the related
script/object.
Only one of the arguments are supposed to be supplied
at a time, since they are exclusive to each other.
scripts = a list of scripts objects obtained somewhere.
obj = validate only scripts defined on a special object.
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
force-restarted.
This method also makes sure start any scripts it validates,
this should be harmless, since already-active scripts
have the property 'is_running' set and will be skipped.
"""
# we store a variable that tracks if we are calling a
# validation from within another validation (avoids
# loops).
global VALIDATE_ITERATION
if VALIDATE_ITERATION > 0:
# we are in a nested validation. Exit.
VALIDATE_ITERATION -= 1
return None, None
VALIDATE_ITERATION += 1
# not in a validation - loop. Validate as normal.
nr_started = 0
nr_stopped = 0
if init_mode:
# special mode when server starts or object logs in.
# This deletes all non-persistent scripts from database
nr_stopped += self.remove_non_persistent()
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.get_all_scripts(key=key)
if not scripts:
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))
nr_started += script.start(force_restart=init_mode)
#print "back from start."
else:
script.stop()
nr_stopped += 1
VALIDATE_ITERATION -= 1
return nr_started, nr_stopped
@returns_typeclass_list
def script_search(self, ostring, obj=None, only_timed=False):
"""
Search for a particular script.
ostring - search criterion - a script ID or key
obj - limit search to scripts defined on this object
only_timed - limit search only to scripts that run
on a timer.
"""
ostring = ostring.strip()
dbref = self.dbref(ostring)
if dbref:
# this is a dbref, try to find the script directly
dbref_match = self.dbref_search(dbref)
if dbref_match:
ok = True
if obj and obj != dbref_match.obj:
ok = False
if only_timed and dbref_match.interval:
ok = False
if ok:
return [dbref_match]
# not a dbref; normal search
scripts = self.filter(db_key__iexact=ostring)
if obj:
scripts = scripts.exclude(db_obj=None).filter(db_obj__db_key__iexact=ostring)
if only_timed:
scripts = scripts.exclude(interval=0)
return scripts

255
src/scripts/models.py Normal file
View file

@ -0,0 +1,255 @@
"""
Scripts are entities that perform some sort of action, either only
once or repeatedly. They can be directly linked to a particular
Evennia Object or be stand-alonw (in the latter case it is considered
a 'global' script). Scripts can indicate both actions related to the
game world as well as pure behind-the-scenes events and
effects. Everything that has a time component in the game (i.e. is not
hard-coded at startup or directly created/controlled by players) is
handled by Scripts.
Scripts have to check for themselves that they should be applied at a
particular moment of time; this is handled by the is_valid() hook.
Scripts can also implement at_start and at_end hooks for preparing and
cleaning whatever effect they have had on the game object.
Common examples of uses of Scripts:
- load the default cmdset to the player object's cmdhandler
when logging in.
- switch to a different state, such as entering a text editor,
start combat or enter a dark room.
- Weather patterns in-game
- merge a new cmdset with the default one for changing which
commands are available at a particular time
- give the player/object a time-limited bonus/effect
"""
from django.conf import settings
from django.db import models
from src.objects.models import ObjectDB
from src.typeclasses.models import Attribute, TypedObject
from src.scripts.manager import ScriptManager
#------------------------------------------------------------
#
# ScriptAttribute
#
#------------------------------------------------------------
class ScriptAttribute(Attribute):
"Attributes for ScriptDB objects."
db_obj = models.ForeignKey("ScriptDB")
class Meta:
"Define Django meta options"
verbose_name = "Script Attribute"
verbose_name_plural = "Script Attributes"
#------------------------------------------------------------
#
# ScriptDB
#
#------------------------------------------------------------
class ScriptDB(TypedObject):
"""
The Script database representation.
The TypedObject supplies the following (inherited) properties:
key - main name
name - alias for key
typeclass_path - the path to the decorating typeclass
typeclass - auto-linked typeclass
date_created - time stamp of object creation
permissions - perm strings
dbref - #id of object
db - persistent attribute storage
ndb - non-persistent attribute storage
The ScriptDB adds the following properties:
desc - optional description of script
obj - the object the script is linked to, if any
interval - how often script should run
start_delay - if the script should start repeating right away
repeats - how many times the script should repeat
persistent - if script should survive a server reboot
is_active - bool if script is currently running
"""
#
# ScriptDB Database Model setup
#
# These databse fields are all set using their corresponding properties,
# named same as the field, but withtou the db_* prefix.
# inherited fields (from TypedObject):
# db_key, db_typeclass_path, db_date_created, db_permissions
# optional description.
db_desc = models.CharField(max_length=255, blank=True)
# A reference to the database object affected by this Script, if any.
db_obj = models.ForeignKey(ObjectDB, null=True, blank=True)
# how often to run Script (secs). 0 means there is no timer
db_interval = models.IntegerField(default=0)
# start script right away or wait interval seconds first
db_start_delay = models.BooleanField(default=False)
# how many times this script is to be repeated, if interval!=0.
db_repeats = models.IntegerField(default=0)
# defines if this script should survive a reboot or not
db_persistent = models.BooleanField(default=False)
# defines if this script has already been started in this session
db_is_active = models.BooleanField(default=False)
# Database manager
objects = ScriptManager()
class Meta:
"Define Django meta options"
verbose_name = "Script"
verbose_name_plural = "Scripts"
# Wrapper properties to easily set database fields. These are
# @property decorators that allows to access these fields using
# normal python operations (without having to remember to save()
# etc). So e.g. a property 'attr' has a get/set/del decorator
# defined that allows the user to do self.attr = value,
# value = self.attr and del self.attr respectively (where self
# is the script in question).
# desc property (wraps db_desc)
#@property
def desc_get(self):
"Getter. Allows for value = self.desc"
return self.db_desc
#@desc.setter
def desc_set(self, value):
"Setter. Allows for self.desc = value"
self.db_desc = value
self.save()
#@desc.deleter
def desc_del(self):
"Deleter. Allows for del self.desc"
self.db_desc = ""
self.save()
desc = property(desc_get, desc_set, desc_del)
# obj property (wraps db_obj)
#@property
def obj_get(self):
"Getter. Allows for value = self.obj"
return self.db_obj
#@obj.setter
def obj_set(self, value):
"Setter. Allows for self.obj = value"
self.db_obj = value
self.save()
#@obj.deleter
def obj_del(self):
"Deleter. Allows for del self.obj"
self.db_obj = None
self.save()
obj = property(obj_get, obj_set, obj_del)
# interval property (wraps db_interval)
#@property
def interval_get(self):
"Getter. Allows for value = self.interval"
return self.db_interval
#@interval.setter
def interval_set(self, value):
"Setter. Allows for self.interval = value"
self.db_interval = int(value)
self.save()
#@interval.deleter
def interval_del(self):
"Deleter. Allows for del self.interval"
self.db_interval = 0
self.save()
interval = property(interval_get, interval_set, interval_del)
# start_delay property (wraps db_start_delay)
#@property
def start_delay_get(self):
"Getter. Allows for value = self.start_delay"
return self.db_start_delay
#@start_delay.setter
def start_delay_set(self, value):
"Setter. Allows for self.start_delay = value"
self.db_start_delay = value
self.save()
#@start_delay.deleter
def start_delay_del(self):
"Deleter. Allows for del self.start_delay"
self.db_start_delay = False
self.save()
start_delay = property(start_delay_get, start_delay_set, start_delay_del)
# repeats property (wraps db_repeats)
#@property
def repeats_get(self):
"Getter. Allows for value = self.repeats"
return self.db_repeats
#@repeats.setter
def repeats_set(self, value):
"Setter. Allows for self.repeats = value"
self.db_repeats = int(value)
self.save()
#@repeats.deleter
def repeats_del(self):
"Deleter. Allows for del self.repeats"
self.db_repeats = 0
self.save()
repeats = property(repeats_get, repeats_set, repeats_del)
# persistent property (wraps db_persistent)
#@property
def persistent_get(self):
"Getter. Allows for value = self.persistent"
return self.db_persistent
#@persistent.setter
def persistent_set(self, value):
"Setter. Allows for self.persistent = value"
self.db_persistent = value
self.save()
#@persistent.deleter
def persistent_del(self):
"Deleter. Allows for del self.persistent"
self.db_persistent = False
self.save()
persistent = property(persistent_get, persistent_set, persistent_del)
# is_active property (wraps db_is_active)
#@property
def is_active_get(self):
"Getter. Allows for value = self.is_active"
return self.db_is_active
#@is_active.setter
def is_active_set(self, value):
"Setter. Allows for self.is_active = value"
self.db_is_active = value
self.save()
#@is_active.deleter
def is_active_del(self):
"Deleter. Allows for del self.is_active"
self.db_is_active = False
self.save()
is_active = property(is_active_get, is_active_set, is_active_del)
#
#
# ScriptDB class properties
#
#
# this is required to properly handle typeclass-dependent attributes
attribute_model_path = "src.scripts.models"
attribute_model_name = "ScriptAttribute"
# this is used by all typedobjects as a fallback
try:
default_typeclass_path = settings.DEFAULT_SCRIPT_TYPECLASS
except:
default_typeclass_path = "src.scripts.scripts.DoNothing"

View file

@ -0,0 +1,158 @@
"""
The script handler makes sure to check through all stored scripts
to make sure they are still relevant.
An scripthandler is automatically added to all game objects. You
access it through the property 'scripts' on the game object.
"""
from src.scripts.models import ScriptDB
from src.utils import create
from src.utils import logger
def format_script_list(scripts):
"Takes a list of scripts and format the output."
if not scripts:
return "<No scripts>"
string = "id obj\t\tkey\t\tinterval\trepeats\tnext\tpersistent"
for script in scripts:
obj = None
interval = None
repeats = None
try:
obj = script.obj
except AttributeError:
pass
try:
interval = script.interval
except AttributeError:
pass
try:
repeats = script.repeats
except AttributeError:
pass
try:
next_repeat = script.time_until_next_repeat()
except:
pass
if not obj:
obj = "<Global>"
if not interval:
interval = "N/A"
else:
interval = "%ss" % interval
if not repeats:
repeats = "N/A"
else:
repeats = "%ss" % repeats
if not next_repeat:
next_repeat = "N/A"
else:
next_repeat = "%ss" % next_repeat
string += "\n %s %s\t\t%s\t%s\t%s\t%s\t%s\n %s (%s)" % \
(script.id, obj, script.key, interval,
repeats, next_repeat, script.persistent,
script.typeclass_path, script.desc)
return string
class ScriptHandler(object):
"""
Implements the handler. This sits on each game object.
"""
def __init__(self, obj):
"""
Set up internal state.
obj - a reference to the object this handler is attached to.
We retrieve all scripts attached to this object and check
if they are all peristent. If they are not, they are just
cruft left over from a server shutdown.
"""
self.obj = obj
# this is required to stop a nasty loop in some situations that
# has the handler infinitely recursively re-added to its object.
self.obj.scripts = self
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj)
#print "starting scripthandler. %s has scripts %s" % (self.obj, scripts)
if scripts:
okscripts = [script for script in scripts if script.persistent == True]
delscripts = [script for script in scripts if script not in okscripts]
for script in delscripts:
#print "stopping script %s" % script
script.stop()
for script in okscripts:
#print "starting script %s" % script
script.start()
def __str__(self):
"List the scripts tied to this object"
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj)
string = ""
for script in scripts:
interval = "inf"
next_repeat = "inf"
repeats = "inf"
if script.interval:
interval = script.interval
if script.repeats:
repeats = script.repeats
try: next_repeat = script.time_until_next_repeat()
except: next_repeat = "?"
string += "\n '%s' (%s/%s, %s repeats): %s" % (script.key,
next_repeat,
interval,
repeats,
script.desc)
return string.strip()
def add(self, scriptclass, key=None, autostart=True):
"""
Add an script to this object. The scriptclass
argument can be either a class object
inheriting from Script, an instantiated script object
or a python path to such a class object.
"""
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)
def start(self, scriptkey):
"""
Find an already added script and force-start it
"""
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptkey)
for script in scripts:
script.start()
def delete(self, scriptkey):
"""
Forcibly delete a script from this object.
"""
delscripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptkey)
for script in delscripts:
script.stop()
def stop(self, scriptkey):
"""
Alias for delete.
"""
self.delete(scriptkey)
def all(self, scriptkey=None):
"""
Get all scripts stored in the handler, alternatively all matching a key.
"""
return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptkey)
def validate(self):
"""
Runs a validation on this object's scripts only.
This should be called regularly to crank the wheels.
"""
ScriptDB.objects.validate(obj=self.obj)

303
src/scripts/scripts.py Normal file
View file

@ -0,0 +1,303 @@
"""
This module contains the base Script class that all
scripts are inheriting from.
It also defines a few common scripts.
"""
from time import time
from twisted.internet import task
from src.server import sessionhandler
from src.typeclasses.typeclass import TypeClass
from src.scripts.models import ScriptDB
from src.comms import channelhandler
from src.utils import logger
#
# Base script, inherit from Script below instead.
#
class ScriptClass(TypeClass):
"""
Base class for all Scripts.
"""
# private methods for handling timers.
def __eq__(self, other):
"""
This has to be located at this level, having it in the
parent doesn't work.
"""
if other:
return other.id == self.id
return False
def _start_task(self):
"start the task runner."
if self.interval:
#print "Starting task runner"
start_now = not self.start_delay
self.ndb.twisted_task = task.LoopingCall(self._step_task)
self.ndb.twisted_task.start(self.interval, now=start_now)
self.ndb.time_last_called = int(time())
#self.save()
def _stop_task(self):
"stop the task runner"
if hasattr(self.ndb, "twisted_task"):
self.ndb.twisted_task.stop()
def _step_task(self):
"perform one repeat step of the script"
#print "Stepping task runner (obj %s)" % id(self)
#print "Has dbobj: %s" % hasattr(self, 'dbobj')
if not self.is_valid():
#the script is not valid anymore. Abort.
self.stop()
return
try:
self.at_repeat()
if self.repeats:
if self.repeats <= 1:
self.stop()
return
else:
self.repeats -= 1
self.ndb.time_last_called = int(time())
self.save()
except Exception:
logger.log_trace()
self._stop_task()
def time_until_next_repeat(self):
"""
Returns the time in seconds until the script will be
run again. If this is not a stepping script, returns None.
This is not used in any way by the script's stepping
system; it's only here for the user to be able to
check in on their scripts and when they will next be run.
"""
if self.interval and hasattr(self.ndb, 'time_last_called'):
return max(0, (self.ndb.time_last_called + self.interval) - int(time()))
else:
return None
def start(self, force_restart=False):
"""
Called every time the script is started (for
persistent scripts, this is usually once every server start)
force_restart - if True, will always restart the script, regardless
of if it has started before.
"""
#print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj),
# self.is_active, force_restart)
if force_restart:
self.is_active = False
should_start = True
if self.obj:
try:
#print "checking cmdset ... for obj", self.obj
dummy = object.__getattribute__(self.obj, 'cmdset')
#print "... checked cmdset"
except AttributeError:
#print "self.obj.cmdset not found. Setting is_active=False."
self.is_active = False
should_start = False
if self.is_active and not force_restart:
should_start = False
if should_start:
#print "... starting."
try:
self.is_active = True
self.at_start()
self._start_task()
return 1
except Exception:
#print ".. error when starting"
logger.log_trace()
self.is_active = False
return 0
else:
# avoid starting over.
#print "... Start cancelled (invalid start or already running)."
return 0 # this is used by validate() for counting started scripts
def stop(self):
"""
Called to stop the script from running.
This also deletes the script.
"""
#print "stopping script %s" % self.key
try:
self.at_stop()
except Exception:
logger.log_trace()
if self.interval:
try:
self._stop_task()
except Exception:
pass
self.is_running = False
try:
self.delete()
except AssertionError:
pass
def is_valid(self):
"placeholder"
pass
def at_start(self):
"placeholder."
pass
def at_stop(self):
"placeholder"
pass
def at_repeat(self):
"placeholder"
pass
#
# Base Script - inherit from this
#
class Script(ScriptClass):
"""
This is the class you should inherit from, it implements
the hooks called by the script machinery.
"""
def at_script_creation(self):
"""
Only called once, by the create function.
"""
self.key = "<unnamed>"
self.desc = ""
self.interval = 0
self.start_delay = False
self.repeats = 0
self.persistent = False
def is_valid(self):
"""
Is called to check if the script is valid to run at this time.
Should return a boolean. The method is assumed to collect all needed
information from its related self.obj.
"""
return True
def at_start(self):
"""
Called whenever the script is started, which for persistent
scripts is at least once every server start.
"""
pass
def at_repeat(self):
"""
Called repeatedly if this Script is set to repeat
regularly.
"""
pass
def at_stop(self):
"""
Called whenever when it's time for this script to stop
(either because is_valid returned False or )
"""
pass
# Some useful default Script types
class DoNothing(Script):
"An script that does nothing. Used as default."
def at_script_creation(self):
"Setup the script"
self.key = "sys_do_nothing"
self.desc = "This does nothing."
self.persistent = False
def is_valid(self):
"This script disables itself as soon as possible"
return False
class CheckSessions(Script):
"Check sessions regularly."
def at_script_creation(self):
"Setup the script"
self.key = "sys_session_check"
self.desc = "Checks sessions so they are live."
self.interval = 60 # repeat every 60 seconds
self.persistent = True
def at_repeat(self):
"called every 60 seconds"
#print "session check!"
sessionhandler.check_all_sessions()
class ValidateScripts(Script):
"Check script validation regularly"
def at_script_creation(self):
"Setup the script"
self.key = "sys_scripts_validate"
self.desc = "Validates all scripts regularly."
self.interval = 3600 # validate every hour.
self.persistent = True
def at_repeat(self):
"called every hour"
ScriptDB.objects.validate()
class ValidateChannelHandler(Script):
"Update the channelhandler to make sure it's in sync."
def at_script_creation(self):
"Setup the script"
self.key = "sys_channels_validate"
self.desc = "Updates the channel handler"
self.interval = 3700 # validate a little later than ValidateScripts
self.persistent = True
def at_repeat(self):
"called every hour+"
channelhandler.CHANNELHANDLER.update()
class AddCmdSet(Script):
"""
This script permanently assigns a command set
to an object. This is called automatically by the cmdhandler
when an object is assigned a persistent cmdset.
To use, create this script, then assign to the two attributes
'cmdset' and 'add_default' as appropriate:
> from src.utils import create
> script = create.create_script('src.scripts.scripts.AddCmdSet')
> script.db.cmdset = 'game.gamesrc.commands.mycmdset.MyCmdSet'
> script.db.add_default = False
> obj.scripts.add(script)
"""
def at_script_creation(self):
"Setup the script"
if not self.key:
self.key = "add_cmdset"
if not self.desc:
self.desc = "Adds a cmdset to an object."
self.persistent = True
# this needs to be assigned to upon creation.
# It should be a string pointing to the right
# cmdset module and cmdset class name, e.g.
# 'examples.cmdset_redbutton.RedButtonCmdSet'
# self.db.cmdset = <cmdset_path>
# self.db.add_default = <bool>
def at_start(self):
"Get cmdset and assign it."
cmdset = self.db.cmdset
if cmdset:
if self.db.add_default:
self.obj.cmdset.add_default(cmdset)
else:
self.obj.cmdset.add(cmdset)