mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
912 lines
31 KiB
Python
912 lines
31 KiB
Python
"""
|
|
This module defines Scripts, out-of-character entities that can store
|
|
data both on themselves and on other objects while also having the
|
|
ability to run timers.
|
|
|
|
"""
|
|
|
|
from django.utils.translation import gettext as _
|
|
from evennia.scripts.manager import ScriptManager
|
|
from evennia.scripts.models import ScriptDB
|
|
from evennia.typeclasses.models import TypeclassBase
|
|
from evennia.utils import create, logger
|
|
from twisted.internet.defer import Deferred, maybeDeferred
|
|
from twisted.internet.task import LoopingCall
|
|
|
|
__all__ = ["DefaultScript", "DoNothing", "Store"]
|
|
|
|
|
|
class ExtendedLoopingCall(LoopingCall):
|
|
"""
|
|
Custom child of LoopingCall that can start at a delay different than
|
|
`self.interval` and self.count=0. This allows it to support pausing
|
|
by resuming at a later period.
|
|
|
|
"""
|
|
|
|
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.
|
|
|
|
Args:
|
|
interval (int): Repeat interval in seconds.
|
|
now (bool, optional): Whether to start immediately or after
|
|
`start_delay` seconds.
|
|
start_delay (int, optional): This only applies is `now=False`. It gives
|
|
number of seconds to wait before starting. If `None`, use
|
|
`interval` as this value instead. Internally, this is used as a
|
|
way to start with a variable start time after a pause.
|
|
count_start (int): Number of repeats to start at. The count
|
|
goes up every time the system repeats. This is used to
|
|
implement something repeating `N` number of times etc.
|
|
|
|
Raises:
|
|
AssertError: if trying to start a task which is already running.
|
|
ValueError: If interval is set to an invalid value < 0.
|
|
|
|
Notes:
|
|
As opposed to Twisted's inbuilt count mechanism, this
|
|
system will count also if force_repeat() was called rather
|
|
than just the number of `interval` seconds since the start.
|
|
This allows us to force-step through a limited number of
|
|
steps if we want.
|
|
|
|
"""
|
|
assert not self.running, "Tried to start an already running ExtendedLoopingCall."
|
|
if interval < 0:
|
|
raise ValueError("interval must be >= 0")
|
|
self.running = True
|
|
deferred = self._deferred = Deferred()
|
|
self.starttime = self.clock.seconds()
|
|
self.interval = interval
|
|
self._runAtStart = now
|
|
self.callcount = max(0, count_start)
|
|
self.start_delay = start_delay if start_delay is None else max(0, start_delay)
|
|
|
|
if now:
|
|
# run immediately
|
|
self()
|
|
elif start_delay is not None and start_delay >= 0:
|
|
# start after some time: for this to work we need to
|
|
# trick _scheduleFrom by temporarily setting a different
|
|
# self.interval for it to check.
|
|
real_interval, self.interval = self.interval, start_delay
|
|
self._scheduleFrom(self.starttime)
|
|
# re-set the actual interval (this will be picked up
|
|
# next time it runs
|
|
self.interval = real_interval
|
|
else:
|
|
self._scheduleFrom(self.starttime)
|
|
return deferred
|
|
|
|
def __call__(self):
|
|
"""
|
|
Tick one step. We update callcount (tracks number of calls) as
|
|
well as null start_delay (needed in order to correctly
|
|
estimate next_call_time at all times).
|
|
|
|
"""
|
|
self.callcount += 1
|
|
if self.start_delay:
|
|
self.start_delay = None
|
|
self.starttime = self.clock.seconds()
|
|
if self._deferred:
|
|
LoopingCall.__call__(self)
|
|
|
|
def force_repeat(self):
|
|
"""
|
|
Force-fire the callback
|
|
|
|
Raises:
|
|
AssertionError: When trying to force a task that is not
|
|
running.
|
|
|
|
"""
|
|
assert self.running, "Tried to fire an ExtendedLoopingCall that was not running."
|
|
self.call.cancel()
|
|
self.call = None
|
|
self.starttime = self.clock.seconds()
|
|
self()
|
|
|
|
def next_call_time(self):
|
|
"""
|
|
Get the next call time. This also takes the eventual effect
|
|
of start_delay into account.
|
|
|
|
Returns:
|
|
int or None: The time in seconds until the next call. This
|
|
takes `start_delay` into account. Returns `None` if
|
|
the task is not running.
|
|
|
|
"""
|
|
if self.running and self.interval > 0:
|
|
total_runtime = self.clock.seconds() - self.starttime
|
|
interval = self.start_delay or self.interval
|
|
return max(0, interval - (total_runtime % self.interval))
|
|
|
|
|
|
class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|
"""
|
|
Base class for scripts. Don't inherit from this, inherit from the
|
|
class `DefaultScript` below instead.
|
|
|
|
This handles the timer-component of the Script.
|
|
|
|
"""
|
|
|
|
objects = ScriptManager()
|
|
|
|
def __str__(self):
|
|
return "<{cls} {key}>".format(cls=self.__class__.__name__, key=self.key)
|
|
|
|
def __repr__(self):
|
|
return str(self)
|
|
|
|
def at_idmapper_flush(self):
|
|
"""
|
|
If we're flushing this object, make sure the LoopingCall is gone too.
|
|
"""
|
|
ret = super().at_idmapper_flush()
|
|
if ret and self.ndb._task:
|
|
self.ndb._pause_task(auto_pause=True)
|
|
# TODO - restart anew ?
|
|
return ret
|
|
|
|
def _start_task(
|
|
self,
|
|
interval=None,
|
|
start_delay=None,
|
|
repeats=None,
|
|
force_restart=False,
|
|
auto_unpause=False,
|
|
**kwargs,
|
|
):
|
|
"""
|
|
Start/Unpause task runner, optionally with new values. If given, this will
|
|
update the Script's fields.
|
|
|
|
Keyword Args:
|
|
interval (int): How often to tick the task, in seconds. If this is <= 0,
|
|
no task will start and properties will not be updated on the Script.
|
|
start_delay (int): If the start should be delayed.
|
|
repeats (int): How many repeats. 0 for infinite repeats.
|
|
force_restart (bool): If set, always create a new task running even if an
|
|
old one already was running. Otherwise this will only happen if
|
|
new script properties were passed.
|
|
auto_unpause (bool): This is an automatic unpaused (used e.g by Evennia after
|
|
a reload) and should not un-pause manually paused Script timers.
|
|
Note:
|
|
If setting the `start-delay` of a *paused* Script, the Script will
|
|
restart exactly after that new start-delay, ignoring the time it
|
|
was paused at. If only changing the `interval`, the Script will
|
|
come out of pause comparing the time it spent in the *old* interval
|
|
with the *new* interval in order to determine when next to fire.
|
|
|
|
Examples:
|
|
- Script previously had an interval of 10s and was paused 5s into that interval.
|
|
Script is now restarted with a 20s interval. It will next fire after 15s.
|
|
- Same Script is restarted with a 3s interval. It will fire immediately.
|
|
|
|
"""
|
|
if self.pk is None:
|
|
# script object already deleted from db - don't start a new timer
|
|
raise ScriptDB.DoesNotExist
|
|
|
|
# handle setting/updating fields
|
|
update_fields = []
|
|
old_interval = self.db_interval
|
|
if interval is not None:
|
|
self.db_interval = interval
|
|
update_fields.append("db_interval")
|
|
if start_delay is not None:
|
|
# note that for historical reasons, the start_delay is a boolean field, not an int; the
|
|
# actual value is only used with the task.
|
|
self.db_start_delay = bool(start_delay)
|
|
update_fields.append("db_start_delay")
|
|
if repeats is not None:
|
|
self.db_repeats = repeats
|
|
update_fields.append("db_repeats")
|
|
|
|
# validate interval
|
|
if self.db_interval and self.db_interval > 0:
|
|
if not self.is_active:
|
|
self.db_is_active = True
|
|
update_fields.append("db_is_active")
|
|
else:
|
|
# no point in starting a task with no interval.
|
|
return
|
|
|
|
restart = bool(update_fields) or force_restart
|
|
self.save(update_fields=update_fields)
|
|
|
|
if self.ndb._task and self.ndb._task.running:
|
|
if restart:
|
|
# a change needed/forced; stop/remove old task
|
|
self._stop_task()
|
|
else:
|
|
# task alreaady running and no changes needed
|
|
return
|
|
|
|
if not self.ndb._task:
|
|
# we should have a fresh task after this point
|
|
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
|
|
|
self._unpause_task(
|
|
interval=interval,
|
|
start_delay=start_delay,
|
|
auto_unpause=auto_unpause,
|
|
old_interval=old_interval,
|
|
)
|
|
|
|
if not self.ndb._task.running:
|
|
# if not unpausing started it, start script anew with the new values
|
|
self.ndb._task.start(
|
|
self.db_interval, now=not self.db_start_delay, start_delay=start_delay
|
|
)
|
|
|
|
self.at_start(**kwargs)
|
|
|
|
def _pause_task(self, auto_pause=False, **kwargs):
|
|
"""
|
|
Pause task where it is, saving the current status.
|
|
|
|
Args:
|
|
auto_pause (str):
|
|
|
|
"""
|
|
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.db._manually_paused = not auto_pause
|
|
if task.running:
|
|
task.stop()
|
|
self.ndb._task = None
|
|
|
|
self.at_pause(auto_pause=auto_pause, **kwargs)
|
|
|
|
def _unpause_task(
|
|
self, interval=None, start_delay=None, auto_unpause=False, old_interval=0, **kwargs
|
|
):
|
|
"""
|
|
Unpause task from paused status. This is used for auto-paused tasks, such
|
|
as tasks paused on a server reload.
|
|
|
|
Args:
|
|
interval (int): How often to tick the task, in seconds.
|
|
start_delay (int): If the start should be delayed.
|
|
auto_unpause (bool): If set, this will only unpause scripts that were unpaused
|
|
automatically (useful during a system reload/shutdown).
|
|
old_interval (int): The old Script interval (or current one if nothing changed). Used
|
|
to recalculate the unpause startup interval.
|
|
|
|
"""
|
|
paused_time = self.db._paused_time
|
|
if paused_time:
|
|
if auto_unpause and self.db._manually_paused:
|
|
# this was manually paused.
|
|
return
|
|
|
|
# task was paused. This will use the new values as needed.
|
|
callcount = self.db._paused_callcount or 0
|
|
if start_delay is None and interval is not None:
|
|
# adjust start-delay based on how far we were into previous interval
|
|
start_delay = max(0, interval - (old_interval - paused_time))
|
|
else:
|
|
start_delay = paused_time
|
|
|
|
if not self.ndb._task:
|
|
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
|
|
|
self.ndb._task.start(
|
|
self.db_interval, now=False, start_delay=start_delay, count_start=callcount
|
|
)
|
|
self.db._paused_time = None
|
|
self.db._paused_callcount = None
|
|
self.db._manually_paused = None
|
|
|
|
self.at_start(**kwargs)
|
|
|
|
def _stop_task(self, **kwargs):
|
|
"""
|
|
Stop task runner and delete the task.
|
|
|
|
"""
|
|
task_stopped = False
|
|
task = self.ndb._task
|
|
if task and task.running:
|
|
task.stop()
|
|
task_stopped = True
|
|
|
|
self.ndb._task = None
|
|
self.db_is_active = False
|
|
|
|
# make sure this is not confused as a paused script
|
|
self.db._paused_time = None
|
|
self.db._paused_callcount = None
|
|
self.db._manually_paused = None
|
|
|
|
self.save(update_fields=["db_is_active"])
|
|
if task_stopped:
|
|
self.at_stop(**kwargs)
|
|
|
|
def _step_errback(self, e):
|
|
"""
|
|
Callback for runner errors
|
|
|
|
"""
|
|
cname = self.__class__.__name__
|
|
estring = _(
|
|
"Script {key}(#{dbid}) of type '{name}': at_repeat() error '{err}'.".format(
|
|
key=self.key, dbid=self.dbid, name=cname, err=e.getErrorMessage()
|
|
)
|
|
)
|
|
try:
|
|
self.db_obj.msg(estring)
|
|
except Exception:
|
|
# we must not crash inside the errback, even if db_obj is None.
|
|
pass
|
|
logger.log_err(estring)
|
|
|
|
def _step_callback(self):
|
|
"""
|
|
Step task runner. No try..except needed due to defer wrap.
|
|
|
|
"""
|
|
if not self.ndb._task:
|
|
# if there is no task, we have no business using this method
|
|
return
|
|
|
|
if not self.is_valid():
|
|
self.stop()
|
|
return
|
|
|
|
# call hook
|
|
try:
|
|
self.at_repeat()
|
|
except Exception:
|
|
logger.log_trace()
|
|
raise
|
|
|
|
# check repeats
|
|
if self.ndb._task:
|
|
# we need to check for the task in case stop() was called
|
|
# inside at_repeat() and it already went away.
|
|
callcount = self.ndb._task.callcount
|
|
maxcount = self.db_repeats
|
|
if maxcount > 0 and maxcount <= callcount:
|
|
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()
|
|
return None
|
|
|
|
# Access methods / hooks
|
|
|
|
def at_first_save(self, **kwargs):
|
|
"""
|
|
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.
|
|
|
|
Args:
|
|
**kwargs (dict): Arbitrary, optional arguments for users
|
|
overriding the call (unused by default).
|
|
|
|
"""
|
|
self.basetype_setup()
|
|
self.at_script_creation()
|
|
# initialize Attribute/TagProperties
|
|
self.init_evennia_properties()
|
|
|
|
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 = max(0, 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 = max(0, 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 cdict.get("desc") and self.desc != cdict["desc"]:
|
|
self.db_desc = cdict["desc"]
|
|
updates.append("db_desc")
|
|
if updates:
|
|
self.save(update_fields=updates)
|
|
|
|
if cdict.get("permissions"):
|
|
self.permissions.batch_add(*cdict["permissions"])
|
|
if cdict.get("locks"):
|
|
self.locks.add(cdict["locks"])
|
|
if cdict.get("tags"):
|
|
# this should be a list of tags, tuples (key, category) or (key, category, data)
|
|
self.tags.batch_add(*cdict["tags"])
|
|
if cdict.get("attributes"):
|
|
# this should be tuples (key, val, ...)
|
|
self.attributes.batch_add(*cdict["attributes"])
|
|
if cdict.get("nattributes"):
|
|
# this should be a dict of nattrname:value
|
|
for key, value in cdict["nattributes"]:
|
|
self.nattributes.add(key, value)
|
|
|
|
if cdict.get("autostart"):
|
|
# autostart the script
|
|
self._start_task(force_restart=True)
|
|
|
|
def delete(self):
|
|
"""
|
|
Delete the Script. Normally stops any timer task. This fires at_script_delete before
|
|
deletion.
|
|
|
|
Returns:
|
|
bool: If deletion was successful or not. Only time this can fail would be if
|
|
the script was already previously deleted, or `at_script_delete` returns
|
|
False.
|
|
|
|
"""
|
|
if not self.pk or not self.at_script_delete():
|
|
return False
|
|
|
|
self._stop_task()
|
|
super().delete()
|
|
return True
|
|
|
|
def basetype_setup(self):
|
|
"""
|
|
Changes fundamental aspects of the type. Usually changes are made in at_script creation
|
|
instead.
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_init(self):
|
|
"""
|
|
Called when the Script is cached in the idmapper. This is usually more reliable
|
|
than overriding `__init__` since the latter can be called at unexpected times.
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_script_creation(self):
|
|
"""
|
|
Should be overridden in child.
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_script_delete(self):
|
|
"""
|
|
Called when script is deleted, before the script timer stops.
|
|
|
|
Returns:
|
|
bool: If False, deletion is aborted.
|
|
|
|
"""
|
|
return True
|
|
|
|
def is_valid(self):
|
|
"""
|
|
If returning False, `at_repeat` will not be called and timer will stop
|
|
updating.
|
|
"""
|
|
return True
|
|
|
|
def at_repeat(self, **kwargs):
|
|
"""
|
|
Called repeatedly every `interval` seconds, once `.start()` has
|
|
been called on the Script at least once.
|
|
|
|
Args:
|
|
**kwargs (dict): Arbitrary, optional arguments for users
|
|
overriding the call (unused by default).
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_start(self, **kwargs):
|
|
pass
|
|
|
|
def at_pause(self, **kwargs):
|
|
pass
|
|
|
|
def at_stop(self, **kwargs):
|
|
pass
|
|
|
|
def start(self, interval=None, start_delay=None, repeats=None, **kwargs):
|
|
"""
|
|
Start/Unpause timer component, optionally with new values. If given,
|
|
this will update the Script's fields. This will start `at_repeat` being
|
|
called every `interval` seconds.
|
|
|
|
Keyword Args:
|
|
interval (int): How often to fire `at_repeat` in seconds.
|
|
start_delay (int): If the start of ticking should be delayed and by how much.
|
|
repeats (int): How many repeats. 0 for infinite repeats.
|
|
**kwargs: Optional (default unused) kwargs passed on into the `at_start` hook.
|
|
|
|
Notes:
|
|
If setting the `start-delay` of a *paused* Script, the Script will
|
|
restart exactly after that new start-delay, ignoring the time it
|
|
was paused at. If only changing the `interval`, the Script will
|
|
come out of pause comparing the time it spent in the *old* interval
|
|
with the *new* interval in order to determine when next to fire.
|
|
|
|
Examples:
|
|
- Script previously had an interval of 10s and was paused 5s into that interval.
|
|
Script is now restarted with a 20s interval. It will next fire after 15s.
|
|
- Same Script is restarted with a 3s interval. It will fire immediately.
|
|
|
|
"""
|
|
self._start_task(interval=interval, start_delay=start_delay, repeats=repeats, **kwargs)
|
|
|
|
# legacy alias
|
|
update = start
|
|
|
|
def stop(self, **kwargs):
|
|
"""
|
|
Stop the Script's timer component. This will not delete the Sctipt,
|
|
just stop the regular firing of `at_repeat`. Running `.start()` will
|
|
start the timer anew, optionally with new settings..
|
|
|
|
Args:
|
|
**kwargs: Optional (default unused) kwargs passed on into the `at_stop` hook.
|
|
|
|
"""
|
|
self._stop_task(**kwargs)
|
|
|
|
def pause(self, **kwargs):
|
|
"""
|
|
Manually the Script's timer component manually.
|
|
|
|
Args:
|
|
**kwargs: Optional (default unused) kwargs passed on into the `at_pause` hook.
|
|
|
|
"""
|
|
self._pause_task(manual_pause=True, **kwargs)
|
|
|
|
def unpause(self, **kwargs):
|
|
"""
|
|
Manually unpause a Paused Script.
|
|
|
|
Args:
|
|
**kwargs: Optional (default unused) kwargs passed on into the `at_start` hook.
|
|
|
|
"""
|
|
self._unpause_task(**kwargs)
|
|
|
|
def time_until_next_repeat(self):
|
|
"""
|
|
Get time until the script fires it `at_repeat` hook again.
|
|
|
|
Returns:
|
|
int or None: Time in seconds until the script runs again.
|
|
If not a timed script, return `None`.
|
|
|
|
Notes:
|
|
This hook 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 for limited Scripts.
|
|
|
|
Returns:
|
|
int or None: The number of repeats remaining until the Script
|
|
stops. Returns `None` if it has unlimited repeats.
|
|
|
|
"""
|
|
task = self.ndb._task
|
|
if task:
|
|
return max(0, self.db_repeats - task.callcount)
|
|
return None
|
|
|
|
def reset_callcount(self, value=0):
|
|
"""
|
|
Reset the count of the number of calls done.
|
|
|
|
Args:
|
|
value (int, optional): The repeat value to reset to. Default
|
|
is to set it all the way back to 0.
|
|
|
|
Notes:
|
|
This is only useful if repeats != 0.
|
|
|
|
"""
|
|
task = self.ndb._task
|
|
if task:
|
|
task.callcount = max(0, int(value))
|
|
|
|
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()
|
|
|
|
|
|
class DefaultScript(ScriptBase):
|
|
"""
|
|
This is the base TypeClass for all Scripts. Scripts describe
|
|
all entities/systems without a physical existence in the game world
|
|
that require database storage (like an economic system or
|
|
combat tracker). They
|
|
can also have a timer/ticker component.
|
|
|
|
A script type is customized by redefining some or all of its hook
|
|
methods and variables.
|
|
|
|
* available properties (check docs for full listing, this could be
|
|
outdated).
|
|
|
|
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
|
|
|
|
create(key, **kwargs)
|
|
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
|
|
from the paused timer (but at_start() will be called).
|
|
time_until_next_repeat() - if a timed script (interval>0), returns time
|
|
until next tick
|
|
|
|
* Hook methods (should also include self as the first argument):
|
|
|
|
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().
|
|
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_pause()
|
|
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.
|
|
at_script_delete()
|
|
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.
|
|
at_server_start()
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
def create(cls, key, **kwargs):
|
|
"""
|
|
Provides a passthrough interface to the utils.create_script() function.
|
|
|
|
Args:
|
|
key (str): Name of the new object.
|
|
|
|
Returns:
|
|
object (Object): A newly created object of the given typeclass.
|
|
errors (list): A list of errors in string form, if any.
|
|
|
|
"""
|
|
errors = []
|
|
obj = None
|
|
|
|
kwargs["key"] = key
|
|
|
|
# If no typeclass supplied, use this class
|
|
kwargs["typeclass"] = kwargs.pop("typeclass", cls)
|
|
|
|
try:
|
|
obj = create.create_script(**kwargs)
|
|
except Exception:
|
|
logger.log_trace()
|
|
errors.append("The script '%s' encountered errors and could not be created." % key)
|
|
|
|
return obj, errors
|
|
|
|
def at_script_creation(self):
|
|
"""
|
|
Only called once, when script is first created.
|
|
|
|
"""
|
|
pass
|
|
|
|
def is_valid(self):
|
|
"""
|
|
Is called to check if the script's timer is valid to run at this time.
|
|
Should return a boolean. If False, the timer will be stopped.
|
|
|
|
"""
|
|
return True
|
|
|
|
def at_start(self, **kwargs):
|
|
"""
|
|
Called whenever the script timer is started, which for persistent
|
|
timed scripts is at least once every server start. It will also be
|
|
called when starting again after a pause (including after a
|
|
server reload).
|
|
|
|
Args:
|
|
**kwargs (dict): Arbitrary, optional arguments for users
|
|
overriding the call (unused by default).
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_repeat(self, **kwargs):
|
|
"""
|
|
Called repeatedly if this Script is set to repeat regularly.
|
|
|
|
Args:
|
|
**kwargs (dict): Arbitrary, optional arguments for users
|
|
overriding the call (unused by default).
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_pause(self, manual_pause=True, **kwargs):
|
|
"""
|
|
Called when this script's timer pauses.
|
|
|
|
Args:
|
|
manual_pause (bool): If set, pausing was done by a direct call. The
|
|
non-manual pause indicates the script was paused as part of
|
|
the server reload.
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_stop(self, **kwargs):
|
|
"""
|
|
Called whenever when it's time for this script's timer to stop (either
|
|
because is_valid returned False, it ran out of iterations or it was manuallys
|
|
stopped.
|
|
|
|
Args:
|
|
**kwargs (dict): Arbitrary, optional arguments for users
|
|
overriding the call (unused by default).
|
|
|
|
"""
|
|
pass
|
|
|
|
def at_script_delete(self):
|
|
"""
|
|
Called when the Script is deleted, before stopping the timer.
|
|
|
|
Returns:
|
|
bool: If False, the deletion is aborted.
|
|
|
|
"""
|
|
return True
|
|
|
|
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
|
|
|
|
def at_server_start(self):
|
|
"""
|
|
This hook is called after the server has started. It can be used to add
|
|
post-startup setup for Scripts without a timer component (for which at_start
|
|
could be used).
|
|
|
|
"""
|
|
pass
|
|
|
|
|
|
# Some useful default Script types used by Evennia.
|
|
|
|
|
|
class DoNothing(DefaultScript):
|
|
"""
|
|
A 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(DefaultScript):
|
|
"""
|
|
Simple storage script
|
|
"""
|
|
|
|
def at_script_creation(self):
|
|
"""
|
|
Setup the script
|
|
"""
|
|
self.key = "sys_storage"
|
|
self.desc = "This is a generic storage container."
|