Add chained events with persistent delays
This commit is contained in:
parent
e898ee0ec2
commit
d6c9d28d4f
4 changed files with 214 additions and 32 deletions
|
|
@ -226,13 +226,13 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
|
||||||
else:
|
else:
|
||||||
msg += "\nThis event |rhasn't been|n accepted yet."
|
msg += "\nThis event |rhasn't been|n accepted yet."
|
||||||
|
|
||||||
msg += "\nEvent code:\n "
|
msg += "\nEvent code:\n"
|
||||||
msg += "\n ".join([l for l in event["code"].splitlines()])
|
msg += "\n".join([l for l in event["code"].splitlines()])
|
||||||
self.msg(msg)
|
self.msg(msg)
|
||||||
return
|
return
|
||||||
|
|
||||||
# No parameter has been specified, display the table of events
|
# No parameter has been specified, display the table of events
|
||||||
cols = ["Number", "Author", "Updated"]
|
cols = ["Number", "Author", "Updated", "Param"]
|
||||||
if self.is_validator:
|
if self.is_validator:
|
||||||
cols.append("Valid")
|
cols.append("Valid")
|
||||||
|
|
||||||
|
|
@ -251,25 +251,28 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
|
||||||
(now - updated_on).total_seconds(), 1)
|
(now - updated_on).total_seconds(), 1)
|
||||||
else:
|
else:
|
||||||
updated_on = "|gUnknown|n"
|
updated_on = "|gUnknown|n"
|
||||||
|
parameters = event.get("parameters", "")
|
||||||
|
|
||||||
row = [str(i + 1), author, updated_on]
|
row = [str(i + 1), author, updated_on, parameters]
|
||||||
if self.is_validator:
|
if self.is_validator:
|
||||||
row.append("Yes" if event.get("valid") else "No")
|
row.append("Yes" if event.get("valid") else "No")
|
||||||
table.add_row(*row)
|
table.add_row(*row)
|
||||||
|
|
||||||
self.msg(table)
|
self.msg(table)
|
||||||
else:
|
else:
|
||||||
|
names = list(set(list(types.keys()) + list(events.keys())))
|
||||||
table = EvTable("Event name", "Number", "Description",
|
table = EvTable("Event name", "Number", "Description",
|
||||||
valign="t", width=78)
|
valign="t", width=78)
|
||||||
table.reformat_column(0, width=20)
|
table.reformat_column(0, width=20)
|
||||||
table.reformat_column(1, width=10, align="r")
|
table.reformat_column(1, width=10, align="r")
|
||||||
table.reformat_column(2, width=48)
|
table.reformat_column(2, width=48)
|
||||||
for name, infos in sorted(types.items()):
|
for name in sorted(names):
|
||||||
number = len(events.get(name, []))
|
number = len(events.get(name, []))
|
||||||
lines = sum(len(e["code"].splitlines()) for e in \
|
lines = sum(len(e["code"].splitlines()) for e in \
|
||||||
events.get(name, []))
|
events.get(name, []))
|
||||||
no = "{} ({})".format(number, lines)
|
no = "{} ({})".format(number, lines)
|
||||||
description = infos[1].splitlines()[0]
|
description = types.get(name, (None, "Chained event."))[1]
|
||||||
|
description = description.splitlines()[0]
|
||||||
table.add_row(name, no, description)
|
table.add_row(name, no, description)
|
||||||
|
|
||||||
self.msg(table)
|
self.msg(table)
|
||||||
|
|
@ -281,12 +284,12 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
|
||||||
types = self.handler.get_event_types(obj)
|
types = self.handler.get_event_types(obj)
|
||||||
|
|
||||||
# Check that the event exists
|
# Check that the event exists
|
||||||
if not event_name in types:
|
if not event_name.startswith("chain_") and not event_name in types:
|
||||||
self.msg("The event name {} can't be found in {} of " \
|
self.msg("The event name {} can't be found in {} of " \
|
||||||
"typeclass {}.".format(event_name, obj, type(obj)))
|
"typeclass {}.".format(event_name, obj, type(obj)))
|
||||||
return
|
return
|
||||||
|
|
||||||
definition = types[event_name]
|
definition = types.get(event_name, (None, "Chain event"))
|
||||||
description = definition[1]
|
description = definition[1]
|
||||||
self.msg(description)
|
self.msg(description)
|
||||||
|
|
||||||
|
|
@ -319,6 +322,7 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
# If there's only one event, just edit it
|
# If there's only one event, just edit it
|
||||||
if len(events[event_name]) == 1:
|
if len(events[event_name]) == 1:
|
||||||
|
parameters = 0
|
||||||
event = events[event_name][0]
|
event = events[event_name][0]
|
||||||
else:
|
else:
|
||||||
if not parameters:
|
if not parameters:
|
||||||
|
|
@ -343,7 +347,7 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check the definition of the event
|
# Check the definition of the event
|
||||||
definition = types[event_name]
|
definition = types.get(event_name, (None, "Chained event"))
|
||||||
description = definition[1]
|
description = definition[1]
|
||||||
self.msg(description)
|
self.msg(description)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ def get_event_handler():
|
||||||
return script
|
return script
|
||||||
|
|
||||||
def create_event_type(typeclass, event_name, variables, help_text,
|
def create_event_type(typeclass, event_name, variables, help_text,
|
||||||
custom_add=None):
|
custom_add=None, custom_call=None):
|
||||||
"""
|
"""
|
||||||
Create a new event type for a specific typeclass.
|
Create a new event type for a specific typeclass.
|
||||||
|
|
||||||
|
|
@ -41,6 +41,8 @@ def create_event_type(typeclass, event_name, variables, help_text,
|
||||||
help_text (str): a help text of the event.
|
help_text (str): a help text of the event.
|
||||||
custom_add (function, default None): a callback to call when adding
|
custom_add (function, default None): a callback to call when adding
|
||||||
the new event.
|
the new event.
|
||||||
|
custom_xcall (function, default None): a callback to call when
|
||||||
|
preparing to call the events.
|
||||||
|
|
||||||
Events obey the inheritance hierarchy: if you set an event on
|
Events obey the inheritance hierarchy: if you set an event on
|
||||||
DefaultRoom, for instance, and if your Room typeclass inherits
|
DefaultRoom, for instance, and if your Room typeclass inherits
|
||||||
|
|
@ -53,7 +55,7 @@ def create_event_type(typeclass, event_name, variables, help_text,
|
||||||
"""
|
"""
|
||||||
typeclass_name = typeclass.__module__ + "." + typeclass.__name__
|
typeclass_name = typeclass.__module__ + "." + typeclass.__name__
|
||||||
event_types.append((typeclass_name, event_name, variables, help_text,
|
event_types.append((typeclass_name, event_name, variables, help_text,
|
||||||
custom_add))
|
custom_add, custom_call))
|
||||||
|
|
||||||
def del_event_type(typeclass, event_name):
|
def del_event_type(typeclass, event_name):
|
||||||
"""
|
"""
|
||||||
|
|
@ -127,8 +129,13 @@ def connect_event_types():
|
||||||
"cannot be found.")
|
"cannot be found.")
|
||||||
return
|
return
|
||||||
|
|
||||||
for typeclass_name, event_name, variables, help_text, \
|
if script.ndb.event_types is None:
|
||||||
custom_add in event_types:
|
return
|
||||||
|
|
||||||
|
while event_types:
|
||||||
|
typeclass_name, event_name, variables, help_text, \
|
||||||
|
custom_add, custom_call = event_types[0]
|
||||||
|
|
||||||
# Get the event types for this typeclass
|
# Get the event types for this typeclass
|
||||||
if typeclass_name not in script.ndb.event_types:
|
if typeclass_name not in script.ndb.event_types:
|
||||||
script.ndb.event_types[typeclass_name] = {}
|
script.ndb.event_types[typeclass_name] = {}
|
||||||
|
|
@ -136,7 +143,8 @@ def connect_event_types():
|
||||||
|
|
||||||
# Add or replace the event
|
# Add or replace the event
|
||||||
help_text = dedent(help_text.strip("\n"))
|
help_text = dedent(help_text.strip("\n"))
|
||||||
types[event_name] = (variables, help_text, custom_add)
|
types[event_name] = (variables, help_text, custom_add, custom_call)
|
||||||
|
del event_types[0]
|
||||||
|
|
||||||
# Custom callbacks for specific events
|
# Custom callbacks for specific events
|
||||||
def get_next_wait(format):
|
def get_next_wait(format):
|
||||||
|
|
@ -213,3 +221,30 @@ def create_time_event(obj, event_name, number, parameters):
|
||||||
script.desc = "time event called regularly on {}".format(key)
|
script.desc = "time event called regularly on {}".format(key)
|
||||||
script.db.time_format = parameters
|
script.db.time_format = parameters
|
||||||
script.db.number = number
|
script.db.number = number
|
||||||
|
|
||||||
|
def keyword_event(events, parameters):
|
||||||
|
"""
|
||||||
|
Custom call for events with keywords (like say, or push, or pull, or turn...).
|
||||||
|
|
||||||
|
This function should be imported and added as a custom_call
|
||||||
|
parameter to add the event type when the event supports keywords
|
||||||
|
as parameters. Keywords in parameters are one or more words
|
||||||
|
separated by a comma. For instance, a 'push 1, one' event can
|
||||||
|
be triggered to trigger when the player 'push 1' or 'push one'.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
events (list of dict): the list of events to be called.
|
||||||
|
parameters (str): the actual parameters entered to trigger the event.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list containing the event dictionaries to be called.
|
||||||
|
|
||||||
|
"""
|
||||||
|
key = parameters.strip().lower()
|
||||||
|
to_call = []
|
||||||
|
for event in events:
|
||||||
|
keys = event["parameters"]
|
||||||
|
if not keys or key in [p.strip().lower() for p in keys.split(",")]:
|
||||||
|
to_call.append(event)
|
||||||
|
|
||||||
|
return to_call
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ Hlpers are just Python function that can be used inside of events. They
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from evennia import ObjectDB
|
from evennia import ObjectDB, ScriptDB
|
||||||
from evennia.contrib.events.exceptions import InterruptEvent
|
from evennia.contrib.events.exceptions import InterruptEvent
|
||||||
|
|
||||||
def deny():
|
def deny():
|
||||||
|
|
@ -51,3 +51,34 @@ def get(**kwargs):
|
||||||
object = None
|
object = None
|
||||||
|
|
||||||
return object
|
return object
|
||||||
|
|
||||||
|
def call(obj, event_name, seconds=0):
|
||||||
|
"""
|
||||||
|
Call the specified event in X seconds.
|
||||||
|
|
||||||
|
This helper can be used to call other events from inside of an event
|
||||||
|
in a given time. This will create a pause between events. This
|
||||||
|
will not freeze the game, and you can expect characters to move
|
||||||
|
around (unless you prevent them from doing so).
|
||||||
|
|
||||||
|
Variables that are accessible in your event using 'call()' will be
|
||||||
|
kept and passed on to the event to call.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (Object): the typeclassed object containing the event.
|
||||||
|
event_name (str): the event name to be called.
|
||||||
|
seconds (int or float): the number of seconds to wait before calling
|
||||||
|
the event.
|
||||||
|
|
||||||
|
Notice that chained events are designed for this very purpose: they
|
||||||
|
are never called automatically by the game, rather, they need to be
|
||||||
|
called from inside another event.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
script = ScriptDB.objects.get(db_key="event_handler")
|
||||||
|
except ScriptDB.DoesNotExist:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Schedule the task
|
||||||
|
script.set_task(seconds, obj, event_name)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
Scripts for the event system.
|
Scripts for the event system.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -12,7 +12,8 @@ from evennia.contrib.events.custom import connect_event_types, \
|
||||||
get_next_wait, patch_hooks
|
get_next_wait, patch_hooks
|
||||||
from evennia.contrib.events.exceptions import InterruptEvent
|
from evennia.contrib.events.exceptions import InterruptEvent
|
||||||
from evennia.contrib.events import typeclasses
|
from evennia.contrib.events import typeclasses
|
||||||
from evennia.utils.utils import all_from_module
|
from evennia.utils.dbserialize import dbserialize
|
||||||
|
from evennia.utils.utils import all_from_module, delay
|
||||||
|
|
||||||
class EventHandler(DefaultScript):
|
class EventHandler(DefaultScript):
|
||||||
|
|
||||||
|
|
@ -27,12 +28,34 @@ class EventHandler(DefaultScript):
|
||||||
self.db.events = {}
|
self.db.events = {}
|
||||||
self.db.to_valid = []
|
self.db.to_valid = []
|
||||||
|
|
||||||
|
# Tasks
|
||||||
|
self.db.task_id = 0
|
||||||
|
self.db.tasks = {}
|
||||||
|
|
||||||
def at_start(self):
|
def at_start(self):
|
||||||
"""Set up the event system."""
|
"""Set up the event system."""
|
||||||
self.ndb.event_types = {}
|
self.ndb.event_types = {}
|
||||||
connect_event_types()
|
connect_event_types()
|
||||||
patch_hooks()
|
patch_hooks()
|
||||||
|
|
||||||
|
# Generate locals
|
||||||
|
self.ndb.current_locals = {}
|
||||||
|
addresses = ["evennia.contrib.events.helpers"]
|
||||||
|
self.ndb.fresh_locals = {}
|
||||||
|
for address in addresses:
|
||||||
|
self.ndb.fresh_locals.update(all_from_module(address))
|
||||||
|
|
||||||
|
# Restart the delayed tasks
|
||||||
|
now = datetime.now()
|
||||||
|
for task_id, definition in tuple(self.db.tasks.items()):
|
||||||
|
future, obj, event_name, locals = definition
|
||||||
|
seconds = (future - now).total_seconds()
|
||||||
|
if seconds < 0:
|
||||||
|
seconds = 0
|
||||||
|
|
||||||
|
delay(seconds, complete_task, task_id)
|
||||||
|
|
||||||
|
|
||||||
def get_events(self, obj):
|
def get_events(self, obj):
|
||||||
"""
|
"""
|
||||||
Return a dictionary of the object's events.
|
Return a dictionary of the object's events.
|
||||||
|
|
@ -98,6 +121,7 @@ class EventHandler(DefaultScript):
|
||||||
"author": author,
|
"author": author,
|
||||||
"valid": valid,
|
"valid": valid,
|
||||||
"code": code,
|
"code": code,
|
||||||
|
"parameters": parameters,
|
||||||
})
|
})
|
||||||
|
|
||||||
# If not valid, set it in 'to_valid'
|
# If not valid, set it in 'to_valid'
|
||||||
|
|
@ -107,7 +131,6 @@ class EventHandler(DefaultScript):
|
||||||
# Call the custom_add if needed
|
# Call the custom_add if needed
|
||||||
custom_add = self.get_event_types(obj).get(
|
custom_add = self.get_event_types(obj).get(
|
||||||
event_name, [None, None, None])[2]
|
event_name, [None, None, None])[2]
|
||||||
print "custom_add", custom_add
|
|
||||||
if custom_add:
|
if custom_add:
|
||||||
custom_add(obj, event_name, len(events) - 1, parameters)
|
custom_add(obj, event_name, len(events) - 1, parameters)
|
||||||
|
|
||||||
|
|
@ -174,7 +197,7 @@ class EventHandler(DefaultScript):
|
||||||
if (obj, event_name, number) in self.db.to_valid:
|
if (obj, event_name, number) in self.db.to_valid:
|
||||||
self.db.to_valid.remove((obj, event_name, number))
|
self.db.to_valid.remove((obj, event_name, number))
|
||||||
|
|
||||||
def call_event(self, obj, event_name, number=None, *args):
|
def call_event(self, obj, event_name, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Call the event.
|
Call the event.
|
||||||
|
|
||||||
|
|
@ -182,7 +205,11 @@ class EventHandler(DefaultScript):
|
||||||
obj (Object): the Evennia typeclassed object.
|
obj (Object): the Evennia typeclassed object.
|
||||||
event_name (str): the event name to call.
|
event_name (str): the event name to call.
|
||||||
*args: additional variables for this event.
|
*args: additional variables for this event.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
number (int, default None): call just a specific event.
|
number (int, default None): call just a specific event.
|
||||||
|
parameters (str, default ""): call an event with parameters.
|
||||||
|
locals (dict): a locals replacement.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True to report the event was called without interruption,
|
True to report the event was called without interruption,
|
||||||
|
|
@ -190,26 +217,46 @@ class EventHandler(DefaultScript):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# First, look for the event type corresponding to this name
|
# First, look for the event type corresponding to this name
|
||||||
# To do so, go back the inheritance tree
|
number = kwargs.get("number")
|
||||||
|
parameters = kwargs.get("parameters")
|
||||||
|
locals = kwargs.get("locals")
|
||||||
|
|
||||||
|
# Errors should not pass silently
|
||||||
|
allowed = ("number", "parameters", "locals")
|
||||||
|
if any(k for k in kwargs if k not in allowed):
|
||||||
|
raise TypeError("Unknown keyword arguments were specified " \
|
||||||
|
"to call events: {}".format(kwargs))
|
||||||
|
|
||||||
event_type = self.get_event_types(obj).get(event_name)
|
event_type = self.get_event_types(obj).get(event_name)
|
||||||
if not event_type:
|
if locals is None and not event_type:
|
||||||
logger.log_err("The event {} for the object {} (typeclass " \
|
logger.log_err("The event {} for the object {} (typeclass " \
|
||||||
"{}) can't be found".format(event_name, obj, type(obj)))
|
"{}) can't be found".format(event_name, obj, type(obj)))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Prepare the locals
|
# Prepare the locals if necessary
|
||||||
locals = all_from_module("evennia.contrib.events.helpers")
|
if locals is None:
|
||||||
for i, variable in enumerate(event_type[0]):
|
locals = self.ndb.fresh_locals.copy()
|
||||||
try:
|
for i, variable in enumerate(event_type[0]):
|
||||||
locals[variable] = args[i]
|
try:
|
||||||
except IndexError:
|
locals[variable] = args[i]
|
||||||
logger.log_err("event {} of {} ({}): need variable " \
|
except IndexError:
|
||||||
"{} in position {}".format(event_name, obj,
|
logger.log_err("event {} of {} ({}): need variable " \
|
||||||
type(obj), variable, i))
|
"{} in position {}".format(event_name, obj,
|
||||||
return False
|
type(obj), variable, i))
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
locals = {key: value for key, value in locals.items()}
|
||||||
|
|
||||||
|
events = self.db.events.get(obj, {}).get(event_name, [])
|
||||||
|
|
||||||
|
# Filter down of events if there is a custom call
|
||||||
|
if event_type:
|
||||||
|
custom_call = event_type[3]
|
||||||
|
if custom_call:
|
||||||
|
events = custom_call(events, parameters)
|
||||||
|
|
||||||
# Now execute all the valid events linked at this address
|
# Now execute all the valid events linked at this address
|
||||||
events = self.db.events.get(obj, {}).get(event_name, [])
|
self.ndb.current_locals = locals
|
||||||
for i, event in enumerate(events):
|
for i, event in enumerate(events):
|
||||||
if not event["valid"]:
|
if not event["valid"]:
|
||||||
continue
|
continue
|
||||||
|
|
@ -224,6 +271,45 @@ class EventHandler(DefaultScript):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_task(self, seconds, obj, event_name):
|
||||||
|
"""
|
||||||
|
Set and schedule a task to run.
|
||||||
|
|
||||||
|
This method allows to schedule a "persistent" task.
|
||||||
|
'utils.delay' is called, but a copy of the task is kept in
|
||||||
|
the event handler, and when the script restarts (after reload),
|
||||||
|
the differed delay is called again.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
seconds (int/float): the delay in seconds from now.
|
||||||
|
obj (Object): the typecalssed object connected to the event.
|
||||||
|
event_name (str): the event's name.
|
||||||
|
|
||||||
|
Note that the dictionary of locals is frozen and will be
|
||||||
|
available again when the task runs. This feature, however,
|
||||||
|
is limited by the database: all data cannot be saved. Lambda
|
||||||
|
functions, class methods, objects inside an instance and so
|
||||||
|
on will not be kept in the locals dictionary.
|
||||||
|
|
||||||
|
"""
|
||||||
|
now = datetime.now()
|
||||||
|
delta = timedelta(seconds=seconds)
|
||||||
|
task_id = self.db.task_id
|
||||||
|
self.db.task_id += 1
|
||||||
|
|
||||||
|
# Collect and freeze current locals
|
||||||
|
locals = {}
|
||||||
|
for key, value in self.ndb.current_locals.items():
|
||||||
|
try:
|
||||||
|
dbserialize(value)
|
||||||
|
except TypeError:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
locals[key] = value
|
||||||
|
|
||||||
|
self.db.tasks[task_id] = (now + delta, obj, event_name, locals)
|
||||||
|
delay(seconds, complete_task, task_id)
|
||||||
|
|
||||||
|
|
||||||
# Script to call time-related events
|
# Script to call time-related events
|
||||||
class TimeEventScript(DefaultScript):
|
class TimeEventScript(DefaultScript):
|
||||||
|
|
@ -271,3 +357,29 @@ class TimeEventScript(DefaultScript):
|
||||||
if self.db.time_format:
|
if self.db.time_format:
|
||||||
seconds, details = get_next_wait(self.db.time_format)
|
seconds, details = get_next_wait(self.db.time_format)
|
||||||
self.restart(interval=seconds)
|
self.restart(interval=seconds)
|
||||||
|
|
||||||
|
|
||||||
|
# Functions to manipulate tasks
|
||||||
|
def complete_task(task_id):
|
||||||
|
"""
|
||||||
|
Mark the task in the event handler as complete.
|
||||||
|
|
||||||
|
This function should be called automatically for individual tasks.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_id (int): the task id.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
script = ScriptDB.objects.get(db_key="event_handler")
|
||||||
|
except ScriptDB.DoesNotExist:
|
||||||
|
logger.log_err("Can't get the event handler.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if task_id not in script.db.tasks:
|
||||||
|
logger.log_err("The task #{} was scheduled, but it cannot be " \
|
||||||
|
"found".format(task_id))
|
||||||
|
return
|
||||||
|
|
||||||
|
delta, obj, event_name, locals = script.db.tasks.pop(task_id)
|
||||||
|
script.call_event(obj, event_name, locals=locals)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue