Reshuffling the Evennia package into the new template paradigm.

This commit is contained in:
Griatch 2015-01-06 14:53:45 +01:00
parent 2846e64833
commit 2b3a32e447
371 changed files with 17250 additions and 304 deletions

15
lib/scripts/__init__.py Normal file
View file

@ -0,0 +1,15 @@
"""
Makes it easier to import by grouping all relevant things already at this
level.
You can henceforth import most things directly from src.scripts
Also, the initiated object manager is available as src.scripts.manager.
"""
# Note - we MUST NOT import src.scripts.scripts here, or
# proxy models will fall under Django migrations.
#from src.scripts.scripts import *
#from src.scripts.models import ScriptDB
#manager = ScriptDB.objects

40
lib/scripts/admin.py Normal file
View file

@ -0,0 +1,40 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from src.typeclasses.admin import AttributeInline, TagInline
from src.scripts.models import ScriptDB
from django.contrib import admin
class ScriptTagInline(TagInline):
model = ScriptDB.db_tags.through
class ScriptAttributeInline(AttributeInline):
model = ScriptDB.db_attributes.through
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']
search_fields = ['^db_key', 'db_typeclass_path']
save_as = True
save_on_top = True
list_select_related = True
raw_id_fields = ('db_obj',)
fieldsets = (
(None, {
'fields': (('db_key', 'db_typeclass_path'), 'db_interval',
'db_repeats', 'db_start_delay', 'db_persistent',
'db_obj')}),
)
inlines = [ScriptTagInline, ScriptAttributeInline]
admin.site.register(ScriptDB, ScriptDBAdmin)

234
lib/scripts/manager.py Normal file
View file

@ -0,0 +1,234 @@
"""
The custom manager for Scripts.
"""
from django.db.models import Q
from src.typeclasses.managers import TypedObjectManager, TypeclassManager
from src.typeclasses.managers import returns_typeclass_list
from src.utils.utils import make_iter
__all__ = ("ScriptManager",)
_GA = object.__getattribute__
VALIDATE_ITERATION = 0
class ScriptDBManager(TypedObjectManager):
"""
This Scriptmanager implements methods for searching
and manipulating Scripts directly from the database.
Evennia-specific search methods (will return Typeclasses or
lists of Typeclasses, whereas Django-general methods will return
Querysets or database objects).
dbref (converter)
get_id (or dbref_search)
get_dbref_range
object_totals
typeclass_search
get_all_scripts_on_obj
get_all_scripts
delete_script
remove_non_persistent
validate
script_search (equivalent to ev.search_script)
copy_script
"""
@returns_typeclass_list
def get_all_scripts_on_obj(self, obj, key=None):
"""
Returns as result all the Scripts related to a particular object.
key can be given as a dbref or name string. If given, only scripts
matching the key on the object will be returned.
"""
if not obj:
return []
player = _GA(_GA(obj, "__class__"), "__name__") == "PlayerDB"
if key:
dbref = self.dbref(key)
if dbref or dbref == 0:
if player:
return self.filter(db_player=obj, id=dbref)
else:
return self.filter(db_obj=obj, id=dbref)
elif player:
return self.filter(db_player=obj, db_key=key)
else:
return self.filter(db_obj=obj, db_key=key)
elif player:
return self.filter(db_player=obj)
else:
return self.filter(db_obj=obj)
@returns_typeclass_list
def get_all_scripts(self, key=None):
"""
Return all scripts, alternative only
scripts with a certain key/dbref
"""
if key:
script = []
dbref = self.dbref(key)
if dbref or dbref == 0:
script = self.dbref_search(dbref)
if not script:
script = self.filter(db_key=key)
return script
return self.all()
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 make_iter(scripts):
script.stop()
def remove_non_persistent(self, obj=None):
"""
This cleans up the script database of all non-persistent
scripts, or only those on obj. It is called every time the server
restarts.
"""
if obj:
to_stop = self.filter(db_obj=obj, db_persistent=False, db_is_active=True)
to_delete = self.filter(db_obj=obj, db_persistent=False, db_is_active=False)
else:
to_stop = self.filter(db_persistent=False, db_is_active=True)
to_delete = self.filter(db_persistent=False, db_is_active=False)
nr_deleted = to_stop.count() + to_delete.count()
for script in to_stop:
script.stop()
for script in to_delete:
script.delete()
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 - This is used during server upstart and can have
three values:
False (no init mode). Called during run.
"reset" - server reboot. Kill non-persistent scripts
"reload" - server reload. Keep non-persistent scripts.
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:
if init_mode == 'reset':
# special mode when server starts or object logs in.
# 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
scripts = self.get_all_scripts()
for script in scripts:
script.is_active = False
elif not scripts:
# normal operation
if dbref and self.dbref(dbref, reqhash=False):
scripts = self.get_id(dbref)
elif obj:
#print "calling get_all_scripts_on_obj", obj, key, VALIDATE_ITERATION
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 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:
#print "validating %s (%i) (init_mode=%s)" % (script.key, id(script), init_mode)
if script.is_valid():
nr_started += script.start(force_restart=init_mode)
#print "back from start. nr_started=", nr_started
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 or dbref == 0:
# this is a dbref, try to find the script directly
dbref_match = self.dbref_search(dbref)
if dbref_match and not ((obj and obj != dbref_match.obj)
or (only_timed and dbref_match.interval)):
return [dbref_match]
# not a dbref; normal search
obj_restriction = obj and Q(db_obj=obj) or Q()
timed_restriction = only_timed and Q(interval__gt=0) or Q()
scripts = self.filter(timed_restriction & obj_restriction & Q(db_key__iexact=ostring))
return scripts
def copy_script(self, original_script, new_key=None, new_obj=None, new_locks=None):
"""
Make an identical copy of the original_script
"""
typeclass = original_script.typeclass_path
new_key = new_key if new_key is not None else original_script.key
new_obj = new_obj if new_obj is not None else original_script.obj
new_locks = new_locks if new_locks is not None else original_script.db_lock_storage
from src.utils import create
new_script = create.create_script(typeclass, key=new_key, obj=new_obj,
locks=new_locks, autostart=True)
return new_script
class ScriptManager(ScriptDBManager, TypeclassManager):
pass

View file

@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('objects', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('typeclasses', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ScriptDB',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('db_key', models.CharField(max_length=255, verbose_name=b'key', db_index=True)),
('db_typeclass_path', models.CharField(help_text=b"this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name=b'typeclass')),
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
('db_lock_storage', models.TextField(help_text=b"locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name=b'locks', blank=True)),
('db_desc', models.CharField(max_length=255, verbose_name=b'desc', blank=True)),
('db_interval', models.IntegerField(default=-1, help_text=b'how often to repeat script, in seconds. -1 means off.', verbose_name=b'interval')),
('db_start_delay', models.BooleanField(default=False, help_text=b'pause interval seconds before starting.', verbose_name=b'start delay')),
('db_repeats', models.IntegerField(default=0, help_text=b'0 means off.', verbose_name=b'number of repeats')),
('db_persistent', models.BooleanField(default=False, verbose_name=b'survive server reboot')),
('db_is_active', models.BooleanField(default=False, verbose_name=b'script active')),
('db_attributes', models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute', null=True)),
('db_obj', models.ForeignKey(blank=True, to='objects.ObjectDB', help_text=b'the object to store this script on, if not a global script.', null=True, verbose_name=b'scripted object')),
('db_player', models.ForeignKey(blank=True, to=settings.AUTH_USER_MODEL, help_text=b'the player to store this script on (should not be set if obj is set)', null=True, verbose_name=b'scripted player')),
('db_tags', models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag', null=True)),
],
options={
'verbose_name': 'Script',
},
bases=(models.Model,),
),
]

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

176
lib/scripts/models.py Normal file
View file

@ -0,0 +1,176 @@
"""
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 django.core.exceptions import ObjectDoesNotExist
from src.typeclasses.models import TypedObject
from src.scripts.manager import ScriptDBManager
from src.utils.utils import dbref, to_str
__all__ = ("ScriptDB",)
_GA = object.__getattribute__
_SA = object.__setattr__
#------------------------------------------------------------
#
# 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
player - the player the script is linked to (exclusive with obj)
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 database 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('desc', max_length=255, blank=True)
# A reference to the database object affected by this Script, if any.
db_obj = models.ForeignKey("objects.ObjectDB", null=True, blank=True, verbose_name='scripted object',
help_text='the object to store this script on, if not a global script.')
db_player = models.ForeignKey("players.PlayerDB", null=True, blank=True, verbose_name="scripted player",
help_text='the player to store this script on (should not be set if obj is set)')
# how often to run Script (secs). -1 means there is no timer
db_interval = models.IntegerField('interval', default=-1, help_text='how often to repeat script, in seconds. -1 means off.')
# start script right away or wait interval seconds first
db_start_delay = models.BooleanField('start delay', default=False, help_text='pause interval seconds before starting.')
# how many times this script is to be repeated, if interval!=0.
db_repeats = models.IntegerField('number of repeats', default=0, help_text='0 means off.')
# defines if this script should survive a reboot or not
db_persistent = models.BooleanField('survive server reboot', default=False)
# defines if this script has already been started in this session
db_is_active = models.BooleanField('script active', default=False)
# Database manager
objects = ScriptDBManager()
class Meta:
"Define Django meta options"
verbose_name = "Script"
#
#
# ScriptDB class properties
#
#
# obj property
def __get_obj(self):
"""
property wrapper that homogenizes access to either
the db_player or db_obj field, using the same obj
property name
"""
obj = _GA(self, "db_player")
if not obj:
obj = _GA(self, "db_obj")
return obj
def __set_obj(self, value):
"""
Set player or obj to their right database field. If
a dbref is given, assume ObjectDB.
"""
try:
value = _GA(value, "dbobj")
except AttributeError:
pass
if isinstance(value, (basestring, int)):
from src.objects.models import ObjectDB
value = to_str(value, force_string=True)
if (value.isdigit() or value.startswith("#")):
dbid = dbref(value, reqhash=False)
if dbid:
try:
value = ObjectDB.objects.get(id=dbid)
except ObjectDoesNotExist:
# maybe it is just a name that happens to look like a dbid
pass
if value.__class__.__name__ == "PlayerDB":
fname = "db_player"
_SA(self, fname, value)
else:
fname = "db_obj"
_SA(self, fname, value)
# saving the field
_GA(self, "save")(update_fields=[fname])
obj = property(__get_obj, __set_obj)
object = property(__get_obj, __set_obj)
# 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()
#
# delete_iter = 0
# def delete(self):
# "Delete script"
# if self.delete_iter > 0:
# return
# self.delete_iter += 1
# _GA(self, "attributes").clear()
# super(ScriptDB, self).delete()

View file

@ -0,0 +1,126 @@
"""
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
from django.utils.translation import ugettext as _
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
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 > 0:
interval = script.interval
if script.repeats:
repeats = script.repeats
try:
next_repeat = script.time_until_next_repeat()
except:
next_repeat = "?"
string += _("\n '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s") % \
{"key": script.key, "next_repeat": next_repeat,
"interval": interval, "repeats": repeats, "desc": script.desc}
return string.strip()
def add(self, scriptclass, key=None, autostart=True):
"""
Add an script to this object.
scriptclass - either a class object
inheriting from Script, an instantiated script 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
"""
if self.obj.__class__.__name__ == "PlayerDB":
# we add to a Player, not an Object
script = create.create_script(scriptclass, key=key, player=self.obj,
autostart=autostart)
else:
# the normal - adding to an Object
script = create.create_script(scriptclass, key=key, obj=self.obj,
autostart=autostart)
if not script:
logger.log_errmsg("Script %s could not be created and/or started." % scriptclass)
return False
return True
def start(self, scriptid):
"""
Find an already added script and force-start it
"""
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
num = 0
for script in scripts:
num += script.start()
return num
def get(self, scriptid):
"""
Return one or all scripts on this object matching scriptid. Will return
a list.
"""
return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
def delete(self, scriptid=None):
"""
Forcibly delete a script from this object.
scriptid can be a script key or the path to a script (in the
latter case all scripts with this path will be deleted!)
If no scriptid is set, delete all scripts on the object.
"""
delscripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
if not delscripts:
delscripts = [script for script in ScriptDB.objects.get_all_scripts_on_obj(self.obj) if script.path == scriptid]
num = 0
for script in delscripts:
num += script.stop()
return num
def stop(self, scriptid=None):
"""
Alias for delete. scriptid can be a script key or a script path string.
"""
return self.delete(scriptid)
def all(self, scriptid=None):
"""
Get all scripts stored in the handler, alternatively all matching a key.
"""
return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=scriptid)
def validate(self, init_mode=False):
"""
Runs a validation on this object's scripts only.
This should be called regularly to crank the wheels.
"""
ScriptDB.objects.validate(obj=self.obj, init_mode=init_mode)

610
lib/scripts/scripts.py Normal file
View file

@ -0,0 +1,610 @@
"""
This module contains the base Script class that all
scripts are inheriting from.
It also defines a few common scripts.
"""
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.task import LoopingCall
from django.conf import settings
from src.typeclasses.models import TypeclassBase
from django.utils.translation import ugettext as _
from src.scripts.models import ScriptDB
from src.scripts.manager import ScriptManager
from src.comms import channelhandler
from src.utils import logger
__all__ = ["Script", "DoNothing", "CheckSessions",
"ValidateScripts", "ValidateChannelHandler"]
_GA = object.__getattribute__
_SESSIONS = None
class ExtendedLoopingCall(LoopingCall):
"""
LoopingCall that can start at a delay different
than self.interval.
"""
start_delay = None
callcount = 0
def start(self, interval, now=True, start_delay=None, count_start=0):
"""
Start running function every interval seconds.
This overloads the LoopingCall default by offering
the start_delay keyword and ability to repeat.
start_delay: The number of seconds before starting.
If None, wait interval seconds. Only
valid is now is False.
repeat_start: the task will track how many times it has run.
this will change where it starts counting from.
Note that as opposed to Twisted's inbuild
counter, this will count also if force_repeat()
was called (so it will not just count the number
of interval seconds since start).
"""
assert not self.running, ("Tried to start an already running "
"ExtendedLoopingCall.")
if interval < 0:
raise ValueError, "interval must be >= 0"
self.running = True
d = self.deferred = Deferred()
self.starttime = self.clock.seconds()
self._expectNextCallAt = self.starttime
self.interval = interval
self._runAtStart = now
self.callcount = max(0, count_start)
if now:
self()
else:
if start_delay is not None and start_delay >= 0:
# we set start_delay after the _reshedule call to make
# next_call_time() find it until next reshedule.
self.interval = start_delay
self._reschedule()
self.interval = interval
self.start_delay = start_delay
else:
self._reschedule()
return d
def __call__(self):
"tick one step"
self.callcount += 1
super(ExtendedLoopingCall, self).__call__()
def _reschedule(self):
"""
Handle call rescheduling including
nulling start_delay and stopping if
number of repeats is reached.
"""
self.start_delay = None
super(ExtendedLoopingCall, self)._reschedule()
def force_repeat(self):
"Force-fire the callback"
assert self.running, ("Tried to fire an ExtendedLoopingCall "
"that was not running.")
if self.call is not None:
self.call.cancel()
self._expectNextCallAt = self.clock.seconds()
self()
def next_call_time(self):
"""
Return the time in seconds until the next call. This takes
start_delay into account.
"""
if self.running:
currentTime = self.clock.seconds()
return self._expectNextCallAt - currentTime
return None
#
# Base script, inherit from Script below instead.
#
class ScriptBase(ScriptDB):
"""
Base class for scripts. Don't inherit from this, inherit
from the class 'Script' instead.
"""
__metaclass__ = TypeclassBase
objects = ScriptManager()
def __eq__(self, other):
"""
This has to be located at this level, having it in the
parent doesn't work.
"""
try:
return other.dbid == self.dbid
except Exception:
return False
def _start_task(self):
"start task runner"
self.ndb._task = ExtendedLoopingCall(self._step_task)
if self.db._paused_time:
# the script was paused; restarting
callcount = self.db._paused_callcount or 0
self.ndb._task.start(self.db_interval,
now=False,
start_delay=self.db._paused_time,
count_start=callcount)
del self.db._paused_time
del self.db._paused_repeats
else:
# starting script anew
self.ndb._task.start(self.db_interval,
now=not self.db_start_delay)
def _stop_task(self):
"stop task runner"
task = self.ndb._task
if task and task.running:
task.stop()
def _step_errback(self, e):
"callback for runner errors"
cname = self.__class__.__name__
estring = _("Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'.") % \
{"key": self.key, "dbid": self.dbid, "cname": cname,
"err": e.getErrorMessage()}
try:
self.db_obj.msg(estring)
except Exception:
pass
logger.log_errmsg(estring)
def _step_callback(self):
"step task runner. No try..except needed due to defer wrap."
if not self.is_valid():
self.stop()
return
# call hook
self.at_repeat()
# check repeats
callcount = self.ndb._task.callcount
maxcount = self.db_repeats
if maxcount > 0 and maxcount <= callcount:
#print "stopping script!"
self.stop()
def _step_task(self):
"Step task. This groups error handling."
try:
return maybeDeferred(self._step_callback).addErrback(self._step_errback)
except Exception:
logger.log_trace()
# Public methods
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.
"""
task = self.ndb._task
if task:
try:
return int(round(task.next_call_time()))
except TypeError:
pass
return None
def remaining_repeats(self):
"Get the number of returning repeats. Returns None if unlimited repeats."
task = self.ndb._task
if task:
return max(0, self.db_repeats - task.callcount)
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.
returns 0 or 1 to indicated the script has been started or not.
Used in counting.
"""
if self.is_active and not force_restart:
# script already runs and should not be restarted.
return 0
obj = self.obj
if obj:
# check so the scripted object is valid and initalized
try:
obj.cmdset
except AttributeError:
# this means the object is not initialized.
logger.log_trace()
self.is_active = False
return 0
# try to restart a paused script
if self.unpause():
return 1
# start the script from scratch
self.is_active = True
try:
self.at_start()
except Exception:
logger.log_trace()
if self.db_interval > 0:
self._start_task()
return 1
def stop(self, kill=False):
"""
Called to stop the script from running.
This also deletes the script.
kill - don't call finishing hooks.
"""
#print "stopping script %s" % self.key
#import pdb
#pdb.set_trace()
if not kill:
try:
self.at_stop()
except Exception:
logger.log_trace()
self._stop_task()
try:
self.delete()
except AssertionError:
logger.log_trace()
return 0
return 1
def pause(self):
"""
This stops a running script and stores its active state.
It WILL NOT call that at_stop() hook.
"""
if not self.db._paused_time:
# only allow pause if not already paused
task = self.ndb._task
if task:
self.db._paused_time = task.next_call_time()
self.db._paused_callcount = task.callcount
self._stop_task()
self.is_active = False
def unpause(self):
"""
Restart a paused script. This WILL call the at_start() hook.
"""
if self.db._paused_time:
# only unpause if previously paused
self.is_active = True
try:
self.at_start()
except Exception:
logger.log_trace()
self._start_task()
return True
def force_repeat(self):
"""
Fire a premature triggering of the script callback. This
will reset the timer and count down repeats as if the script
had fired normally.
"""
task = self.ndb._task
if task:
task.force_repeat()
# hooks
def at_script_creation(self):
"placeholder"
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
def at_init(self):
"called when typeclass re-caches. Usually not used for scripts."
pass
#
# Base Script - inherit from this
#
class Script(ScriptBase):
"""
This is the base TypeClass for all Scripts. Scripts describe events,
timers and states in game, they can have a time component or describe
a state that changes under certain conditions.
Script API:
* Available properties (only available on initiated Typeclass objects)
key (string) - name of object
name (string)- same as key
aliases (list of strings) - aliases to the object. Will be saved to
database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
desc (string) - optional description of script, shown in listings
obj (Object) - optional object that this script is connected to
and acts on (set automatically
by obj.scripts.add())
interval (int) - how often script should run, in seconds.
<=0 turns off ticker
start_delay (bool) - if the script should start repeating right
away or wait self.interval seconds
repeats (int) - how many times the script should repeat before
stopping. <=0 means infinite repeats
persistent (bool) - if script should survive a server shutdown or not
is_active (bool) - if script is currently running
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this
self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not
create a database entry when storing data
* Helper methods
start() - start script (this usually happens automatically at creation
and obj.script.add() etc)
stop() - stop script, and delete it
pause() - put the script on hold, until unpause() is called. If script
is persistent, the pause state will survive a shutdown.
unpause() - restart a previously paused script. The script will
continue as if it was never paused.
force_repeat() - force-step the script, regardless of how much remains
until next step. This counts like a normal firing in all ways.
time_until_next_repeat() - if a timed script (interval>0), returns
time until next tick
remaining_repeats() - number of repeats remaining, if limited
* Hook methods
at_script_creation() - called only once, when an object of this
class is first created.
is_valid() - is called to check if the script is valid to be running
at the current time. If is_valid() returns False, the
running script is stopped and removed from the game. You
can use this to check state changes (i.e. an script
tracking some combat stats at regular intervals is only
valid to run while there is actual combat going on).
at_start() - Called every time the script is started, which for
persistent scripts is at least once every server start.
Note that this is unaffected by self.delay_start, which
only delays the first call to at_repeat(). It will also
be called after a pause, to allow for setting up the script.
at_repeat() - Called every self.interval seconds. It will be called
immediately upon launch unless self.delay_start is True,
which will delay the first call of this method by
self.interval seconds. If self.interval<=0, this method
will never be called.
at_stop() - Called as the script object is stopped and is about to
be removed from the game, e.g. because is_valid()
returned False or self.stop() was called manually.
at_server_reload() - Called when server reloads. Can be used to save
temporary variables you want should survive a reload.
at_server_shutdown() - called at a full server shutdown.
"""
def at_first_save(self):
"""
This is called after very first time this object is saved.
Generally, you don't need to overload this, but only the hooks
called by this method.
"""
self.at_script_creation()
if hasattr(self, "_createdict"):
# this will only be set if the utils.create_script
# function was used to create the object. We want
# the create call's kwargs to override the values
# set by hooks.
cdict = self._createdict
updates = []
if not cdict.get("key"):
if not self.db_key:
self.db_key = "#%i" % self.dbid
updates.append("db_key")
elif self.db_key != cdict["key"]:
self.db_key = cdict["key"]
updates.append("db_key")
if cdict.get("interval") and self.interval != cdict["interval"]:
self.db_interval = cdict["interval"]
updates.append("db_interval")
if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
self.db_start_delay = cdict["start_delay"]
updates.append("db_start_delay")
if cdict.get("repeats") and self.repeats != cdict["repeats"]:
self.db_repeats = cdict["repeats"]
updates.append("db_repeats")
if cdict.get("persistent") and self.persistent != cdict["persistent"]:
self.db_persistent = cdict["persistent"]
updates.append("db_persistent")
if updates:
self.save(update_fields=updates)
if not cdict.get("autostart"):
# don't auto-start the script
return
# auto-start script (default)
self.start()
def at_script_creation(self):
"""
Only called once, by the create function.
"""
pass
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 not self._is_deleted
def at_start(self):
"""
Called whenever the script is started, which for persistent
scripts is at least once every server start. It will also be called
when starting again after a pause (such as after a server reload)
"""
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 it runs out of iterations)
"""
pass
def at_server_reload(self):
"""
This hook is called whenever the server is shutting down for
restart/reboot. If you want to, for example, save non-persistent
properties across a restart, this is the place to do it.
"""
pass
def at_server_shutdown(self):
"""
This hook is called whenever the server is shutting down fully
(i.e. not for a restart).
"""
pass
# Some useful default Script types used by Evennia.
class DoNothing(Script):
"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 is an empty placeholder script.")
class Store(Script):
"Simple storage script"
def at_script_creation(self):
"Setup the script"
self.key = "sys_storage"
self.desc = _("This is a generic storage container.")
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"
global _SESSIONS
if not _SESSIONS:
from src.server.sessionhandler import SESSIONS as _SESSIONS
#print "session check!"
#print "ValidateSessions run"
_SESSIONS.validate_sessions()
_FLUSH_CACHE = None
_IDMAPPER_CACHE_MAX_MEMORY = settings.IDMAPPER_CACHE_MAXSIZE
class ValidateIdmapperCache(Script):
"""
Check memory use of idmapper cache
"""
def at_script_creation(self):
self.key = "sys_cache_validate"
self.desc = _("Restrains size of idmapper cache.")
self.interval = 61 * 5 # staggered compared to session check
self.persistent = True
def at_repeat(self):
"Called every ~5 mins"
global _FLUSH_CACHE
if not _FLUSH_CACHE:
from src.utils.idmapper.base import conditional_flush as _FLUSH_CACHE
_FLUSH_CACHE(_IDMAPPER_CACHE_MAX_MEMORY)
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"
#print "ValidateScripts run."
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+"
#print "ValidateChannelHandler run."
channelhandler.CHANNELHANDLER.update()

View file

@ -0,0 +1,329 @@
"""
TickerHandler
This implements an efficient Ticker which uses a subscription
model to 'tick' subscribed objects at regular intervals.
The ticker mechanism is used by importing and accessing
the instantiated TICKER_HANDLER instance in this module. This
instance is run by the server; it will save its status across
server reloads and be started automaticall on boot.
Example:
from src.scripts.tickerhandler import TICKER_HANDLER
# tick myobj every 15 seconds
TICKER_HANDLER.add(myobj, 15)
The handler will by default try to call a hook "at_tick()"
on the subscribing object. The hook's name can be changed
if the "hook_key" keyword is given to the add() method (only
one such alternate name per interval though). The
handler will transparently set up and add new timers behind
the scenes to tick at given intervals, using a TickerPool.
To remove:
TICKER_HANDLER.remove(myobj, 15)
The interval must be given since a single object can be subcribed
to many different tickers at the same time.
The TickerHandler's functionality can be overloaded by modifying the
Ticker class and then changing TickerPool and TickerHandler to use the
custom classes
class MyTicker(Ticker):
# [doing custom stuff]
class MyTickerPool(TickerPool):
ticker_class = MyTicker
class MyTickerHandler(TickerHandler):
ticker_pool_class = MyTickerPool
If one wants to duplicate TICKER_HANDLER's auto-saving feature in
a custom handler one can make a custom AT_STARTSTOP_MODULE entry to
call the handler's save() and restore() methods when the server reboots.
"""
from twisted.internet.defer import inlineCallbacks
from src.scripts.scripts import ExtendedLoopingCall
from src.server.models import ServerConfig
from src.utils.logger import log_trace
from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj
_GA = object.__getattribute__
_SA = object.__setattr__
class Ticker(object):
"""
Represents a repeatedly running task that calls
hooks repeatedly. Overload _callback to change the
way it operates.
"""
@inlineCallbacks
def _callback(self):
"""
This will be called repeatedly every self.interval seconds.
self.subscriptions contain tuples of (obj, args, kwargs) for
each subscribing object.
If overloading, this callback is expected to handle all
subscriptions when it is triggered. It should not return
anything and should not traceback on poorly designed hooks.
The callback should ideally work under @inlineCallbacks so it can yield
appropriately.
"""
for key, (obj, args, kwargs) in self.subscriptions.items():
hook_key = yield kwargs.get("hook_key", "at_tick")
if not obj:
# object was deleted between calls
self.validate()
continue
try:
yield _GA(obj, hook_key)(*args, **kwargs)
except Exception:
log_trace()
def __init__(self, interval):
"""
Set up the ticker
"""
self.interval = interval
self.subscriptions = {}
# set up a twisted asynchronous repeat call
self.task = ExtendedLoopingCall(self._callback)
def validate(self, start_delay=None):
"""
Start/stop the task depending on how many
subscribers we have using it.
"""
subs = self.subscriptions
if None in subs.values():
# clean out objects that may have been deleted
subs = dict((store_key, obj) for store_key, obj in subs if obj)
self.subscriptions = subs
if self.task.running:
if not subs:
self.task.stop()
elif subs:
#print "starting with start_delay=", start_delay
self.task.start(self.interval, now=False, start_delay=start_delay)
def add(self, store_key, obj, *args, **kwargs):
"""
Sign up a subscriber to this ticker. If kwargs contains
a keyword _start_delay, this will be used to delay the start
of the trigger instead of interval.
"""
start_delay = kwargs.pop("_start_delay", None)
self.subscriptions[store_key] = (obj, args, kwargs)
self.validate(start_delay=start_delay)
def remove(self, store_key):
"""
Unsubscribe object from this ticker
"""
self.subscriptions.pop(store_key, False)
self.validate()
def stop(self):
"""
Kill the Task, regardless of subscriptions
"""
self.subscriptions = {}
self.validate()
class TickerPool(object):
"""
This maintains a pool of Twisted LoopingCall tasks
for calling subscribed objects at given times.
"""
ticker_class = Ticker
def __init__(self):
"Initialize the pool"
self.tickers = {}
def add(self, store_key, obj, interval, *args, **kwargs):
"""
Add new ticker subscriber
"""
if interval not in self.tickers:
self.tickers[interval] = self.ticker_class(interval)
self.tickers[interval].add(store_key, obj, *args, **kwargs)
def remove(self, store_key, interval):
"""
Remove subscription from pool
"""
if interval in self.tickers:
self.tickers[interval].remove(store_key)
def stop(self, interval=None):
"""
Stop all scripts in pool. This is done at server reload since
restoring the pool will automatically re-populate the pool.
If interval is given, only stop tickers with that interval.
"""
if interval and interval in self.tickers:
self.tickers[interval].stop()
else:
for ticker in self.tickers.values():
ticker.stop()
class TickerHandler(object):
"""
The Tickerhandler maintains a pool of tasks for subscribing
objects to various tick rates. The pool maintains creation
instructions and and re-applies them at a server restart.
"""
ticker_pool_class = TickerPool
def __init__(self, save_name="ticker_storage"):
"""
Initialize handler
"""
self.ticker_storage = {}
self.save_name = save_name
self.ticker_pool = self.ticker_pool_class()
def _store_key(self, obj, interval, idstring=""):
"""
Tries to create a store_key for the object.
Returns a tuple (isdb, store_key) where isdb
is a boolean True if obj was a database object,
False otherwise.
"""
if hasattr(obj, "db_key"):
# create a store_key using the database representation
objkey = pack_dbobj(obj)
isdb = True
else:
# non-db object, look for a property "key" on it, otherwise
# use its memory location.
try:
objkey = _GA(obj, "key")
except AttributeError:
objkey = id(obj)
isdb = False
# return sidb and store_key
return isdb, (objkey, interval, idstring)
def save(self):
"""
Save ticker_storage as a serialized string into a temporary
ServerConf field. Whereas saving is done on the fly, if called by
server when it shuts down, the current timer of each ticker will be
saved so it can start over from that point.
"""
if self.ticker_storage:
start_delays = dict((interval, ticker.task.next_call_time())
for interval, ticker in self.ticker_pool.tickers.items())
# update the timers for the tickers
#for (obj, interval, idstring), (args, kwargs) in self.ticker_storage.items():
for store_key, (args, kwargs) in self.ticker_storage.items():
interval = store_key[1]
# this is a mutable, so it's updated in-place in ticker_storage
kwargs["_start_delay"] = start_delays.get(interval, None)
ServerConfig.objects.conf(key=self.save_name,
value=dbserialize(self.ticker_storage))
else:
ServerConfig.objects.conf(key=self.save_name, delete=True)
def restore(self):
"""
Restore ticker_storage from database and re-initialize the handler from storage. This is triggered by the server at restart.
"""
# load stored command instructions and use them to re-initialize handler
ticker_storage = ServerConfig.objects.conf(key=self.save_name)
if ticker_storage:
self.ticker_storage = dbunserialize(ticker_storage)
#print "restore:", self.ticker_storage
for store_key, (args, kwargs) in self.ticker_storage.items():
if len(store_key) == 2:
# old form of store_key - update it
store_key = (store_key[0], store_key[1], "")
obj, interval, idstring = store_key
obj = unpack_dbobj(obj)
_, store_key = self._store_key(obj, interval, idstring)
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs)
def add(self, obj, interval, idstring="", *args, **kwargs):
"""
Add object to tickerhandler. The object must have an at_tick
method. This will be called every interval seconds until the
object is unsubscribed from the ticker.
"""
isdb, store_key = self._store_key(obj, interval, idstring)
if isdb:
self.ticker_storage[store_key] = (args, kwargs)
self.save()
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs)
def remove(self, obj, interval=None, idstring=""):
"""
Remove object from ticker, or only this object ticking
at a given interval.
"""
if interval:
isdb, store_key = self._store_key(obj, interval, idstring)
if isdb:
self.ticker_storage.pop(store_key, None)
self.save()
self.ticker_pool.remove(store_key, interval)
else:
# remove all objects with any intervals
intervals = self.ticker_pool.tickers.keys()
should_save = False
for interval in intervals:
isdb, store_key = self._store_key(obj, interval, idstring)
if isdb:
self.ticker_storage.pop(store_key, None)
should_save = True
self.ticker_pool.remove(store_key, interval)
if should_save:
self.save()
def clear(self, interval=None):
"""
Stop/remove all tickers from handler, or the ones
with a given interval. This is the only supported
way to kill tickers for non-db objects. If interval
is given, only stop tickers with this interval.
"""
self.ticker_pool.stop(interval)
if interval:
self.ticker_storage = dict((store_key, store_key) for store_key in self.ticker_storage if store_key[1] != interval)
else:
self.ticker_storage = {}
self.save()
def all(self, interval=None):
"""
Get the subsciptions for a given interval. If interval
is not given, return a dictionary with lists for every
interval in the tickerhandler.
"""
if interval is None:
# return dict of all, ordered by interval
return dict((interval, ticker.subscriptions.values())
for interval, ticker in self.ticker_pool.tickers.items())
else:
# get individual interval
ticker = self.ticker_pool.tickers.get(interval, None)
if ticker:
return ticker.subscriptions.values()
# main tickerhandler
TICKER_HANDLER = TickerHandler()