[feat] Script refactor; decouple timer component from script lifetime. Resolve #1715
This commit is contained in:
parent
cd579fb649
commit
b5195a6e96
29 changed files with 1136 additions and 1266 deletions
|
|
@ -31,6 +31,13 @@
|
||||||
- Fix typo in UnixCommand contrib, where `help` was given as `--hel`.
|
- Fix typo in UnixCommand contrib, where `help` was given as `--hel`.
|
||||||
- Latin (la) i18n translation (jamalainm)
|
- Latin (la) i18n translation (jamalainm)
|
||||||
- Made the `evennia` dir possible to use without gamedir for purpose of doc generation.
|
- Made the `evennia` dir possible to use without gamedir for purpose of doc generation.
|
||||||
|
- Make Scripts' timer component independent from script object deletion; can now start/stop
|
||||||
|
timer without deleting Script. The `.persistent` flag now only controls if timer survives
|
||||||
|
reload - Script has to be removed with `.delete()` like other typeclassed entities.
|
||||||
|
- Add `utils.repeat` and `utils.unrepeat` as shortcuts to TickerHandler add/remove, similar
|
||||||
|
to how `utils.delay` is a shortcut for TaskHandler add.
|
||||||
|
- Refactor the classic `red_button` example to use `utils.delay/repeat` and modern recommended
|
||||||
|
code style and paradigms instead of relying on `Scripts` for everything.
|
||||||
|
|
||||||
### Evennia 0.9.5 (2019-2020)
|
### Evennia 0.9.5 (2019-2020)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -318,8 +318,6 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
||||||
obj.account = self
|
obj.account = self
|
||||||
session.puid = obj.id
|
session.puid = obj.id
|
||||||
session.puppet = obj
|
session.puppet = obj
|
||||||
# validate/start persistent scripts on object
|
|
||||||
obj.scripts.validate()
|
|
||||||
|
|
||||||
# re-cache locks to make sure superuser bypass is updated
|
# re-cache locks to make sure superuser bypass is updated
|
||||||
obj.locks.cache_lock_bypass(obj)
|
obj.locks.cache_lock_bypass(obj)
|
||||||
|
|
|
||||||
|
|
@ -608,11 +608,6 @@ def cmdhandler(
|
||||||
# since this may be different for every command when
|
# since this may be different for every command when
|
||||||
# merging multuple cmdsets
|
# merging multuple cmdsets
|
||||||
|
|
||||||
if hasattr(cmd, "obj") and hasattr(cmd.obj, "scripts"):
|
|
||||||
# cmd.obj is automatically made available by the cmdhandler.
|
|
||||||
# we make sure to validate its scripts.
|
|
||||||
yield cmd.obj.scripts.validate()
|
|
||||||
|
|
||||||
if _testing:
|
if _testing:
|
||||||
# only return the command instance
|
# only return the command instance
|
||||||
returnValue(cmd)
|
returnValue(cmd)
|
||||||
|
|
|
||||||
|
|
@ -421,7 +421,7 @@ class CmdSetHandler(object):
|
||||||
self.mergetype_stack.append(new_current.actual_mergetype)
|
self.mergetype_stack.append(new_current.actual_mergetype)
|
||||||
self.current = new_current
|
self.current = new_current
|
||||||
|
|
||||||
def add(self, cmdset, emit_to_obj=None, permanent=False, default_cmdset=False):
|
def add(self, cmdset, emit_to_obj=None, persistent=True, permanent=True, default_cmdset=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add a cmdset to the handler, on top of the old ones, unless it
|
Add a cmdset to the handler, on top of the old ones, unless it
|
||||||
is set as the default one (it will then end up at the bottom of the stack)
|
is set as the default one (it will then end up at the bottom of the stack)
|
||||||
|
|
@ -430,7 +430,9 @@ class CmdSetHandler(object):
|
||||||
cmdset (CmdSet or str): Can be a cmdset object or the python path
|
cmdset (CmdSet or str): Can be a cmdset object or the python path
|
||||||
to such an object.
|
to such an object.
|
||||||
emit_to_obj (Object, optional): An object to receive error messages.
|
emit_to_obj (Object, optional): An object to receive error messages.
|
||||||
permanent (bool, optional): This cmdset will remain across a server reboot.
|
persistent (bool, optional): Let cmdset remain across server reload.
|
||||||
|
permanent (bool, optional): DEPRECATED. This has the same use as
|
||||||
|
`persistent`.
|
||||||
default_cmdset (Cmdset, optional): Insert this to replace the
|
default_cmdset (Cmdset, optional): Insert this to replace the
|
||||||
default cmdset position (there is only one such position,
|
default cmdset position (there is only one such position,
|
||||||
always at the bottom of the stack).
|
always at the bottom of the stack).
|
||||||
|
|
@ -447,6 +449,12 @@ class CmdSetHandler(object):
|
||||||
it's a 'quirk' that has to be documented.
|
it's a 'quirk' that has to be documented.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if "permanent" in kwargs:
|
||||||
|
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed to "
|
||||||
|
"'persistent' and now defaults to True.")
|
||||||
|
|
||||||
|
permanent = persistent or permanent
|
||||||
|
|
||||||
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):
|
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):
|
||||||
string = _("Only CmdSets can be added to the cmdsethandler!")
|
string = _("Only CmdSets can be added to the cmdsethandler!")
|
||||||
raise Exception(string)
|
raise Exception(string)
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ def purge_processor(caller):
|
||||||
# something went wrong. Purge cmdset except default
|
# something went wrong. Purge cmdset except default
|
||||||
caller.cmdset.clear()
|
caller.cmdset.clear()
|
||||||
|
|
||||||
caller.scripts.validate() # this will purge interactive mode
|
# caller.scripts.validate() # this will purge interactive mode
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -3120,7 +3120,6 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
|
||||||
% (script.get_display_name(caller), obj.get_display_name(caller))
|
% (script.get_display_name(caller), obj.get_display_name(caller))
|
||||||
)
|
)
|
||||||
script.stop()
|
script.stop()
|
||||||
obj.scripts.validate()
|
|
||||||
else: # rhs exists
|
else: # rhs exists
|
||||||
if not self.switches:
|
if not self.switches:
|
||||||
# adding a new script, and starting it
|
# adding a new script, and starting it
|
||||||
|
|
|
||||||
|
|
@ -436,7 +436,6 @@ class ScriptEvMore(EvMore):
|
||||||
"|wintval|n",
|
"|wintval|n",
|
||||||
"|wnext|n",
|
"|wnext|n",
|
||||||
"|wrept|n",
|
"|wrept|n",
|
||||||
"|wdb",
|
|
||||||
"|wtypeclass|n",
|
"|wtypeclass|n",
|
||||||
"|wdesc|n",
|
"|wdesc|n",
|
||||||
align="r",
|
align="r",
|
||||||
|
|
@ -448,9 +447,10 @@ class ScriptEvMore(EvMore):
|
||||||
|
|
||||||
nextrep = script.time_until_next_repeat()
|
nextrep = script.time_until_next_repeat()
|
||||||
if nextrep is None:
|
if nextrep is None:
|
||||||
nextrep = "PAUSED" if script.db._paused_time else "--"
|
nextrep = script.db._paused_time
|
||||||
|
nextrep = f"PAUSED {int(nextrep)}s" if nextrep else "--"
|
||||||
else:
|
else:
|
||||||
nextrep = "%ss" % nextrep
|
nextrep = f"{nextrep}s"
|
||||||
|
|
||||||
maxrepeat = script.repeats
|
maxrepeat = script.repeats
|
||||||
remaining = script.remaining_repeats() or 0
|
remaining = script.remaining_repeats() or 0
|
||||||
|
|
@ -468,7 +468,6 @@ class ScriptEvMore(EvMore):
|
||||||
script.interval if script.interval > 0 else "--",
|
script.interval if script.interval > 0 else "--",
|
||||||
nextrep,
|
nextrep,
|
||||||
rept,
|
rept,
|
||||||
"*" if script.persistent else "-",
|
|
||||||
script.typeclass_path.rsplit(".", 1)[-1],
|
script.typeclass_path.rsplit(".", 1)[-1],
|
||||||
crop(script.desc, width=20),
|
crop(script.desc, width=20),
|
||||||
)
|
)
|
||||||
|
|
@ -487,7 +486,6 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
start - start a script (must supply a script path)
|
start - start a script (must supply a script path)
|
||||||
stop - stops an existing script
|
stop - stops an existing script
|
||||||
kill - kills a script - without running its cleanup hooks
|
kill - kills a script - without running its cleanup hooks
|
||||||
validate - run a validation on the script(s)
|
|
||||||
|
|
||||||
If no switches are given, this command just views all active
|
If no switches are given, this command just views all active
|
||||||
scripts. The argument can be either an object, at which point it
|
scripts. The argument can be either an object, at which point it
|
||||||
|
|
@ -500,7 +498,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "scripts"
|
key = "scripts"
|
||||||
aliases = ["globalscript", "listscripts"]
|
aliases = ["globalscript", "listscripts"]
|
||||||
switch_options = ("start", "stop", "kill", "validate")
|
switch_options = ("start", "stop", "kill")
|
||||||
locks = "cmd:perm(listscripts) or perm(Admin)"
|
locks = "cmd:perm(listscripts) or perm(Admin)"
|
||||||
help_category = "System"
|
help_category = "System"
|
||||||
|
|
||||||
|
|
@ -558,18 +556,11 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||||
scripts[0].stop()
|
scripts[0].stop()
|
||||||
# import pdb # DEBUG
|
# import pdb # DEBUG
|
||||||
# pdb.set_trace() # DEBUG
|
# pdb.set_trace() # DEBUG
|
||||||
ScriptDB.objects.validate() # just to be sure all is synced
|
|
||||||
caller.msg(string)
|
caller.msg(string)
|
||||||
else:
|
else:
|
||||||
# multiple matches.
|
# multiple matches.
|
||||||
ScriptEvMore(caller, scripts, session=self.session)
|
ScriptEvMore(caller, scripts, session=self.session)
|
||||||
caller.msg("Multiple script matches. Please refine your search")
|
caller.msg("Multiple script matches. Please refine your search")
|
||||||
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
|
||||||
# run validation on all found scripts
|
|
||||||
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts)
|
|
||||||
string = "Validated %s scripts. " % ScriptDB.objects.all().count()
|
|
||||||
string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped)
|
|
||||||
caller.msg(string)
|
|
||||||
else:
|
else:
|
||||||
# No stopping or validation. We just want to view things.
|
# No stopping or validation. We just want to view things.
|
||||||
ScriptEvMore(caller, scripts.order_by("id"), session=self.session)
|
ScriptEvMore(caller, scripts.order_by("id"), session=self.session)
|
||||||
|
|
|
||||||
|
|
@ -1163,8 +1163,8 @@ class TestBuilding(CommandTest):
|
||||||
self.call(building.CmdScript(), "Obj ", "dbref ")
|
self.call(building.CmdScript(), "Obj ", "dbref ")
|
||||||
|
|
||||||
self.call(
|
self.call(
|
||||||
building.CmdScript(), "/start Obj", "0 scripts started on Obj"
|
building.CmdScript(), "/start Obj", "1 scripts started on Obj"
|
||||||
) # because it's already started
|
) # we allow running start again; this should still happen
|
||||||
self.call(building.CmdScript(), "/stop Obj", "Stopping script")
|
self.call(building.CmdScript(), "/stop Obj", "Stopping script")
|
||||||
|
|
||||||
self.call(
|
self.call(
|
||||||
|
|
@ -1520,7 +1520,10 @@ class TestComms(CommandTest):
|
||||||
|
|
||||||
|
|
||||||
class TestBatchProcess(CommandTest):
|
class TestBatchProcess(CommandTest):
|
||||||
def test_batch_commands(self):
|
|
||||||
|
@patch("evennia.contrib.tutorial_examples.red_button.repeat")
|
||||||
|
@patch("evennia.contrib.tutorial_examples.red_button.delay")
|
||||||
|
def test_batch_commands(self, mock_delay, mock_repeat):
|
||||||
# cannot test batchcode here, it must run inside the server process
|
# cannot test batchcode here, it must run inside the server process
|
||||||
self.call(
|
self.call(
|
||||||
batchprocess.CmdBatchCommands(),
|
batchprocess.CmdBatchCommands(),
|
||||||
|
|
@ -1532,6 +1535,7 @@ class TestBatchProcess(CommandTest):
|
||||||
building.CmdDestroy.confirm = False
|
building.CmdDestroy.confirm = False
|
||||||
self.call(building.CmdDestroy(), "button", "button was destroyed.")
|
self.call(building.CmdDestroy(), "button", "button was destroyed.")
|
||||||
building.CmdDestroy.confirm = confirm
|
building.CmdDestroy.confirm = confirm
|
||||||
|
mock_repeat.assert_called()
|
||||||
|
|
||||||
|
|
||||||
class CmdInterrupt(Command):
|
class CmdInterrupt(Command):
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,9 @@ class EventHandler(DefaultScript):
|
||||||
|
|
||||||
# Tasks
|
# Tasks
|
||||||
self.db.tasks = {}
|
self.db.tasks = {}
|
||||||
|
self.at_server_start()
|
||||||
|
|
||||||
def at_start(self):
|
def at_server_start(self):
|
||||||
"""Set up the event system when starting.
|
"""Set up the event system when starting.
|
||||||
|
|
||||||
Note that this hook is called every time the server restarts
|
Note that this hook is called every time the server restarts
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ class TestEventHandler(EvenniaTest):
|
||||||
"""Stop the event handler."""
|
"""Stop the event handler."""
|
||||||
OLD_EVENTS.clear()
|
OLD_EVENTS.clear()
|
||||||
OLD_EVENTS.update(self.handler.ndb.events)
|
OLD_EVENTS.update(self.handler.ndb.events)
|
||||||
self.handler.stop()
|
self.handler.delete()
|
||||||
CallbackHandler.script = None
|
CallbackHandler.script = None
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
|
@ -270,11 +270,11 @@ class TestCmdCallback(CommandTest):
|
||||||
"""Stop the callback handler."""
|
"""Stop the callback handler."""
|
||||||
OLD_EVENTS.clear()
|
OLD_EVENTS.clear()
|
||||||
OLD_EVENTS.update(self.handler.ndb.events)
|
OLD_EVENTS.update(self.handler.ndb.events)
|
||||||
self.handler.stop()
|
self.handler.delete()
|
||||||
for script in ScriptDB.objects.filter(
|
for script in ScriptDB.objects.filter(
|
||||||
db_typeclass_path="evennia.contrib.ingame_python.scripts.TimeEventScript"
|
db_typeclass_path="evennia.contrib.ingame_python.scripts.TimeEventScript"
|
||||||
):
|
):
|
||||||
script.stop()
|
script.delete()
|
||||||
|
|
||||||
CallbackHandler.script = None
|
CallbackHandler.script = None
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
@ -449,7 +449,7 @@ class TestDefaultCallbacks(CommandTest):
|
||||||
"""Stop the callback handler."""
|
"""Stop the callback handler."""
|
||||||
OLD_EVENTS.clear()
|
OLD_EVENTS.clear()
|
||||||
OLD_EVENTS.update(self.handler.ndb.events)
|
OLD_EVENTS.update(self.handler.ndb.events)
|
||||||
self.handler.stop()
|
self.handler.delete()
|
||||||
CallbackHandler.script = None
|
CallbackHandler.script = None
|
||||||
super().tearDown()
|
super().tearDown()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ def register_events(path_or_typeclass):
|
||||||
typeclass_name = typeclass.__module__ + "." + typeclass.__name__
|
typeclass_name = typeclass.__module__ + "." + typeclass.__name__
|
||||||
try:
|
try:
|
||||||
storage = ScriptDB.objects.get(db_key="event_handler")
|
storage = ScriptDB.objects.get(db_key="event_handler")
|
||||||
assert storage.is_active
|
|
||||||
assert storage.ndb.events is not None
|
assert storage.ndb.events is not None
|
||||||
except (ScriptDB.DoesNotExist, AssertionError):
|
except (ScriptDB.DoesNotExist, AssertionError):
|
||||||
storage = EVENTS
|
storage = EVENTS
|
||||||
|
|
|
||||||
|
|
@ -1477,11 +1477,11 @@ class TestTurnBattleBasicFunc(EvenniaTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestTurnBattleBasicFunc, self).tearDown()
|
super(TestTurnBattleBasicFunc, self).tearDown()
|
||||||
|
self.turnhandler.stop()
|
||||||
|
self.testroom.delete()
|
||||||
self.attacker.delete()
|
self.attacker.delete()
|
||||||
self.defender.delete()
|
self.defender.delete()
|
||||||
self.joiner.delete()
|
self.joiner.delete()
|
||||||
self.testroom.delete()
|
|
||||||
self.turnhandler.stop()
|
|
||||||
|
|
||||||
# Test combat functions
|
# Test combat functions
|
||||||
def test_tbbasicfunc(self):
|
def test_tbbasicfunc(self):
|
||||||
|
|
@ -1570,11 +1570,11 @@ class TestTurnBattleEquipFunc(EvenniaTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestTurnBattleEquipFunc, self).tearDown()
|
super(TestTurnBattleEquipFunc, self).tearDown()
|
||||||
|
self.turnhandler.stop()
|
||||||
|
self.testroom.delete()
|
||||||
self.attacker.delete()
|
self.attacker.delete()
|
||||||
self.defender.delete()
|
self.defender.delete()
|
||||||
self.joiner.delete()
|
self.joiner.delete()
|
||||||
self.testroom.delete()
|
|
||||||
self.turnhandler.stop()
|
|
||||||
|
|
||||||
# Test the combat functions in tb_equip too. They work mostly the same.
|
# Test the combat functions in tb_equip too. They work mostly the same.
|
||||||
def test_tbequipfunc(self):
|
def test_tbequipfunc(self):
|
||||||
|
|
@ -1662,11 +1662,11 @@ class TestTurnBattleRangeFunc(EvenniaTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestTurnBattleRangeFunc, self).tearDown()
|
super(TestTurnBattleRangeFunc, self).tearDown()
|
||||||
|
self.turnhandler.stop()
|
||||||
|
self.testroom.delete()
|
||||||
self.attacker.delete()
|
self.attacker.delete()
|
||||||
self.defender.delete()
|
self.defender.delete()
|
||||||
self.joiner.delete()
|
self.joiner.delete()
|
||||||
self.testroom.delete()
|
|
||||||
self.turnhandler.stop()
|
|
||||||
|
|
||||||
# Test combat functions in tb_range too.
|
# Test combat functions in tb_range too.
|
||||||
def test_tbrangefunc(self):
|
def test_tbrangefunc(self):
|
||||||
|
|
@ -1776,12 +1776,12 @@ class TestTurnBattleItemsFunc(EvenniaTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestTurnBattleItemsFunc, self).tearDown()
|
super(TestTurnBattleItemsFunc, self).tearDown()
|
||||||
|
self.turnhandler.stop()
|
||||||
|
self.testroom.delete()
|
||||||
self.attacker.delete()
|
self.attacker.delete()
|
||||||
self.defender.delete()
|
self.defender.delete()
|
||||||
self.joiner.delete()
|
self.joiner.delete()
|
||||||
self.user.delete()
|
self.user.delete()
|
||||||
self.testroom.delete()
|
|
||||||
self.turnhandler.stop()
|
|
||||||
|
|
||||||
# Test functions in tb_items.
|
# Test functions in tb_items.
|
||||||
def test_tbitemsfunc(self):
|
def test_tbitemsfunc(self):
|
||||||
|
|
@ -1895,11 +1895,11 @@ class TestTurnBattleMagicFunc(EvenniaTest):
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(TestTurnBattleMagicFunc, self).tearDown()
|
super(TestTurnBattleMagicFunc, self).tearDown()
|
||||||
|
self.turnhandler.stop()
|
||||||
|
self.testroom.delete()
|
||||||
self.attacker.delete()
|
self.attacker.delete()
|
||||||
self.defender.delete()
|
self.defender.delete()
|
||||||
self.joiner.delete()
|
self.joiner.delete()
|
||||||
self.testroom.delete()
|
|
||||||
self.turnhandler.stop()
|
|
||||||
|
|
||||||
# Test combat functions in tb_magic.
|
# Test combat functions in tb_magic.
|
||||||
def test_tbbasicfunc(self):
|
def test_tbbasicfunc(self):
|
||||||
|
|
|
||||||
|
|
@ -180,10 +180,10 @@ def apply_damage(defender, damage):
|
||||||
def at_defeat(defeated):
|
def at_defeat(defeated):
|
||||||
"""
|
"""
|
||||||
Announces the defeat of a fighter in combat.
|
Announces the defeat of a fighter in combat.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
defeated (obj): Fighter that's been defeated.
|
defeated (obj): Fighter that's been defeated.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
All this does is announce a defeat message by default, but if you
|
All this does is announce a defeat message by default, but if you
|
||||||
want anything else to happen to defeated fighters (like putting them
|
want anything else to happen to defeated fighters (like putting them
|
||||||
|
|
@ -482,6 +482,7 @@ class TBBasicTurnHandler(DefaultScript):
|
||||||
if disengage_check: # All characters have disengaged
|
if disengage_check: # All characters have disengaged
|
||||||
self.obj.msg_contents("All fighters have disengaged! Combat is over!")
|
self.obj.msg_contents("All fighters have disengaged! Combat is over!")
|
||||||
self.stop() # Stop this script and end combat.
|
self.stop() # Stop this script and end combat.
|
||||||
|
self.delete()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check to see if only one character is left standing. If so, end combat.
|
# Check to see if only one character is left standing. If so, end combat.
|
||||||
|
|
@ -497,6 +498,7 @@ class TBBasicTurnHandler(DefaultScript):
|
||||||
LastStanding = fighter # Pick the one fighter left with HP remaining
|
LastStanding = fighter # Pick the one fighter left with HP remaining
|
||||||
self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding)
|
self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding)
|
||||||
self.stop() # Stop this script and end combat.
|
self.stop() # Stop this script and end combat.
|
||||||
|
self.delete()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Cycle to the next turn.
|
# Cycle to the next turn.
|
||||||
|
|
|
||||||
|
|
@ -1,335 +0,0 @@
|
||||||
"""
|
|
||||||
This defines the cmdset for the red_button. Here we have defined
|
|
||||||
the commands and the cmdset in the same module, but if you
|
|
||||||
have many different commands to merge it is often better
|
|
||||||
to define the cmdset separately, picking and choosing from
|
|
||||||
among the available commands as to what should be included in the
|
|
||||||
cmdset - this way you can often re-use the commands too.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import random
|
|
||||||
from evennia import Command, CmdSet
|
|
||||||
|
|
||||||
# Some simple commands for the red button
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
# Commands defined on the red button
|
|
||||||
# ------------------------------------------------------------
|
|
||||||
|
|
||||||
|
|
||||||
class CmdNudge(Command):
|
|
||||||
"""
|
|
||||||
Try to nudge the button's lid
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
nudge lid
|
|
||||||
|
|
||||||
This command will have you try to
|
|
||||||
push the lid of the button away.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "nudge lid" # two-word command name!
|
|
||||||
aliases = ["nudge"]
|
|
||||||
locks = "cmd:all()"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"""
|
|
||||||
nudge the lid. Random chance of success to open it.
|
|
||||||
"""
|
|
||||||
rand = random.random()
|
|
||||||
if rand < 0.5:
|
|
||||||
self.caller.msg("You nudge at the lid. It seems stuck.")
|
|
||||||
elif rand < 0.7:
|
|
||||||
self.caller.msg("You move the lid back and forth. It won't budge.")
|
|
||||||
else:
|
|
||||||
self.caller.msg("You manage to get a nail under the lid.")
|
|
||||||
self.caller.execute_cmd("open lid")
|
|
||||||
|
|
||||||
|
|
||||||
class CmdPush(Command):
|
|
||||||
"""
|
|
||||||
Push the red button
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
push button
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "push button"
|
|
||||||
aliases = ["push", "press button", "press"]
|
|
||||||
locks = "cmd:all()"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"""
|
|
||||||
Note that we choose to implement this with checking for
|
|
||||||
if the lid is open/closed. This is because this command
|
|
||||||
is likely to be tried regardless of the state of the lid.
|
|
||||||
|
|
||||||
An alternative would be to make two versions of this command
|
|
||||||
and tuck them into the cmdset linked to the Open and Closed
|
|
||||||
lid-state respectively.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if self.obj.db.lid_open:
|
|
||||||
string = "You reach out to press the big red button ..."
|
|
||||||
string += "\n\nA BOOM! A bright light blinds you!"
|
|
||||||
string += "\nThe world goes dark ..."
|
|
||||||
self.caller.msg(string)
|
|
||||||
self.caller.location.msg_contents(
|
|
||||||
"%s presses the button. BOOM! %s is blinded by a flash!"
|
|
||||||
% (self.caller.name, self.caller.name),
|
|
||||||
exclude=self.caller,
|
|
||||||
)
|
|
||||||
# the button's method will handle all setup of scripts etc.
|
|
||||||
self.obj.press_button(self.caller)
|
|
||||||
else:
|
|
||||||
string = "You cannot push the button - there is a glass lid covering it."
|
|
||||||
self.caller.msg(string)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdSmashGlass(Command):
|
|
||||||
"""
|
|
||||||
smash glass
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
smash glass
|
|
||||||
|
|
||||||
Try to smash the glass of the button.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "smash glass"
|
|
||||||
aliases = ["smash lid", "break lid", "smash"]
|
|
||||||
locks = "cmd:all()"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"""
|
|
||||||
The lid won't open, but there is a small chance
|
|
||||||
of causing the lamp to break.
|
|
||||||
"""
|
|
||||||
rand = random.random()
|
|
||||||
|
|
||||||
if rand < 0.2:
|
|
||||||
string = "You smash your hand against the glass"
|
|
||||||
string += " with all your might. The lid won't budge"
|
|
||||||
string += " but you cause quite the tremor through the button's mount."
|
|
||||||
string += "\nIt looks like the button's lamp stopped working for the time being."
|
|
||||||
self.obj.lamp_works = False
|
|
||||||
elif rand < 0.6:
|
|
||||||
string = "You hit the lid hard. It doesn't move an inch."
|
|
||||||
else:
|
|
||||||
string = "You place a well-aimed fist against the glass of the lid."
|
|
||||||
string += " Unfortunately all you get is a pain in your hand. Maybe"
|
|
||||||
string += " you should just try to open the lid instead?"
|
|
||||||
self.caller.msg(string)
|
|
||||||
self.caller.location.msg_contents(
|
|
||||||
"%s tries to smash the glass of the button." % (self.caller.name), exclude=self.caller
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdOpenLid(Command):
|
|
||||||
"""
|
|
||||||
open lid
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
open lid
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "open lid"
|
|
||||||
aliases = ["open button", "open"]
|
|
||||||
locks = "cmd:all()"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"simply call the right function."
|
|
||||||
|
|
||||||
if self.obj.db.lid_locked:
|
|
||||||
self.caller.msg("This lid seems locked in place for the moment.")
|
|
||||||
return
|
|
||||||
|
|
||||||
string = "\nA ticking sound is heard, like a winding mechanism. Seems "
|
|
||||||
string += "the lid will soon close again."
|
|
||||||
self.caller.msg(string)
|
|
||||||
self.caller.location.msg_contents(
|
|
||||||
"%s opens the lid of the button." % (self.caller.name), exclude=self.caller
|
|
||||||
)
|
|
||||||
# add the relevant cmdsets to button
|
|
||||||
self.obj.cmdset.add(LidClosedCmdSet)
|
|
||||||
# call object method
|
|
||||||
self.obj.open_lid()
|
|
||||||
|
|
||||||
|
|
||||||
class CmdCloseLid(Command):
|
|
||||||
"""
|
|
||||||
close the lid
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
close lid
|
|
||||||
|
|
||||||
Closes the lid of the red button.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "close lid"
|
|
||||||
aliases = ["close"]
|
|
||||||
locks = "cmd:all()"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"Close the lid"
|
|
||||||
|
|
||||||
self.obj.close_lid()
|
|
||||||
|
|
||||||
# this will clean out scripts dependent on lid being open.
|
|
||||||
self.caller.msg("You close the button's lid. It clicks back into place.")
|
|
||||||
self.caller.location.msg_contents(
|
|
||||||
"%s closes the button's lid." % (self.caller.name), exclude=self.caller
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdBlindLook(Command):
|
|
||||||
"""
|
|
||||||
Looking around in darkness
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
look <obj>
|
|
||||||
|
|
||||||
... not that there's much to see in the dark.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "look"
|
|
||||||
aliases = ["l", "get", "examine", "ex", "feel", "listen"]
|
|
||||||
locks = "cmd:all()"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"This replaces all the senses when blinded."
|
|
||||||
|
|
||||||
# we decide what to reply based on which command was
|
|
||||||
# actually tried
|
|
||||||
|
|
||||||
if self.cmdstring == "get":
|
|
||||||
string = "You fumble around blindly without finding anything."
|
|
||||||
elif self.cmdstring == "examine":
|
|
||||||
string = "You try to examine your surroundings, but can't see a thing."
|
|
||||||
elif self.cmdstring == "listen":
|
|
||||||
string = "You are deafened by the boom."
|
|
||||||
elif self.cmdstring == "feel":
|
|
||||||
string = "You fumble around, hands outstretched. You bump your knee."
|
|
||||||
else:
|
|
||||||
# trying to look
|
|
||||||
string = "You are temporarily blinded by the flash. "
|
|
||||||
string += "Until it wears off, all you can do is feel around blindly."
|
|
||||||
self.caller.msg(string)
|
|
||||||
self.caller.location.msg_contents(
|
|
||||||
"%s stumbles around, blinded." % (self.caller.name), exclude=self.caller
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdBlindHelp(Command):
|
|
||||||
"""
|
|
||||||
Help function while in the blinded state
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
help
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "help"
|
|
||||||
aliases = "h"
|
|
||||||
locks = "cmd:all()"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"Give a message."
|
|
||||||
self.caller.msg("You are beyond help ... until you can see again.")
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
# Command sets for the red button
|
|
||||||
# ---------------------------------------------------------------
|
|
||||||
|
|
||||||
# We next tuck these commands into their respective command sets.
|
|
||||||
# (note that we are overdoing the cdmset separation a bit here
|
|
||||||
# to show how it works).
|
|
||||||
|
|
||||||
|
|
||||||
class DefaultCmdSet(CmdSet):
|
|
||||||
"""
|
|
||||||
The default cmdset always sits
|
|
||||||
on the button object and whereas other
|
|
||||||
command sets may be added/merge onto it
|
|
||||||
and hide it, removing them will always
|
|
||||||
bring it back. It's added to the object
|
|
||||||
using obj.cmdset.add_default().
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "RedButtonDefault"
|
|
||||||
mergetype = "Union" # this is default, we don't really need to put it here.
|
|
||||||
|
|
||||||
def at_cmdset_creation(self):
|
|
||||||
"Init the cmdset"
|
|
||||||
self.add(CmdPush())
|
|
||||||
|
|
||||||
|
|
||||||
class LidClosedCmdSet(CmdSet):
|
|
||||||
"""
|
|
||||||
A simple cmdset tied to the redbutton object.
|
|
||||||
|
|
||||||
It contains the commands that launches the other
|
|
||||||
command sets, making the red button a self-contained
|
|
||||||
item (i.e. you don't have to manually add any
|
|
||||||
scripts etc to it when creating it).
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "LidClosedCmdSet"
|
|
||||||
# default Union is used *except* if we are adding to a
|
|
||||||
# cmdset named LidOpenCmdSet - this one we replace
|
|
||||||
# completely.
|
|
||||||
key_mergetype = {"LidOpenCmdSet": "Replace"}
|
|
||||||
|
|
||||||
def at_cmdset_creation(self):
|
|
||||||
"Populates the cmdset when it is instantiated."
|
|
||||||
self.add(CmdNudge())
|
|
||||||
self.add(CmdSmashGlass())
|
|
||||||
self.add(CmdOpenLid())
|
|
||||||
|
|
||||||
|
|
||||||
class LidOpenCmdSet(CmdSet):
|
|
||||||
"""
|
|
||||||
This is the opposite of the Closed cmdset.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "LidOpenCmdSet"
|
|
||||||
# default Union is used *except* if we are adding to a
|
|
||||||
# cmdset named LidClosedCmdSet - this one we replace
|
|
||||||
# completely.
|
|
||||||
key_mergetype = {"LidClosedCmdSet": "Replace"}
|
|
||||||
|
|
||||||
def at_cmdset_creation(self):
|
|
||||||
"setup the cmdset (just one command)"
|
|
||||||
self.add(CmdCloseLid())
|
|
||||||
|
|
||||||
|
|
||||||
class BlindCmdSet(CmdSet):
|
|
||||||
"""
|
|
||||||
This is the cmdset added to the *account* when
|
|
||||||
the button is pushed.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "BlindCmdSet"
|
|
||||||
# we want it to completely replace all normal commands
|
|
||||||
# until the timed script removes it again.
|
|
||||||
mergetype = "Replace"
|
|
||||||
# we want to stop the account from walking around
|
|
||||||
# in this blinded state, so we hide all exits too.
|
|
||||||
# (channel commands will still work).
|
|
||||||
no_exits = True # keep account in the same room
|
|
||||||
no_objs = True # don't allow object commands
|
|
||||||
|
|
||||||
def at_cmdset_creation(self):
|
|
||||||
"Setup the blind cmdset"
|
|
||||||
from evennia.commands.default.general import CmdSay
|
|
||||||
from evennia.commands.default.general import CmdPose
|
|
||||||
|
|
||||||
self.add(CmdSay())
|
|
||||||
self.add(CmdPose())
|
|
||||||
self.add(CmdBlindLook())
|
|
||||||
self.add(CmdBlindHelp())
|
|
||||||
|
|
@ -9,11 +9,382 @@ Create this button with
|
||||||
@create/drop examples.red_button.RedButton
|
@create/drop examples.red_button.RedButton
|
||||||
|
|
||||||
Note that you must drop the button before you can see its messages!
|
Note that you must drop the button before you can see its messages!
|
||||||
|
|
||||||
|
## Technical
|
||||||
|
|
||||||
|
The button's functionality is controlled by CmdSets that gets added and removed
|
||||||
|
depending on the 'state' the button is in. Since this is an example we have
|
||||||
|
gone a little overboard separating the states for clarity.
|
||||||
|
|
||||||
|
- The default state is the fallback state and is represented by a default cmdset with
|
||||||
|
the 'push' command. This is always available but acts differently depending on
|
||||||
|
what other state the button has:
|
||||||
|
- Lid-closed state: In this state the button is covered by a glass cover and trying
|
||||||
|
to 'push' it will fail. You can 'nudge', 'smash' or 'open' the lid.
|
||||||
|
- Lid-open state: In this state the lid is open but will close again after a certain
|
||||||
|
time. Using 'push' now will press the button and trigger the Blind-state.
|
||||||
|
- Blind-state: In this mode you are blinded by a bright flash. This will affect your
|
||||||
|
normal commands like 'look' and help until the blindness wears off after a certain
|
||||||
|
time.
|
||||||
|
|
||||||
|
Timers are handled by persistent delays on the button. These are examples of
|
||||||
|
`evennia.utils.utils.delay` calls that wait a certain time before calling a method -
|
||||||
|
such as when closing the lid and un-blinding a character.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import random
|
import random
|
||||||
from evennia import DefaultObject
|
from evennia import DefaultObject
|
||||||
from evennia.contrib.tutorial_examples import red_button_scripts as scriptexamples
|
from evennia import Command, CmdSet
|
||||||
from evennia.contrib.tutorial_examples import cmdset_red_button as cmdsetexamples
|
from evennia.utils.utils import delay, repeat, interactive
|
||||||
|
|
||||||
|
|
||||||
|
# Commands on the button (not all awailable at the same time)
|
||||||
|
|
||||||
|
|
||||||
|
# Commands for the state when the lid covering the button is closed.
|
||||||
|
|
||||||
|
class CmdPushLidClosed(Command):
|
||||||
|
"""
|
||||||
|
Push the red button (lid closed)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
push button
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "push button"
|
||||||
|
aliases = ["push", "press button", "press"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"""
|
||||||
|
This is the version of push used when the lid is closed.
|
||||||
|
|
||||||
|
An alternative would be to make a 'push' command in a default cmdset
|
||||||
|
that is always available on the button and then use if-statements to
|
||||||
|
check if the lid is open or closed.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.caller.msg("You cannot push the button = there is a glass lid covering it.")
|
||||||
|
|
||||||
|
|
||||||
|
class CmdNudge(Command):
|
||||||
|
"""
|
||||||
|
Try to nudge the button's lid.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
nudge lid
|
||||||
|
|
||||||
|
This command will have you try to push the lid of the button away.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "nudge lid" # two-word command name!
|
||||||
|
aliases = ["nudge"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"""
|
||||||
|
Nudge the lid. Random chance of success to open it.
|
||||||
|
|
||||||
|
"""
|
||||||
|
rand = random.random()
|
||||||
|
if rand < 0.5:
|
||||||
|
self.caller.msg("You nudge at the lid. It seems stuck.")
|
||||||
|
elif rand < 0.7:
|
||||||
|
self.caller.msg("You move the lid back and forth. It won't budge.")
|
||||||
|
else:
|
||||||
|
self.caller.msg("You manage to get a nail under the lid.")
|
||||||
|
# self.obj is the button object
|
||||||
|
self.obj.to_open_state()
|
||||||
|
|
||||||
|
|
||||||
|
class CmdSmashGlass(Command):
|
||||||
|
"""
|
||||||
|
Smash the protective glass.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
smash glass
|
||||||
|
|
||||||
|
Try to smash the glass of the button.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "smash glass"
|
||||||
|
aliases = ["smash lid", "break lid", "smash"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"""
|
||||||
|
The lid won't open, but there is a small chance of causing the lamp to
|
||||||
|
break.
|
||||||
|
|
||||||
|
"""
|
||||||
|
rand = random.random()
|
||||||
|
self.caller.location.msg_contents(
|
||||||
|
f"{self.caller.name} tries to smash the glass of the button.",
|
||||||
|
exclude=self.caller)
|
||||||
|
|
||||||
|
if rand < 0.2:
|
||||||
|
string = ("You smash your hand against the glass"
|
||||||
|
" with all your might. The lid won't budge"
|
||||||
|
" but you cause quite the tremor through the button's mount."
|
||||||
|
"\nIt looks like the button's lamp stopped working for the time being, "
|
||||||
|
"but the lid is still as closed as ever.")
|
||||||
|
# self.obj is the button itself
|
||||||
|
self.obj.break_lamp()
|
||||||
|
elif rand < 0.6:
|
||||||
|
string = "You hit the lid hard. It doesn't move an inch."
|
||||||
|
else:
|
||||||
|
string = ("You place a well-aimed fist against the glass of the lid."
|
||||||
|
" Unfortunately all you get is a pain in your hand. Maybe"
|
||||||
|
" you should just try to just ... open the lid instead?")
|
||||||
|
self.caller.msg(string)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdOpenLid(Command):
|
||||||
|
"""
|
||||||
|
open lid
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
open lid
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "open lid"
|
||||||
|
aliases = ["open button"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"simply call the right function."
|
||||||
|
|
||||||
|
if self.obj.db.lid_locked:
|
||||||
|
self.caller.msg("This lid seems locked in place for the moment.")
|
||||||
|
return
|
||||||
|
|
||||||
|
string = "\nA ticking sound is heard, like a winding mechanism. Seems "
|
||||||
|
string += "the lid will soon close again."
|
||||||
|
self.caller.msg(string)
|
||||||
|
self.caller.location.msg_contents(
|
||||||
|
f"{self.caller.name} opens the lid of the button.",
|
||||||
|
exclude=self.caller)
|
||||||
|
self.obj.to_open_state()
|
||||||
|
|
||||||
|
|
||||||
|
class LidClosedCmdSet(CmdSet):
|
||||||
|
"""
|
||||||
|
A simple cmdset tied to the redbutton object.
|
||||||
|
|
||||||
|
It contains the commands that launches the other
|
||||||
|
command sets, making the red button a self-contained
|
||||||
|
item (i.e. you don't have to manually add any
|
||||||
|
scripts etc to it when creating it).
|
||||||
|
|
||||||
|
Note that this is given with a `key_mergetype` set. This
|
||||||
|
is set up so that the cmdset with merge with Union merge type
|
||||||
|
*except* if the other cmdset to merge with is LidOpenCmdSet,
|
||||||
|
in which case it will Replace that. So these two cmdsets will
|
||||||
|
be mutually exclusive.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "LidClosedCmdSet"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
"Populates the cmdset when it is instantiated."
|
||||||
|
self.add(CmdPushLidClosed())
|
||||||
|
self.add(CmdNudge())
|
||||||
|
self.add(CmdSmashGlass())
|
||||||
|
self.add(CmdOpenLid())
|
||||||
|
|
||||||
|
|
||||||
|
# Commands for the state when the button's protective cover is open - now the
|
||||||
|
# push command will work. You can also close the lid again.
|
||||||
|
|
||||||
|
class CmdPushLidOpen(Command):
|
||||||
|
"""
|
||||||
|
Push the red button
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
push button
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "push button"
|
||||||
|
aliases = ["push", "press button", "press"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
@interactive
|
||||||
|
def func(self):
|
||||||
|
"""
|
||||||
|
This version of push will immediately trigger the next button state.
|
||||||
|
|
||||||
|
The use of the @interactive decorator allows for using `yield` to add
|
||||||
|
simple pauses in how quickly a message is returned to the user. This
|
||||||
|
kind of pause will not survive a server reload.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# pause a little between each message.
|
||||||
|
self.caller.msg("You reach out to press the big red button ...")
|
||||||
|
yield(2) # pause 2s before next message
|
||||||
|
self.caller.msg("\n\n|wBOOOOM! A bright light blinds you!|n")
|
||||||
|
yield(1) # pause 1s before next message
|
||||||
|
self.caller.msg("\n\n|xThe world goes dark ...|n")
|
||||||
|
|
||||||
|
name = self.caller.name
|
||||||
|
self.caller.location.msg_contents(
|
||||||
|
f"{name} presses the button. BOOM! {name} is blinded by a flash!",
|
||||||
|
exclude=self.caller)
|
||||||
|
self.obj.blind_target(self.caller)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdCloseLid(Command):
|
||||||
|
"""
|
||||||
|
Close the lid
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
close lid
|
||||||
|
|
||||||
|
Closes the lid of the red button.
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "close lid"
|
||||||
|
aliases = ["close"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"Close the lid"
|
||||||
|
|
||||||
|
self.obj.to_closed_state()
|
||||||
|
|
||||||
|
# this will clean out scripts dependent on lid being open.
|
||||||
|
self.caller.msg("You close the button's lid. It clicks back into place.")
|
||||||
|
self.caller.location.msg_contents(
|
||||||
|
f"{self.caller.name} closes the button's lid.",
|
||||||
|
exclude=self.caller)
|
||||||
|
|
||||||
|
|
||||||
|
class LidOpenCmdSet(CmdSet):
|
||||||
|
"""
|
||||||
|
This is the opposite of the Closed cmdset.
|
||||||
|
|
||||||
|
Note that this is given with a `key_mergetype` set. This
|
||||||
|
is set up so that the cmdset with merge with Union merge type
|
||||||
|
*except* if the other cmdset to merge with is LidClosedCmdSet,
|
||||||
|
in which case it will Replace that. So these two cmdsets will
|
||||||
|
be mutually exclusive.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "LidOpenCmdSet"
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
"""Setup the cmdset"""
|
||||||
|
self.add(CmdPushLidOpen())
|
||||||
|
self.add(CmdCloseLid())
|
||||||
|
|
||||||
|
|
||||||
|
# Commands for when the button has been pushed and the player is blinded. This
|
||||||
|
# replaces commands on the player making them 'blind' for a while.
|
||||||
|
|
||||||
|
class CmdBlindLook(Command):
|
||||||
|
"""
|
||||||
|
Looking around in darkness
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
look <obj>
|
||||||
|
|
||||||
|
... not that there's much to see in the dark.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "look"
|
||||||
|
aliases = ["l", "get", "examine", "ex", "feel", "listen"]
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"This replaces all the senses when blinded."
|
||||||
|
|
||||||
|
# we decide what to reply based on which command was
|
||||||
|
# actually tried
|
||||||
|
|
||||||
|
if self.cmdstring == "get":
|
||||||
|
string = "You fumble around blindly without finding anything."
|
||||||
|
elif self.cmdstring == "examine":
|
||||||
|
string = "You try to examine your surroundings, but can't see a thing."
|
||||||
|
elif self.cmdstring == "listen":
|
||||||
|
string = "You are deafened by the boom."
|
||||||
|
elif self.cmdstring == "feel":
|
||||||
|
string = "You fumble around, hands outstretched. You bump your knee."
|
||||||
|
else:
|
||||||
|
# trying to look
|
||||||
|
string = ("You are temporarily blinded by the flash. "
|
||||||
|
"Until it wears off, all you can do is feel around blindly.")
|
||||||
|
self.caller.msg(string)
|
||||||
|
self.caller.location.msg_contents(
|
||||||
|
f"{self.caller.name} stumbles around, blinded.",
|
||||||
|
exclude=self.caller)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdBlindHelp(Command):
|
||||||
|
"""
|
||||||
|
Help function while in the blinded state
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
help
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "help"
|
||||||
|
aliases = "h"
|
||||||
|
locks = "cmd:all()"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"""
|
||||||
|
Just give a message while blinded. We could have added this to the
|
||||||
|
CmdBlindLook command too if we wanted to keep things more compact.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.caller.msg("You are beyond help ... until you can see again.")
|
||||||
|
|
||||||
|
|
||||||
|
class BlindCmdSet(CmdSet):
|
||||||
|
"""
|
||||||
|
This is the cmdset added to the *account* when
|
||||||
|
the button is pushed.
|
||||||
|
|
||||||
|
Since this has mergetype Replace it will completely remove the commands of
|
||||||
|
all other cmdsets while active. To allow some limited interaction
|
||||||
|
(pose/say) we import those default commands and add them too.
|
||||||
|
|
||||||
|
We also disable all exit-commands generated by exits and
|
||||||
|
object-interactions while blinded by setting `no_exits` and `no_objs` flags
|
||||||
|
on the cmdset. This is to avoid the player walking off or interfering with
|
||||||
|
other objects while blinded. Account-level commands however (channel messaging
|
||||||
|
etc) will not be affected by the blinding.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
key = "BlindCmdSet"
|
||||||
|
# we want it to completely replace all normal commands
|
||||||
|
# until the timed script removes it again.
|
||||||
|
mergetype = "Replace"
|
||||||
|
# we want to stop the player from walking around
|
||||||
|
# in this blinded state, so we hide all exits too.
|
||||||
|
# (channel commands will still work).
|
||||||
|
no_exits = True # keep player in the same room
|
||||||
|
no_objs = True # don't allow object commands
|
||||||
|
|
||||||
|
def at_cmdset_creation(self):
|
||||||
|
"Setup the blind cmdset"
|
||||||
|
from evennia.commands.default.general import CmdSay
|
||||||
|
from evennia.commands.default.general import CmdPose
|
||||||
|
|
||||||
|
self.add(CmdSay())
|
||||||
|
self.add(CmdPose())
|
||||||
|
self.add(CmdBlindLook())
|
||||||
|
self.add(CmdBlindHelp())
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Definition of the object itself
|
# Definition of the object itself
|
||||||
|
|
@ -22,146 +393,189 @@ from evennia.contrib.tutorial_examples import cmdset_red_button as cmdsetexample
|
||||||
|
|
||||||
class RedButton(DefaultObject):
|
class RedButton(DefaultObject):
|
||||||
"""
|
"""
|
||||||
This class describes an evil red button. It will use the script
|
This class describes an evil red button. It will blink invitingly and
|
||||||
definition in contrib/examples/red_button_scripts to blink at regular
|
temporarily blind whomever presses it.
|
||||||
intervals. It also uses a series of script and commands to handle
|
|
||||||
pushing the button and causing effects when doing so.
|
|
||||||
|
|
||||||
The following attributes can be set on the button:
|
The button can take a few optional attributes controlling how things will
|
||||||
desc_lid_open - description when lid is open
|
be displayed in its various states. This is a useful way to give builders
|
||||||
desc_lid_closed - description when lid is closed
|
the option to customize a complex object from in-game. Actual return messages
|
||||||
desc_lamp_broken - description when lamp is broken
|
to event-actions are (in this example) left with each command, but one could
|
||||||
|
also imagine having those handled via Attributes as well, if one wanted a
|
||||||
|
completely in-game customizable button without needing to tweak command
|
||||||
|
classes.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
- `desc_closed_lid`: This is the description to show of the button
|
||||||
|
when the lid is closed.
|
||||||
|
- `desc_open_lid`": Shown when the lid is open
|
||||||
|
- `auto_close_msg`: Message to show when lid auto-closes
|
||||||
|
- `desc_add_lamp_broken`: Extra desc-line added after normal desc when lamp
|
||||||
|
is broken.
|
||||||
|
- blink_msg: A list of strings to randomly choose from when the lamp
|
||||||
|
blinks.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The button starts with lid closed. To set the initial description,
|
||||||
|
you can either set desc after creating it or pass a `desc` attribute
|
||||||
|
when creating it, such as
|
||||||
|
`button = create_object(RedButton, ..., attributes=[('desc', 'my desc')])`.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# these are the pre-set descriptions. Setting attributes will override
|
||||||
|
# these on the fly.
|
||||||
|
|
||||||
|
desc_closed_lid = ("This is a large red button, inviting yet evil-looking. "
|
||||||
|
"A closed glass lid protects it.")
|
||||||
|
desc_open_lid = ("This is a large red button, inviting yet evil-looking. "
|
||||||
|
"Its glass cover is open and the button exposed.")
|
||||||
|
auto_close_msg = "The button's glass lid silently slides back in place."
|
||||||
|
lamp_breaks_msg = "The lamp flickers, the button going dark."
|
||||||
|
desc_add_lamp_broken = "\nThe big red button has stopped blinking for the time being."
|
||||||
|
# note that this is a list. A random message will display each time
|
||||||
|
blink_msgs = ["The red button flashes briefly.",
|
||||||
|
"The red button blinks invitingly.",
|
||||||
|
"The red button flashes. You know you wanna push it!"]
|
||||||
|
|
||||||
def at_object_creation(self):
|
def at_object_creation(self):
|
||||||
"""
|
"""
|
||||||
This function is called when object is created. Use this
|
This function is called (once) when object is created.
|
||||||
instead of e.g. __init__.
|
|
||||||
"""
|
|
||||||
# store desc (default, you can change this at creation time)
|
|
||||||
desc = "This is a large red button, inviting yet evil-looking. "
|
|
||||||
desc += "A closed glass lid protects it."
|
|
||||||
self.db.desc = desc
|
|
||||||
|
|
||||||
# We have to define all the variables the scripts
|
"""
|
||||||
# are checking/using *before* adding the scripts or
|
|
||||||
# they might be deactivated before even starting!
|
|
||||||
self.db.lid_open = False
|
|
||||||
self.db.lamp_works = True
|
self.db.lamp_works = True
|
||||||
self.db.lid_locked = False
|
|
||||||
|
|
||||||
self.cmdset.add_default(cmdsetexamples.DefaultCmdSet, permanent=True)
|
# start closed
|
||||||
|
self.to_closed_state()
|
||||||
|
|
||||||
# since the cmdsets relevant to the button are added 'on the fly',
|
# start blinking every 35s.
|
||||||
# we need to setup custom scripts to do this for us (also, these scripts
|
repeat(35, self._do_blink, persistent=True)
|
||||||
# check so they are valid (i.e. the lid is actually still closed)).
|
|
||||||
# The AddClosedCmdSet script makes sure to add the Closed-cmdset.
|
|
||||||
self.scripts.add(scriptexamples.ClosedLidState)
|
|
||||||
# the script EventBlinkButton makes the button blink regularly.
|
|
||||||
self.scripts.add(scriptexamples.BlinkButtonEvent)
|
|
||||||
|
|
||||||
# state-changing methods
|
def _do_blink(self):
|
||||||
|
|
||||||
def open_lid(self):
|
|
||||||
"""
|
"""
|
||||||
Opens the glass lid and start the timer so it will soon close
|
Have the button blink invitingly unless it's broken.
|
||||||
again.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if self.location and self.db.lamp_works:
|
||||||
|
possible_messages = self.db.blink_msgs or self.blink_msgs
|
||||||
|
self.location.msg_contents(random.choice(possible_messages))
|
||||||
|
|
||||||
if self.db.lid_open:
|
def _set_desc(self, attrname=None):
|
||||||
return
|
|
||||||
desc = self.db.desc_lid_open
|
|
||||||
if not desc:
|
|
||||||
desc = "This is a large red button, inviting yet evil-looking. "
|
|
||||||
desc += "Its glass cover is open and the button exposed."
|
|
||||||
self.db.desc = desc
|
|
||||||
self.db.lid_open = True
|
|
||||||
|
|
||||||
# with the lid open, we validate scripts; this will clean out
|
|
||||||
# scripts that depend on the lid to be closed.
|
|
||||||
self.scripts.validate()
|
|
||||||
# now add new scripts that define the open-lid state
|
|
||||||
self.scripts.add(scriptexamples.OpenLidState)
|
|
||||||
# we also add a scripted event that will close the lid after a while.
|
|
||||||
# (this one cleans itself after being called once)
|
|
||||||
self.scripts.add(scriptexamples.CloseLidEvent)
|
|
||||||
|
|
||||||
def close_lid(self):
|
|
||||||
"""
|
"""
|
||||||
Close the glass lid. This validates all scripts on the button,
|
Set a description, based on the attrname given, taking the lamp-status
|
||||||
which means that scripts only being valid when the lid is open
|
into account.
|
||||||
will go away automatically.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not self.db.lid_open:
|
|
||||||
return
|
|
||||||
desc = self.db.desc_lid_closed
|
|
||||||
if not desc:
|
|
||||||
desc = "This is a large red button, inviting yet evil-looking. "
|
|
||||||
desc += "Its glass cover is closed, protecting it."
|
|
||||||
self.db.desc = desc
|
|
||||||
self.db.lid_open = False
|
|
||||||
|
|
||||||
# clean out scripts depending on lid to be open
|
|
||||||
self.scripts.validate()
|
|
||||||
# add scripts related to the closed state
|
|
||||||
self.scripts.add(scriptexamples.ClosedLidState)
|
|
||||||
|
|
||||||
def break_lamp(self, feedback=True):
|
|
||||||
"""
|
|
||||||
Breaks the lamp in the button, stopping it from blinking.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
feedback (bool): Show a message about breaking the lamp.
|
attrname (str, optional): This will first check for an Attribute with this name,
|
||||||
|
secondly for a property on the class. So if `attrname="auto_close_msg"`,
|
||||||
|
we will first look for an attribute `.db.auto_close_msg` and if that's
|
||||||
|
not found we'll use `.auto_close_msg` instead. If unset (`None`), the
|
||||||
|
currently set desc will not be changed (only lamp will be checked).
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
If `self.db.lamp_works` is `False`, we'll append
|
||||||
|
`desc_add_lamp_broken` text.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if attrname:
|
||||||
|
# change desc
|
||||||
|
desc = self.attributes.get(attrname) or getattr(self, attrname)
|
||||||
|
else:
|
||||||
|
# use existing desc
|
||||||
|
desc = self.db.desc
|
||||||
|
|
||||||
|
if not self.db.lamp_works:
|
||||||
|
# lamp not working. Add extra to button's desc
|
||||||
|
desc += self.db.desc_add_lamp_broken or self.desc_add_lamp_broken
|
||||||
|
|
||||||
|
self.db.desc = desc
|
||||||
|
|
||||||
|
# state-changing methods and actions
|
||||||
|
|
||||||
|
def to_closed_state(self, msg=None):
|
||||||
|
"""
|
||||||
|
Switches the button to having its lid closed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
msg (str, optional): If given, display a message to the room
|
||||||
|
when lid closes.
|
||||||
|
|
||||||
|
This will first try to get the Attribute (self.db.desc_closed_lid) in
|
||||||
|
case it was set by a builder and if that was None, it will fall back to
|
||||||
|
self.desc_closed_lid, the default description (note that lack of .db).
|
||||||
|
"""
|
||||||
|
self._set_desc("desc_closed_lid")
|
||||||
|
# remove lidopen-state, if it exists
|
||||||
|
self.cmdset.remove(LidOpenCmdSet)
|
||||||
|
# add lid-closed cmdset
|
||||||
|
self.cmdset.add(LidClosedCmdSet)
|
||||||
|
|
||||||
|
if msg and self.location:
|
||||||
|
self.location.msg_contents(msg)
|
||||||
|
|
||||||
|
def to_open_state(self):
|
||||||
|
"""
|
||||||
|
Switches the button to having its lid open. This also starts a timer
|
||||||
|
that will eventually close it again.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._set_desc("desc_open_lid")
|
||||||
|
# remove lidopen-state, if it exists
|
||||||
|
self.cmdset.remove(LidClosedCmdSet)
|
||||||
|
# add lid-open cmdset
|
||||||
|
self.cmdset.add(LidOpenCmdSet)
|
||||||
|
|
||||||
|
# wait 20s then call self.to_closed_state with a message as argument
|
||||||
|
delay(35, self.to_closed_state,
|
||||||
|
self.db.auto_close_msg or self.auto_close_msg,
|
||||||
|
persistent=True)
|
||||||
|
|
||||||
|
def _unblind_target(self, caller):
|
||||||
|
"""
|
||||||
|
This is called to un-blind after a certain time.
|
||||||
|
|
||||||
|
"""
|
||||||
|
caller.cmdset.remove(BlindCmdSet)
|
||||||
|
caller.msg("You blink feverishly as your eyesight slowly returns.")
|
||||||
|
self.location.msg_contents(
|
||||||
|
f"{caller.name} seems to be recovering their eyesight, blinking feverishly.",
|
||||||
|
exclude=caller)
|
||||||
|
|
||||||
|
def blind_target(self, caller):
|
||||||
|
"""
|
||||||
|
Someone was foolish enough to press the button! Blind them
|
||||||
|
temporarily.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
caller (Object): The one to be blinded.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# we don't need to remove other cmdsets, this will replace all,
|
||||||
|
# then restore whatever was there when it goes away.
|
||||||
|
caller.cmdset.add(BlindCmdSet)
|
||||||
|
|
||||||
|
# wait 20s then call self._unblind to remove blindness effect. The
|
||||||
|
# persistent=True means the delay should survive a server reload.
|
||||||
|
delay(20, self._unblind_target, caller,
|
||||||
|
persistent=True)
|
||||||
|
|
||||||
|
def _unbreak_lamp(self):
|
||||||
|
"""
|
||||||
|
This is called to un-break the lamp after a certain time.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# we do this quietly, the user will just notice it starting blinking again
|
||||||
|
self.db.lamp_works = True
|
||||||
|
self._set_desc()
|
||||||
|
|
||||||
|
def break_lamp(self):
|
||||||
|
"""
|
||||||
|
Breaks the lamp in the button, stopping it from blinking for a while
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.db.lamp_works = False
|
self.db.lamp_works = False
|
||||||
desc = self.db.desc_lamp_broken
|
# this will update the desc with the info about the broken lamp
|
||||||
if not desc:
|
self._set_desc()
|
||||||
self.db.desc += "\nThe big red button has stopped blinking for the time being."
|
self.location.msg_contents(self.db.lamp_breaks_msg or self.lamp_breaks_msg)
|
||||||
else:
|
|
||||||
self.db.desc = desc
|
|
||||||
|
|
||||||
if feedback and self.location:
|
# wait 21s before unbreaking the lamp again
|
||||||
self.location.msg_contents("The lamp flickers, the button going dark.")
|
delay(21, self._unbreak_lamp)
|
||||||
self.scripts.validate()
|
|
||||||
|
|
||||||
def press_button(self, pobject):
|
|
||||||
"""
|
|
||||||
Someone was foolish enough to press the button!
|
|
||||||
|
|
||||||
Args:
|
|
||||||
pobject (Object): The person pressing the button
|
|
||||||
|
|
||||||
"""
|
|
||||||
# deactivate the button so it won't flash/close lid etc.
|
|
||||||
self.scripts.add(scriptexamples.DeactivateButtonEvent)
|
|
||||||
# blind the person pressing the button. Note that this
|
|
||||||
# script is set on the *character* pressing the button!
|
|
||||||
pobject.scripts.add(scriptexamples.BlindedState)
|
|
||||||
|
|
||||||
# script-related methods
|
|
||||||
|
|
||||||
def blink(self):
|
|
||||||
"""
|
|
||||||
The script system will regularly call this
|
|
||||||
function to make the button blink. Now and then
|
|
||||||
it won't blink at all though, to add some randomness
|
|
||||||
to how often the message is echoed.
|
|
||||||
"""
|
|
||||||
loc = self.location
|
|
||||||
if loc:
|
|
||||||
rand = random.random()
|
|
||||||
if rand < 0.2:
|
|
||||||
string = "The red button flashes briefly."
|
|
||||||
elif rand < 0.4:
|
|
||||||
string = "The red button blinks invitingly."
|
|
||||||
elif rand < 0.6:
|
|
||||||
string = "The red button flashes. You know you wanna push it!"
|
|
||||||
else:
|
|
||||||
# no blink
|
|
||||||
return
|
|
||||||
loc.msg_contents(string)
|
|
||||||
|
|
|
||||||
|
|
@ -1,285 +0,0 @@
|
||||||
"""
|
|
||||||
Example of scripts.
|
|
||||||
|
|
||||||
These are scripts intended for a particular object - the
|
|
||||||
red_button object type in contrib/examples. A few variations
|
|
||||||
on uses of scripts are included.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from evennia import DefaultScript
|
|
||||||
from evennia.contrib.tutorial_examples import cmdset_red_button as cmdsetexamples
|
|
||||||
|
|
||||||
#
|
|
||||||
# Scripts as state-managers
|
|
||||||
#
|
|
||||||
# Scripts have many uses, one of which is to statically
|
|
||||||
# make changes when a particular state of an object changes.
|
|
||||||
# There is no "timer" involved in this case (although there could be),
|
|
||||||
# whenever the script determines it is "invalid", it simply shuts down
|
|
||||||
# along with all the things it controls.
|
|
||||||
#
|
|
||||||
# To show as many features as possible of the script and cmdset systems,
|
|
||||||
# we will use three scripts controlling one state each of the red_button,
|
|
||||||
# each with its own set of commands, handled by cmdsets - one for when
|
|
||||||
# the button has its lid open, and one for when it is closed and a
|
|
||||||
# last one for when the player pushed the button and gets blinded by
|
|
||||||
# a bright light. The last one also has a timer component that allows it
|
|
||||||
# to remove itself after a while (and the player recovers their eyesight).
|
|
||||||
|
|
||||||
|
|
||||||
class ClosedLidState(DefaultScript):
|
|
||||||
"""
|
|
||||||
This manages the cmdset for the "closed" button state. What this
|
|
||||||
means is that while this script is valid, we add the RedButtonClosed
|
|
||||||
cmdset to it (with commands like open, nudge lid etc)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def at_script_creation(self):
|
|
||||||
"Called when script first created."
|
|
||||||
self.key = "closed_lid_script"
|
|
||||||
self.desc = "Script that manages the closed-state cmdsets for red button."
|
|
||||||
self.persistent = True
|
|
||||||
|
|
||||||
def at_start(self):
|
|
||||||
"""
|
|
||||||
This is called once every server restart, so we want to add the
|
|
||||||
(memory-resident) cmdset to the object here. is_valid is automatically
|
|
||||||
checked so we don't need to worry about adding the script to an
|
|
||||||
open lid.
|
|
||||||
"""
|
|
||||||
# All we do is add the cmdset for the closed state.
|
|
||||||
self.obj.cmdset.add(cmdsetexamples.LidClosedCmdSet)
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
"""
|
|
||||||
The script is only valid while the lid is closed.
|
|
||||||
self.obj is the red_button on which this script is defined.
|
|
||||||
"""
|
|
||||||
return not self.obj.db.lid_open
|
|
||||||
|
|
||||||
def at_stop(self):
|
|
||||||
"""
|
|
||||||
When the script stops we must make sure to clean up after us.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.obj.cmdset.delete(cmdsetexamples.LidClosedCmdSet)
|
|
||||||
|
|
||||||
|
|
||||||
class OpenLidState(DefaultScript):
|
|
||||||
"""
|
|
||||||
This manages the cmdset for the "open" button state. This will add
|
|
||||||
the RedButtonOpen
|
|
||||||
"""
|
|
||||||
|
|
||||||
def at_script_creation(self):
|
|
||||||
"Called when script first created."
|
|
||||||
self.key = "open_lid_script"
|
|
||||||
self.desc = "Script that manages the opened-state cmdsets for red button."
|
|
||||||
self.persistent = True
|
|
||||||
|
|
||||||
def at_start(self):
|
|
||||||
"""
|
|
||||||
This is called once every server restart, so we want to add the
|
|
||||||
(memory-resident) cmdset to the object here. is_valid is
|
|
||||||
automatically checked, so we don't need to worry about
|
|
||||||
adding the cmdset to a closed lid-button.
|
|
||||||
"""
|
|
||||||
self.obj.cmdset.add(cmdsetexamples.LidOpenCmdSet)
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
"""
|
|
||||||
The script is only valid while the lid is open.
|
|
||||||
self.obj is the red_button on which this script is defined.
|
|
||||||
"""
|
|
||||||
return self.obj.db.lid_open
|
|
||||||
|
|
||||||
def at_stop(self):
|
|
||||||
"""
|
|
||||||
When the script stops (like if the lid is closed again)
|
|
||||||
we must make sure to clean up after us.
|
|
||||||
"""
|
|
||||||
self.obj.cmdset.delete(cmdsetexamples.LidOpenCmdSet)
|
|
||||||
|
|
||||||
|
|
||||||
class BlindedState(DefaultScript):
|
|
||||||
"""
|
|
||||||
This is a timed state.
|
|
||||||
|
|
||||||
This adds a (very limited) cmdset TO THE ACCOUNT, during a certain time,
|
|
||||||
after which the script will close and all functions are
|
|
||||||
restored. It's up to the function starting the script to actually
|
|
||||||
set it on the right account object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def at_script_creation(self):
|
|
||||||
"""
|
|
||||||
We set up the script here.
|
|
||||||
"""
|
|
||||||
self.key = "temporary_blinder"
|
|
||||||
self.desc = "Temporarily blinds the account for a little while."
|
|
||||||
self.interval = 20 # seconds
|
|
||||||
self.start_delay = True # we don't want it to stop until after 20s.
|
|
||||||
self.repeats = 1 # this will go away after interval seconds.
|
|
||||||
self.persistent = False # we will ditch this if server goes down
|
|
||||||
|
|
||||||
def at_start(self):
|
|
||||||
"""
|
|
||||||
We want to add the cmdset to the linked object.
|
|
||||||
|
|
||||||
Note that the RedButtonBlind cmdset is defined to completly
|
|
||||||
replace the other cmdsets on the stack while it is active
|
|
||||||
(this means that while blinded, only operations in this cmdset
|
|
||||||
will be possible for the account to perform). It is however
|
|
||||||
not persistent, so should there be a bug in it, we just need
|
|
||||||
to restart the server to clear out of it during development.
|
|
||||||
"""
|
|
||||||
self.obj.cmdset.add(cmdsetexamples.BlindCmdSet)
|
|
||||||
|
|
||||||
def at_stop(self):
|
|
||||||
"""
|
|
||||||
It's important that we clear out that blinded cmdset
|
|
||||||
when we are done!
|
|
||||||
"""
|
|
||||||
self.obj.msg("You blink feverishly as your eyesight slowly returns.")
|
|
||||||
self.obj.location.msg_contents(
|
|
||||||
"%s seems to be recovering their eyesight." % self.obj.name, exclude=self.obj
|
|
||||||
)
|
|
||||||
self.obj.cmdset.delete() # this will clear the latest added cmdset,
|
|
||||||
# (which is the blinded one).
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Timer/Event-like Scripts
|
|
||||||
#
|
|
||||||
# Scripts can also work like timers, or "events". Below we
|
|
||||||
# define three such timed events that makes the button a little
|
|
||||||
# more "alive" - one that makes the button blink menacingly, another
|
|
||||||
# that makes the lid covering the button slide back after a while.
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
class CloseLidEvent(DefaultScript):
|
|
||||||
"""
|
|
||||||
This event closes the glass lid over the button
|
|
||||||
some time after it was opened. It's a one-off
|
|
||||||
script that should be started/created when the
|
|
||||||
lid is opened.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def at_script_creation(self):
|
|
||||||
"""
|
|
||||||
Called when script object is first created. Sets things up.
|
|
||||||
We want to have a lid on the button that the user can pull
|
|
||||||
aside in order to make the button 'pressable'. But after a set
|
|
||||||
time that lid should auto-close again, making the button safe
|
|
||||||
from pressing (and deleting this command).
|
|
||||||
"""
|
|
||||||
self.key = "lid_closer"
|
|
||||||
self.desc = "Closes lid on a red buttons"
|
|
||||||
self.interval = 20 # seconds
|
|
||||||
self.start_delay = True # we want to pospone the launch.
|
|
||||||
self.repeats = 1 # we only close the lid once
|
|
||||||
self.persistent = True # even if the server crashes in those 20 seconds,
|
|
||||||
# the lid will still close once the game restarts.
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
"""
|
|
||||||
This script can only operate if the lid is open; if it
|
|
||||||
is already closed, the script is clearly invalid.
|
|
||||||
|
|
||||||
Note that we are here relying on an self.obj being
|
|
||||||
defined (and being a RedButton object) - this we should be able to
|
|
||||||
expect since this type of script is always tied to one individual
|
|
||||||
red button object and not having it would be an error.
|
|
||||||
"""
|
|
||||||
return self.obj.db.lid_open
|
|
||||||
|
|
||||||
def at_repeat(self):
|
|
||||||
"""
|
|
||||||
Called after self.interval seconds. It closes the lid. Before this method is
|
|
||||||
called, self.is_valid() is automatically checked, so there is no need to
|
|
||||||
check this manually.
|
|
||||||
"""
|
|
||||||
self.obj.close_lid()
|
|
||||||
|
|
||||||
|
|
||||||
class BlinkButtonEvent(DefaultScript):
|
|
||||||
"""
|
|
||||||
This timed script lets the button flash at regular intervals.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def at_script_creation(self):
|
|
||||||
"""
|
|
||||||
Sets things up. We want the button's lamp to blink at
|
|
||||||
regular intervals, unless it's broken (can happen
|
|
||||||
if you try to smash the glass, say).
|
|
||||||
"""
|
|
||||||
self.key = "blink_button"
|
|
||||||
self.desc = "Blinks red buttons"
|
|
||||||
self.interval = 35 # seconds
|
|
||||||
self.start_delay = False # blink right away
|
|
||||||
self.persistent = True # keep blinking also after server reboot
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
"""
|
|
||||||
Button will keep blinking unless it is broken.
|
|
||||||
"""
|
|
||||||
return self.obj.db.lamp_works
|
|
||||||
|
|
||||||
def at_repeat(self):
|
|
||||||
"""
|
|
||||||
Called every self.interval seconds. Makes the lamp in
|
|
||||||
the button blink.
|
|
||||||
"""
|
|
||||||
self.obj.blink()
|
|
||||||
|
|
||||||
|
|
||||||
class DeactivateButtonEvent(DefaultScript):
|
|
||||||
"""
|
|
||||||
This deactivates the button for a short while (it won't blink, won't
|
|
||||||
close its lid etc). It is meant to be called when the button is pushed
|
|
||||||
and run as long as the blinded effect lasts. We cannot put these methods
|
|
||||||
in the AddBlindedCmdSet script since that script is defined on the *account*
|
|
||||||
whereas this one must be defined on the *button*.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def at_script_creation(self):
|
|
||||||
"""
|
|
||||||
Sets things up.
|
|
||||||
"""
|
|
||||||
self.key = "deactivate_button"
|
|
||||||
self.desc = "Deactivate red button temporarily"
|
|
||||||
self.interval = 21 # seconds
|
|
||||||
self.start_delay = True # wait with the first repeat for self.interval seconds.
|
|
||||||
self.persistent = True
|
|
||||||
self.repeats = 1 # only do this once
|
|
||||||
|
|
||||||
def at_start(self):
|
|
||||||
"""
|
|
||||||
Deactivate the button. Observe that this method is always
|
|
||||||
called directly, regardless of the value of self.start_delay
|
|
||||||
(that just controls when at_repeat() is called)
|
|
||||||
"""
|
|
||||||
# closing the lid will also add the ClosedState script
|
|
||||||
self.obj.close_lid()
|
|
||||||
# lock the lid so other accounts can't access it until the
|
|
||||||
# first one's effect has worn off.
|
|
||||||
self.obj.db.lid_locked = True
|
|
||||||
# breaking the lamp also sets a correct desc
|
|
||||||
self.obj.break_lamp(feedback=False)
|
|
||||||
|
|
||||||
def at_repeat(self):
|
|
||||||
"""
|
|
||||||
When this is called, reset the functionality of the button.
|
|
||||||
"""
|
|
||||||
# restore button's desc.
|
|
||||||
|
|
||||||
self.obj.db.lamp_works = True
|
|
||||||
desc = "This is a large red button, inviting yet evil-looking. "
|
|
||||||
desc += "Its glass cover is closed, protecting it."
|
|
||||||
self.db.desc = desc
|
|
||||||
# re-activate the blink button event.
|
|
||||||
self.obj.scripts.add(BlinkButtonEvent)
|
|
||||||
# unlock the lid
|
|
||||||
self.obj.db.lid_locked = False
|
|
||||||
self.obj.scripts.validate()
|
|
||||||
|
|
@ -1117,7 +1117,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
||||||
self.account = None
|
self.account = None
|
||||||
|
|
||||||
for script in _ScriptDB.objects.get_all_scripts_on_obj(self):
|
for script in _ScriptDB.objects.get_all_scripts_on_obj(self):
|
||||||
script.stop()
|
script.delete()
|
||||||
|
|
||||||
# Destroy any exits to and from this room, if any
|
# Destroy any exits to and from this room, if any
|
||||||
self.clear_exits()
|
self.clear_exits()
|
||||||
|
|
|
||||||
|
|
@ -104,112 +104,22 @@ class ScriptDBManager(TypedObjectManager):
|
||||||
scripts = self.get_id(dbref)
|
scripts = self.get_id(dbref)
|
||||||
for script in make_iter(scripts):
|
for script in make_iter(scripts):
|
||||||
script.stop()
|
script.stop()
|
||||||
|
|
||||||
def remove_non_persistent(self, obj=None):
|
|
||||||
"""
|
|
||||||
This cleans up the script database of all non-persistent
|
|
||||||
scripts. It is called every time the server restarts.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj (Object, optional): Only remove non-persistent scripts
|
|
||||||
assigned to this object.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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()
|
script.delete()
|
||||||
return nr_deleted
|
|
||||||
|
|
||||||
def validate(self, scripts=None, obj=None, key=None, dbref=None, init_mode=None):
|
def update_scripts_after_server_start(self):
|
||||||
"""
|
"""
|
||||||
This will step through the script database and make sure
|
Update/sync/restart/delete scripts after server shutdown/restart.
|
||||||
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.
|
|
||||||
|
|
||||||
Only one of the arguments are supposed to be supplied
|
|
||||||
at a time, since they are exclusive to each other.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
scripts (list, optional): A list of script objects to
|
|
||||||
validate.
|
|
||||||
obj (Object, optional): Validate only scripts defined on
|
|
||||||
this object.
|
|
||||||
key (str): Validate only scripts with this key.
|
|
||||||
dbref (int): Validate only the single script with this
|
|
||||||
particular id.
|
|
||||||
init_mode (str, optional): This is used during server
|
|
||||||
upstart and can have three values:
|
|
||||||
- `None` (no init mode). Called during run.
|
|
||||||
- `"reset"` - server reboot. Kill non-persistent scripts
|
|
||||||
- `"reload"` - server reload. Keep non-persistent scripts.
|
|
||||||
Returns:
|
|
||||||
nr_started, nr_stopped (tuple): Statistics on how many objects
|
|
||||||
where started and stopped.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
This method also makes sure start any scripts it validates
|
|
||||||
which should be harmless, since already-active scripts have
|
|
||||||
the property 'is_running' set and will be skipped.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
for script in self.filter(db_is_active=True, db_persistent=False):
|
||||||
|
script._stop_task()
|
||||||
|
|
||||||
# we store a variable that tracks if we are calling a
|
for script in self.filter(db_is_active=True):
|
||||||
# validation from within another validation (avoids
|
script._unpause_task(auto_unpause=True)
|
||||||
# loops).
|
script.at_server_start()
|
||||||
|
|
||||||
global VALIDATE_ITERATION
|
for script in self.filter(db_is_active=False):
|
||||||
if VALIDATE_ITERATION > 0:
|
script.at_server_start()
|
||||||
# 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:
|
|
||||||
scripts = self.get_all_scripts_on_obj(obj, key=key)
|
|
||||||
else:
|
|
||||||
scripts = self.get_all_scripts(key=key)
|
|
||||||
|
|
||||||
if not scripts:
|
|
||||||
# no scripts available to validate
|
|
||||||
VALIDATE_ITERATION -= 1
|
|
||||||
return None, None
|
|
||||||
|
|
||||||
for script in scripts:
|
|
||||||
if script.is_valid():
|
|
||||||
nr_started += script.start(force_restart=init_mode)
|
|
||||||
else:
|
|
||||||
script.stop()
|
|
||||||
nr_stopped += 1
|
|
||||||
VALIDATE_ITERATION -= 1
|
|
||||||
return nr_started, nr_stopped
|
|
||||||
|
|
||||||
def search_script(self, ostring, obj=None, only_timed=False, typeclass=None):
|
def search_script(self, ostring, obj=None, only_timed=False, typeclass=None):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ class ScriptDB(TypedObject):
|
||||||
|
|
||||||
# how often to run Script (secs). -1 means there is no timer
|
# how often to run Script (secs). -1 means there is no timer
|
||||||
db_interval = models.IntegerField(
|
db_interval = models.IntegerField(
|
||||||
"interval", default=-1, help_text="how often to repeat script, in seconds. -1 means off."
|
"interval", default=-1, help_text="how often to repeat script, in seconds. <= 0 means off."
|
||||||
)
|
)
|
||||||
# start script right away or wait interval seconds first
|
# start script right away or wait interval seconds first
|
||||||
db_start_delay = models.BooleanField(
|
db_start_delay = models.BooleanField(
|
||||||
|
|
@ -110,7 +110,7 @@ class ScriptDB(TypedObject):
|
||||||
# how many times this script is to be repeated, if interval!=0.
|
# 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.")
|
db_repeats = models.IntegerField("number of repeats", default=0, help_text="0 means off.")
|
||||||
# defines if this script should survive a reboot or not
|
# defines if this script should survive a reboot or not
|
||||||
db_persistent = models.BooleanField("survive server reboot", default=False)
|
db_persistent = models.BooleanField("survive server reboot", default=True)
|
||||||
# defines if this script has already been started in this session
|
# defines if this script has already been started in this session
|
||||||
db_is_active = models.BooleanField("script active", default=False)
|
db_is_active = models.BooleanField("script active", default=False)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,8 @@ class ScriptHandler(object):
|
||||||
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)
|
scripts = ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)
|
||||||
num = 0
|
num = 0
|
||||||
for script in scripts:
|
for script in scripts:
|
||||||
num += script.start()
|
script.start()
|
||||||
|
num += 1
|
||||||
return num
|
return num
|
||||||
|
|
||||||
def get(self, key):
|
def get(self, key):
|
||||||
|
|
@ -143,7 +144,8 @@ class ScriptHandler(object):
|
||||||
]
|
]
|
||||||
num = 0
|
num = 0
|
||||||
for script in delscripts:
|
for script in delscripts:
|
||||||
num += script.stop()
|
script.delete()
|
||||||
|
num += 1
|
||||||
return num
|
return num
|
||||||
|
|
||||||
# alias to delete
|
# alias to delete
|
||||||
|
|
@ -155,18 +157,3 @@ class ScriptHandler(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return ScriptDB.objects.get_all_scripts_on_obj(self.obj)
|
return ScriptDB.objects.get_all_scripts_on_obj(self.obj)
|
||||||
|
|
||||||
def validate(self, init_mode=False):
|
|
||||||
"""
|
|
||||||
Runs a validation on this object's scripts only. This should
|
|
||||||
be called regularly to crank the wheels.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
init_mode (str, optional): - 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.
|
|
||||||
|
|
||||||
"""
|
|
||||||
ScriptDB.objects.validate(obj=self.obj, init_mode=init_mode)
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ ability to run timers.
|
||||||
|
|
||||||
from twisted.internet.defer import Deferred, maybeDeferred
|
from twisted.internet.defer import Deferred, maybeDeferred
|
||||||
from twisted.internet.task import LoopingCall
|
from twisted.internet.task import LoopingCall
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from evennia.typeclasses.models import TypeclassBase
|
from evennia.typeclasses.models import TypeclassBase
|
||||||
from evennia.scripts.models import ScriptDB
|
from evennia.scripts.models import ScriptDB
|
||||||
|
|
@ -17,21 +16,11 @@ from evennia.utils import create, logger
|
||||||
__all__ = ["DefaultScript", "DoNothing", "Store"]
|
__all__ = ["DefaultScript", "DoNothing", "Store"]
|
||||||
|
|
||||||
|
|
||||||
FLUSHING_INSTANCES = False # whether we're in the process of flushing scripts from the cache
|
|
||||||
SCRIPT_FLUSH_TIMERS = {} # stores timers for scripts that are currently being flushed
|
|
||||||
|
|
||||||
|
|
||||||
def restart_scripts_after_flush():
|
|
||||||
"""After instances are flushed, validate scripts so they're not dead for a long period of time"""
|
|
||||||
global FLUSHING_INSTANCES
|
|
||||||
ScriptDB.objects.validate()
|
|
||||||
FLUSHING_INSTANCES = False
|
|
||||||
|
|
||||||
|
|
||||||
class ExtendedLoopingCall(LoopingCall):
|
class ExtendedLoopingCall(LoopingCall):
|
||||||
"""
|
"""
|
||||||
LoopingCall that can start at a delay different
|
Custom child of LoopingCall that can start at a delay different than
|
||||||
than `self.interval`.
|
`self.interval` and self.count=0. This allows it to support pausing
|
||||||
|
by resuming at a later period.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -49,10 +38,10 @@ class ExtendedLoopingCall(LoopingCall):
|
||||||
interval (int): Repeat interval in seconds.
|
interval (int): Repeat interval in seconds.
|
||||||
now (bool, optional): Whether to start immediately or after
|
now (bool, optional): Whether to start immediately or after
|
||||||
`start_delay` seconds.
|
`start_delay` seconds.
|
||||||
start_delay (int): The number of seconds before starting.
|
start_delay (int, optional): This only applies is `now=False`. It gives
|
||||||
If None, wait interval seconds. Only valid if `now` is `False`.
|
number of seconds to wait before starting. If `None`, use
|
||||||
It is used as a way to start with a variable start time
|
`interval` as this value instead. Internally, this is used as a
|
||||||
after a pause.
|
way to start with a variable start time after a pause.
|
||||||
count_start (int): Number of repeats to start at. The count
|
count_start (int): Number of repeats to start at. The count
|
||||||
goes up every time the system repeats. This is used to
|
goes up every time the system repeats. This is used to
|
||||||
implement something repeating `N` number of times etc.
|
implement something repeating `N` number of times etc.
|
||||||
|
|
@ -131,7 +120,7 @@ class ExtendedLoopingCall(LoopingCall):
|
||||||
of start_delay into account.
|
of start_delay into account.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
next (int or None): The time in seconds until the next call. This
|
int or None: The time in seconds until the next call. This
|
||||||
takes `start_delay` into account. Returns `None` if
|
takes `start_delay` into account. Returns `None` if
|
||||||
the task is not running.
|
the task is not running.
|
||||||
|
|
||||||
|
|
@ -139,7 +128,7 @@ class ExtendedLoopingCall(LoopingCall):
|
||||||
if self.running and self.interval > 0:
|
if self.running and self.interval > 0:
|
||||||
total_runtime = self.clock.seconds() - self.starttime
|
total_runtime = self.clock.seconds() - self.starttime
|
||||||
interval = self.start_delay or self.interval
|
interval = self.start_delay or self.interval
|
||||||
return interval - (total_runtime % self.interval)
|
return max(0, interval - (total_runtime % self.interval))
|
||||||
|
|
||||||
|
|
||||||
class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
||||||
|
|
@ -147,6 +136,8 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
||||||
Base class for scripts. Don't inherit from this, inherit from the
|
Base class for scripts. Don't inherit from this, inherit from the
|
||||||
class `DefaultScript` below instead.
|
class `DefaultScript` below instead.
|
||||||
|
|
||||||
|
This handles the timer-component of the Script.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
objects = ScriptManager()
|
objects = ScriptManager()
|
||||||
|
|
@ -157,36 +148,176 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return str(self)
|
return str(self)
|
||||||
|
|
||||||
def _start_task(self):
|
def at_idmapper_flush(self):
|
||||||
"""
|
"""
|
||||||
Start task runner.
|
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:
|
||||||
|
self.db_start_delay = 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:
|
if not self.ndb._task:
|
||||||
|
# we should have a fresh task after this point
|
||||||
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
||||||
|
|
||||||
if self.db._paused_time:
|
self._unpause_task(interval=interval, start_delay=start_delay,
|
||||||
# the script was paused; restarting
|
auto_unpause=auto_unpause,
|
||||||
callcount = self.db._paused_callcount or 0
|
old_interval=old_interval)
|
||||||
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
|
|
||||||
|
|
||||||
elif not self.ndb._task.running:
|
if not self.ndb._task.running:
|
||||||
# starting script anew
|
# 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)
|
self.ndb._task.start(self.db_interval, now=not self.db_start_delay)
|
||||||
|
|
||||||
def _stop_task(self):
|
self.at_start(**kwargs)
|
||||||
|
|
||||||
|
def _pause_task(self, auto_pause=False, **kwargs):
|
||||||
"""
|
"""
|
||||||
Stop task runner
|
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
|
||||||
|
)
|
||||||
|
del self.db._paused_time
|
||||||
|
del self.db._paused_callcount
|
||||||
|
del self.db._manually_paused
|
||||||
|
|
||||||
|
self.at_start(**kwargs)
|
||||||
|
|
||||||
|
def _stop_task(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Stop task runner and delete the task.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
task = self.ndb._task
|
task = self.ndb._task
|
||||||
if task and task.running:
|
if task and task.running:
|
||||||
task.stop()
|
task.stop()
|
||||||
self.ndb._task = None
|
self.ndb._task = None
|
||||||
|
self.db_is_active = False
|
||||||
|
|
||||||
|
# make sure this is not confused as a paused script
|
||||||
|
del self.db._paused_time
|
||||||
|
del self.db._paused_callcount
|
||||||
|
del self.db._manually_paused
|
||||||
|
|
||||||
|
self.save(update_fields=["db_is_active"])
|
||||||
|
self.at_stop(**kwargs)
|
||||||
|
|
||||||
def _step_errback(self, e):
|
def _step_errback(self, e):
|
||||||
"""
|
"""
|
||||||
|
|
@ -239,12 +370,7 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def at_script_creation(self):
|
# Access methods / hooks
|
||||||
"""
|
|
||||||
Should be overridden in child.
|
|
||||||
|
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
def at_first_save(self, **kwargs):
|
def at_first_save(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -306,12 +432,196 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
||||||
for key, value in cdict["nattributes"]:
|
for key, value in cdict["nattributes"]:
|
||||||
self.nattributes.add(key, value)
|
self.nattributes.add(key, value)
|
||||||
|
|
||||||
if not cdict.get("autostart"):
|
if cdict.get("autostart"):
|
||||||
# don't auto-start the script
|
# autostart the script
|
||||||
return
|
self._start_task(force_restart=True)
|
||||||
|
|
||||||
# auto-start script (default)
|
def delete(self):
|
||||||
self.start()
|
"""
|
||||||
|
Delete the Script. Makes sure to stop any timer tasks first.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._stop_task()
|
||||||
|
self.at_script_delete()
|
||||||
|
super().delete()
|
||||||
|
|
||||||
|
def at_script_creation(self):
|
||||||
|
"""
|
||||||
|
Should be overridden in child.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def at_script_delete(self):
|
||||||
|
"""
|
||||||
|
Called when script is deleted, after at_stop.
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
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.
|
||||||
|
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)
|
||||||
|
|
||||||
|
def update(self, interval=None, start_delay=None, repeats=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Update the Script's timer component with new settings.
|
||||||
|
|
||||||
|
Keyword Args:
|
||||||
|
interval (int): How often to fire `at_repeat` in seconds.
|
||||||
|
start_delay (int): If the start of ticking should be delayed.
|
||||||
|
repeats (int): How many repeats. 0 for infinite repeats.
|
||||||
|
**kwargs: Optional (default unused) kwargs passed on into the `at_start` hook.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._start_task(interval=interval, start_delay=start_delay,
|
||||||
|
repeats=repeats, force_restart=True, **kwargs)
|
||||||
|
|
||||||
|
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):
|
class DefaultScript(ScriptBase):
|
||||||
|
|
@ -358,287 +668,20 @@ class DefaultScript(ScriptBase):
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def time_until_next_repeat(self):
|
|
||||||
"""
|
|
||||||
Get time until the script fires it `at_repeat` hook again.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
next (int): 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:
|
|
||||||
remaining (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 at_idmapper_flush(self):
|
|
||||||
"""If we're flushing this object, make sure the LoopingCall is gone too"""
|
|
||||||
ret = super(DefaultScript, self).at_idmapper_flush()
|
|
||||||
if ret and self.ndb._task:
|
|
||||||
try:
|
|
||||||
from twisted.internet import reactor
|
|
||||||
|
|
||||||
global FLUSHING_INSTANCES
|
|
||||||
# store the current timers for the _task and stop it to avoid duplicates after cache flush
|
|
||||||
paused_time = self.ndb._task.next_call_time()
|
|
||||||
callcount = self.ndb._task.callcount
|
|
||||||
self._stop_task()
|
|
||||||
SCRIPT_FLUSH_TIMERS[self.id] = (paused_time, callcount)
|
|
||||||
# here we ensure that the restart call only happens once, not once per script
|
|
||||||
if not FLUSHING_INSTANCES:
|
|
||||||
FLUSHING_INSTANCES = True
|
|
||||||
reactor.callLater(2, restart_scripts_after_flush)
|
|
||||||
except Exception:
|
|
||||||
import traceback
|
|
||||||
|
|
||||||
traceback.print_exc()
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def start(self, force_restart=False):
|
|
||||||
"""
|
|
||||||
Called every time the script is started (for persistent
|
|
||||||
scripts, this is usually once every server start)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
force_restart (bool, optional): Normally an already
|
|
||||||
started script will not be started again. if
|
|
||||||
`force_restart=True`, the script will always restart
|
|
||||||
the script, regardless of if it has started before.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
result (int): 0 or 1 depending on if the script successfully
|
|
||||||
started or not. Used in counting.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if self.is_active and not force_restart:
|
|
||||||
# The script is already running, but make sure we have a _task if
|
|
||||||
# this is after a cache flush
|
|
||||||
if not self.ndb._task and self.db_interval > 0:
|
|
||||||
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
|
||||||
try:
|
|
||||||
start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id]
|
|
||||||
del SCRIPT_FLUSH_TIMERS[self.id]
|
|
||||||
now = False
|
|
||||||
except (KeyError, ValueError, TypeError):
|
|
||||||
now = not self.db_start_delay
|
|
||||||
start_delay = None
|
|
||||||
callcount = 0
|
|
||||||
self.ndb._task.start(
|
|
||||||
self.db_interval, now=now, start_delay=start_delay, count_start=callcount
|
|
||||||
)
|
|
||||||
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
|
|
||||||
try:
|
|
||||||
if self.unpause(manual_unpause=False):
|
|
||||||
return 1
|
|
||||||
except RuntimeError:
|
|
||||||
# manually paused.
|
|
||||||
return 0
|
|
||||||
|
|
||||||
# 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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
kill (bool, optional): - Stop the script without
|
|
||||||
calling any relevant script hooks.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
result (int): 0 if the script failed to stop, 1 otherwise.
|
|
||||||
Used in counting.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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
|
|
||||||
except ObjectDoesNotExist:
|
|
||||||
return 0
|
|
||||||
return 1
|
|
||||||
|
|
||||||
def pause(self, manual_pause=True):
|
|
||||||
"""
|
|
||||||
This stops a running script and stores its active state.
|
|
||||||
It WILL NOT call the `at_stop()` hook.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.db._manual_pause = manual_pause
|
|
||||||
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, manual_unpause=True):
|
|
||||||
"""
|
|
||||||
Restart a paused script. This WILL call the `at_start()` hook.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
manual_unpause (bool, optional): This is False if unpause is
|
|
||||||
called by the server reload/reset mechanism.
|
|
||||||
Returns:
|
|
||||||
result (bool): True if unpause was triggered, False otherwise.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
RuntimeError: If trying to automatically resart this script
|
|
||||||
(usually after a reset/reload), but it was manually paused,
|
|
||||||
and so should not the auto-unpaused.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if not manual_unpause and self.db._manual_pause:
|
|
||||||
# if this script was paused manually (by a direct call of pause),
|
|
||||||
# it cannot be automatically unpaused (e.g. by a @reload)
|
|
||||||
raise RuntimeError
|
|
||||||
|
|
||||||
# Ensure that the script is fully unpaused, so that future calls
|
|
||||||
# to unpause do not raise a RuntimeError
|
|
||||||
self.db._manual_pause = False
|
|
||||||
|
|
||||||
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 restart(self, interval=None, repeats=None, start_delay=None):
|
|
||||||
"""
|
|
||||||
Restarts an already existing/running Script from the
|
|
||||||
beginning, optionally using different settings. This will
|
|
||||||
first call the stop hooks, and then the start hooks again.
|
|
||||||
Args:
|
|
||||||
interval (int, optional): Allows for changing the interval
|
|
||||||
of the Script. Given in seconds. if `None`, will use the already stored interval.
|
|
||||||
repeats (int, optional): The number of repeats. If unset, will
|
|
||||||
use the previous setting.
|
|
||||||
start_delay (bool, optional): If we should wait `interval` seconds
|
|
||||||
before starting or not. If `None`, re-use the previous setting.
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
self.at_stop()
|
|
||||||
except Exception:
|
|
||||||
logger.log_trace()
|
|
||||||
self._stop_task()
|
|
||||||
self.is_active = False
|
|
||||||
# remove all pause flags
|
|
||||||
del self.db._paused_time
|
|
||||||
del self.db._manual_pause
|
|
||||||
del self.db._paused_callcount
|
|
||||||
# set new flags and start over
|
|
||||||
if interval is not None:
|
|
||||||
interval = max(0, interval)
|
|
||||||
self.interval = interval
|
|
||||||
if repeats is not None:
|
|
||||||
self.repeats = repeats
|
|
||||||
if start_delay is not None:
|
|
||||||
self.start_delay = start_delay
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
"""
|
"""
|
||||||
Is called to check if the script is valid to run at this time.
|
Is called to check if the script's timer is valid to run at this time.
|
||||||
Should return a boolean. The method is assumed to collect all
|
Should return a boolean. If False, the timer will be stopped.
|
||||||
needed information from its related self.obj.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return not self._is_deleted
|
return True
|
||||||
|
|
||||||
def at_start(self, **kwargs):
|
def at_start(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Called whenever the script is started, which for persistent
|
Called whenever the script timer is started, which for persistent
|
||||||
scripts is at least once every server start. It will also be
|
timed scripts is at least once every server start. It will also be
|
||||||
called when starting again after a pause (such as after a
|
called when starting again after a pause (including after a
|
||||||
server reload)
|
server reload).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
**kwargs (dict): Arbitrary, optional arguments for users
|
**kwargs (dict): Arbitrary, optional arguments for users
|
||||||
|
|
@ -658,18 +701,38 @@ class DefaultScript(ScriptBase):
|
||||||
"""
|
"""
|
||||||
pass
|
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):
|
def at_stop(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Called whenever when it's time for this script to stop (either
|
Called whenever when it's time for this script's timer to stop (either
|
||||||
because is_valid returned False or it runs out of iterations)
|
because is_valid returned False, it ran out of iterations or it was manuallys
|
||||||
|
stopped.
|
||||||
|
|
||||||
Args
|
Args:
|
||||||
**kwargs (dict): Arbitrary, optional arguments for users
|
**kwargs (dict): Arbitrary, optional arguments for users
|
||||||
overriding the call (unused by default).
|
overriding the call (unused by default).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def at_script_delete(self):
|
||||||
|
"""
|
||||||
|
Called when the Script is deleted, after at_stop().
|
||||||
|
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def at_server_reload(self):
|
def at_server_reload(self):
|
||||||
"""
|
"""
|
||||||
This hook is called whenever the server is shutting down for
|
This hook is called whenever the server is shutting down for
|
||||||
|
|
@ -686,6 +749,15 @@ class DefaultScript(ScriptBase):
|
||||||
"""
|
"""
|
||||||
pass
|
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.
|
# Some useful default Script types used by Evennia.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
# this is an optimized version only available in later Django versions
|
from unittest import TestCase, mock
|
||||||
from unittest import TestCase
|
from parameterized import parameterized
|
||||||
|
|
||||||
from evennia import DefaultScript
|
from evennia import DefaultScript
|
||||||
from evennia.scripts.models import ScriptDB, ObjectDoesNotExist
|
from evennia.scripts.models import ScriptDB, ObjectDoesNotExist
|
||||||
from evennia.utils.create import create_script
|
from evennia.utils.create import create_script
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.scripts.scripts import DoNothing
|
from evennia.scripts.scripts import DoNothing, ExtendedLoopingCall
|
||||||
|
|
||||||
|
|
||||||
class TestScript(EvenniaTest):
|
class TestScript(EvenniaTest):
|
||||||
|
|
@ -42,12 +43,45 @@ class TestScriptDB(TestCase):
|
||||||
def test_deleted_script_fails_start(self):
|
def test_deleted_script_fails_start(self):
|
||||||
"Would it ever be necessary to start a deleted script?"
|
"Would it ever be necessary to start a deleted script?"
|
||||||
self.scr.delete()
|
self.scr.delete()
|
||||||
with self.assertRaises(ObjectDoesNotExist): # See issue #509
|
with self.assertRaises(ScriptDB.DoesNotExist): # See issue #509
|
||||||
self.scr.start()
|
self.scr.start()
|
||||||
# Check the script is not recreated as a side-effect
|
# Check the script is not recreated as a side-effect
|
||||||
self.assertFalse(self.scr in ScriptDB.objects.get_all_scripts())
|
self.assertFalse(self.scr in ScriptDB.objects.get_all_scripts())
|
||||||
|
|
||||||
def test_deleted_script_is_invalid(self):
|
|
||||||
"Can deleted scripts be said to be valid?"
|
class TestExtendedLoopingCall(TestCase):
|
||||||
self.scr.delete()
|
"""
|
||||||
self.assertFalse(self.scr.is_valid()) # assertRaises? See issue #509
|
Test the ExtendedLoopingCall class.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@mock.patch("evennia.scripts.scripts.LoopingCall")
|
||||||
|
def test_start__nodelay(self, MockClass):
|
||||||
|
"""Test the .start method with no delay"""
|
||||||
|
|
||||||
|
callback = mock.MagicMock()
|
||||||
|
loopcall = ExtendedLoopingCall(callback)
|
||||||
|
loopcall.__call__ = mock.MagicMock()
|
||||||
|
loopcall._scheduleFrom = mock.MagicMock()
|
||||||
|
loopcall.clock.seconds = mock.MagicMock(return_value=0)
|
||||||
|
|
||||||
|
loopcall.start(20, now=True, start_delay=None, count_start=1)
|
||||||
|
loopcall._scheduleFrom.assert_not_called()
|
||||||
|
|
||||||
|
@mock.patch("evennia.scripts.scripts.LoopingCall")
|
||||||
|
def test_start__delay(self, MockLoopingCall):
|
||||||
|
"""Test the .start method with delay"""
|
||||||
|
|
||||||
|
callback = mock.MagicMock()
|
||||||
|
MockLoopingCall.clock.seconds = mock.MagicMock(return_value=0)
|
||||||
|
|
||||||
|
loopcall = ExtendedLoopingCall(callback)
|
||||||
|
loopcall.__call__ = mock.MagicMock()
|
||||||
|
loopcall.clock.seconds = mock.MagicMock(return_value=121)
|
||||||
|
loopcall._scheduleFrom = mock.MagicMock()
|
||||||
|
|
||||||
|
loopcall.start(20, now=False, start_delay=10, count_start=1)
|
||||||
|
|
||||||
|
loopcall.__call__.assert_not_called()
|
||||||
|
self.assertEqual(loopcall.interval , 20)
|
||||||
|
loopcall._scheduleFrom.assert_called_with(121)
|
||||||
|
|
|
||||||
|
|
@ -143,9 +143,6 @@ def _server_maintenance():
|
||||||
if _MAINTENANCE_COUNT % 300 == 0:
|
if _MAINTENANCE_COUNT % 300 == 0:
|
||||||
# check cache size every 5 minutes
|
# check cache size every 5 minutes
|
||||||
_FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE)
|
_FLUSH_CACHE(_IDMAPPER_CACHE_MAXSIZE)
|
||||||
if _MAINTENANCE_COUNT % 3600 == 0:
|
|
||||||
# validate scripts every hour
|
|
||||||
evennia.ScriptDB.objects.validate()
|
|
||||||
if _MAINTENANCE_COUNT % 3700 == 0:
|
if _MAINTENANCE_COUNT % 3700 == 0:
|
||||||
# validate channels off-sync with scripts
|
# validate channels off-sync with scripts
|
||||||
evennia.CHANNEL_HANDLER.update()
|
evennia.CHANNEL_HANDLER.update()
|
||||||
|
|
@ -431,9 +428,9 @@ class Evennia:
|
||||||
yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()]
|
yield [o.at_server_reload() for o in ObjectDB.get_all_cached_instances()]
|
||||||
yield [p.at_server_reload() for p in AccountDB.get_all_cached_instances()]
|
yield [p.at_server_reload() for p in AccountDB.get_all_cached_instances()]
|
||||||
yield [
|
yield [
|
||||||
(s.pause(manual_pause=False), s.at_server_reload())
|
(s._pause_task(auto_pause=True), s.at_server_reload())
|
||||||
for s in ScriptDB.get_all_cached_instances()
|
for s in ScriptDB.get_all_cached_instances()
|
||||||
if s.id and (s.is_active or s.attributes.has("_manual_pause"))
|
if s.id and s.is_active
|
||||||
]
|
]
|
||||||
yield self.sessions.all_sessions_portal_sync()
|
yield self.sessions.all_sessions_portal_sync()
|
||||||
self.at_server_reload_stop()
|
self.at_server_reload_stop()
|
||||||
|
|
@ -457,11 +454,9 @@ class Evennia:
|
||||||
]
|
]
|
||||||
yield ObjectDB.objects.clear_all_sessids()
|
yield ObjectDB.objects.clear_all_sessids()
|
||||||
yield [
|
yield [
|
||||||
(
|
(s._pause_task(auto_pause=True), s.at_server_shutdown())
|
||||||
s.pause(manual_pause=s.attributes.get("_manual_pause", False)),
|
|
||||||
s.at_server_shutdown(),
|
|
||||||
)
|
|
||||||
for s in ScriptDB.get_all_cached_instances()
|
for s in ScriptDB.get_all_cached_instances()
|
||||||
|
if s.id and s.is_active
|
||||||
]
|
]
|
||||||
ServerConfig.objects.conf("server_restart_mode", "reset")
|
ServerConfig.objects.conf("server_restart_mode", "reset")
|
||||||
self.at_server_cold_stop()
|
self.at_server_cold_stop()
|
||||||
|
|
@ -532,9 +527,8 @@ class Evennia:
|
||||||
|
|
||||||
TICKER_HANDLER.restore(mode == "reload")
|
TICKER_HANDLER.restore(mode == "reload")
|
||||||
|
|
||||||
# after sync is complete we force-validate all scripts
|
# Un-pause all scripts, stop non-persistent timers
|
||||||
# (this also starts any that didn't yet start)
|
ScriptDB.objects.update_scripts_after_server_start()
|
||||||
ScriptDB.objects.validate(init_mode=mode)
|
|
||||||
|
|
||||||
# start the task handler
|
# start the task handler
|
||||||
from evennia.scripts.taskhandler import TASK_HANDLER
|
from evennia.scripts.taskhandler import TASK_HANDLER
|
||||||
|
|
@ -591,7 +585,7 @@ class Evennia:
|
||||||
from evennia.scripts.models import ScriptDB
|
from evennia.scripts.models import ScriptDB
|
||||||
|
|
||||||
for script in ScriptDB.objects.filter(db_persistent=False):
|
for script in ScriptDB.objects.filter(db_persistent=False):
|
||||||
script.stop()
|
script._stop_task()
|
||||||
|
|
||||||
if GUEST_ENABLED:
|
if GUEST_ENABLED:
|
||||||
for guest in AccountDB.objects.all().filter(
|
for guest in AccountDB.objects.all().filter(
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ class ServerSessionHandler(SessionHandler):
|
||||||
sess.load_sync_data(portalsessiondata)
|
sess.load_sync_data(portalsessiondata)
|
||||||
sess.at_sync()
|
sess.at_sync()
|
||||||
# validate all scripts
|
# validate all scripts
|
||||||
_ScriptDB.objects.validate()
|
# _ScriptDB.objects.validate()
|
||||||
self[sess.sessid] = sess
|
self[sess.sessid] = sess
|
||||||
|
|
||||||
if sess.logged_in and sess.uid:
|
if sess.logged_in and sess.uid:
|
||||||
|
|
|
||||||
|
|
@ -71,10 +71,8 @@ class TestServer(TestCase):
|
||||||
) as mocks:
|
) as mocks:
|
||||||
mocks["connection"].close = MagicMock()
|
mocks["connection"].close = MagicMock()
|
||||||
mocks["ServerConfig"].objects.conf = MagicMock(return_value=100)
|
mocks["ServerConfig"].objects.conf = MagicMock(return_value=100)
|
||||||
with patch("evennia.server.server.evennia.ScriptDB.objects.validate") as mock:
|
self.server._server_maintenance()
|
||||||
self.server._server_maintenance()
|
mocks["_FLUSH_CACHE"].assert_called_with(1000)
|
||||||
mocks["_FLUSH_CACHE"].assert_called_with(1000)
|
|
||||||
mock.assert_called()
|
|
||||||
|
|
||||||
def test__server_maintenance_channel_handler_update(self):
|
def test__server_maintenance_channel_handler_update(self):
|
||||||
with patch.multiple(
|
with patch.multiple(
|
||||||
|
|
|
||||||
|
|
@ -155,12 +155,12 @@ class GlobalScriptContainer(Container):
|
||||||
new_script.start()
|
new_script.start()
|
||||||
return new_script
|
return new_script
|
||||||
|
|
||||||
if (
|
if ((found.interval != interval)
|
||||||
(found.interval != interval)
|
|
||||||
or (found.start_delay != start_delay)
|
or (found.start_delay != start_delay)
|
||||||
or (found.repeats != repeats)
|
or (found.repeats != repeats)
|
||||||
):
|
):
|
||||||
found.restart(interval=interval, start_delay=start_delay, repeats=repeats)
|
# the setup changed
|
||||||
|
found.start(interval=interval, start_delay=start_delay, repeats=repeats)
|
||||||
if found.desc != desc:
|
if found.desc != desc:
|
||||||
found.desc = desc
|
found.desc = desc
|
||||||
return found
|
return found
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,11 @@ class TestCreateScript(EvenniaTest):
|
||||||
self.repeats = 1
|
self.repeats = 1
|
||||||
self.persistent = False
|
self.persistent = False
|
||||||
|
|
||||||
# script is already stopped (interval=1, start_delay=False)
|
# script should still exist even though repeats=1, start_delay=False
|
||||||
script = create.create_script(TestScriptB, key="test_script")
|
script = create.create_script(TestScriptB, key="test_script")
|
||||||
assert script is None
|
assert script
|
||||||
|
# but the timer should be inactive now
|
||||||
|
assert not script.is_active
|
||||||
|
|
||||||
def test_create_script_w_repeats_equal_1_persisted(self):
|
def test_create_script_w_repeats_equal_1_persisted(self):
|
||||||
class TestScriptB1(DefaultScript):
|
class TestScriptB1(DefaultScript):
|
||||||
|
|
@ -45,7 +47,9 @@ class TestCreateScript(EvenniaTest):
|
||||||
|
|
||||||
# script is already stopped (interval=1, start_delay=False)
|
# script is already stopped (interval=1, start_delay=False)
|
||||||
script = create.create_script(TestScriptB1, key="test_script")
|
script = create.create_script(TestScriptB1, key="test_script")
|
||||||
assert script is None
|
assert script
|
||||||
|
assert not script.is_active
|
||||||
|
|
||||||
|
|
||||||
def test_create_script_w_repeats_equal_2(self):
|
def test_create_script_w_repeats_equal_2(self):
|
||||||
class TestScriptC(DefaultScript):
|
class TestScriptC(DefaultScript):
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
|
||||||
_EVENNIA_DIR = settings.EVENNIA_DIR
|
_EVENNIA_DIR = settings.EVENNIA_DIR
|
||||||
_GAME_DIR = settings.GAME_DIR
|
_GAME_DIR = settings.GAME_DIR
|
||||||
ENCODINGS = settings.ENCODINGS
|
ENCODINGS = settings.ENCODINGS
|
||||||
|
|
||||||
|
_TASK_HANDLER = None
|
||||||
|
_TICKER_HANDLER = None
|
||||||
|
|
||||||
_GA = object.__getattribute__
|
_GA = object.__getattribute__
|
||||||
_SA = object.__setattr__
|
_SA = object.__setattr__
|
||||||
_DA = object.__delattr__
|
_DA = object.__delattr__
|
||||||
|
|
@ -1016,7 +1020,6 @@ def uses_database(name="sqlite3"):
|
||||||
return engine == "django.db.backends.%s" % name
|
return engine == "django.db.backends.%s" % name
|
||||||
|
|
||||||
|
|
||||||
_TASK_HANDLER = None
|
|
||||||
|
|
||||||
|
|
||||||
def delay(timedelay, callback, *args, **kwargs):
|
def delay(timedelay, callback, *args, **kwargs):
|
||||||
|
|
@ -1050,12 +1053,81 @@ def delay(timedelay, callback, *args, **kwargs):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
global _TASK_HANDLER
|
global _TASK_HANDLER
|
||||||
# Do some imports here to avoid circular import and speed things up
|
|
||||||
if _TASK_HANDLER is None:
|
if _TASK_HANDLER is None:
|
||||||
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
|
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
|
||||||
|
|
||||||
return _TASK_HANDLER.add(timedelay, callback, *args, **kwargs)
|
return _TASK_HANDLER.add(timedelay, callback, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def repeat(interval, callback, persistent=True, idstring="", stop=False,
|
||||||
|
store_key=None, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Start a repeating task using the TickerHandler.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
interval (int): How often to call callback.
|
||||||
|
callback (callable): This will be called with `*args, **kwargs` every
|
||||||
|
`interval` seconds. This must be possible to pickle regardless
|
||||||
|
of if `persistent` is set or not!
|
||||||
|
persistent (bool, optional): If ticker survives a server reload.
|
||||||
|
idstring (str, optional): Separates multiple tickers. This is useful
|
||||||
|
mainly if wanting to set up multiple repeats for the same
|
||||||
|
interval/callback but with different args/kwargs.
|
||||||
|
stop (bool, optional): If set, use the given parameters to _stop_ a running
|
||||||
|
ticker instead of creating a new one.
|
||||||
|
store_key (tuple, optional): This is only used in combination with `stop` and
|
||||||
|
should be the return given from the original `repeat` call. If this
|
||||||
|
is given, all other args except `stop` are ignored.
|
||||||
|
*args, **kwargs: Used as arguments to `callback`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple or None: This is the `store_key` - the identifier for the created ticker.
|
||||||
|
Store this and pass into unrepat() in order to to stop this ticker
|
||||||
|
later. It this lost you need to stop the ticker via TICKER_HANDLER.remove
|
||||||
|
by supplying all the same arguments
|
||||||
|
directly. No return if `stop=True`
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
KeyError: If trying to stop a ticker that was not found.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global _TICKER_HANDLER
|
||||||
|
if _TICKER_HANDLER is None:
|
||||||
|
from evennia.scripts.tickerhandler import TICKER_HANDLER as _TICKER_HANDLER
|
||||||
|
|
||||||
|
if stop:
|
||||||
|
# we pass all args, but only store_key matters if given
|
||||||
|
_TICKER_HANDLER.remove(interval=interval,
|
||||||
|
callback=callback,
|
||||||
|
idstring=idstring,
|
||||||
|
persistent=persistent,
|
||||||
|
store_key=store_key)
|
||||||
|
else:
|
||||||
|
return _TICKER_HANDLER.add(interval=interval,
|
||||||
|
callback=callback,
|
||||||
|
idstring=idstring,
|
||||||
|
persistent=persistent)
|
||||||
|
|
||||||
|
def unrepeat(store_key):
|
||||||
|
"""
|
||||||
|
This is used to stop a ticker previously started with `repeat`.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
store_key (tuple): This is the return from `repeat`, used to uniquely
|
||||||
|
identify the ticker to stop.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if a ticker was stopped, False if not (for example because no
|
||||||
|
matching ticker was found or it was already stopped).
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
repeat(None, None, stop=True, store_key=store_key)
|
||||||
|
return True
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
_PPOOL = None
|
_PPOOL = None
|
||||||
_PCMD = None
|
_PCMD = None
|
||||||
_PROC_ERR = "A process has ended with a probable error condition: process ended by signal 9."
|
_PROC_ERR = "A process has ended with a probable error condition: process ended by signal 9."
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ pytz
|
||||||
djangorestframework >= 3.10.3, < 3.12
|
djangorestframework >= 3.10.3, < 3.12
|
||||||
django-filter >= 2.2.0, < 2.3
|
django-filter >= 2.2.0, < 2.3
|
||||||
django-sekizai
|
django-sekizai
|
||||||
inflect
|
inflect >= 5.2.0
|
||||||
autobahn >= 17.9.3
|
autobahn >= 17.9.3
|
||||||
lunr == 0.5.6
|
lunr == 0.5.6
|
||||||
|
|
||||||
|
|
@ -18,8 +18,9 @@ attrs >= 19.2.0
|
||||||
# testing and development
|
# testing and development
|
||||||
model_mommy
|
model_mommy
|
||||||
mock >= 1.0.1
|
mock >= 1.0.1
|
||||||
anything
|
anything==0.2.1
|
||||||
black
|
black
|
||||||
|
parameterized==0.8.1
|
||||||
|
|
||||||
# windows-specific
|
# windows-specific
|
||||||
pypiwin32;platform_system=="Windows"
|
pypiwin32;platform_system=="Windows"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue