Add chained events with persistent delays

This commit is contained in:
Vincent Le Goff 2017-03-16 17:47:24 -07:00 committed by Griatch
parent e898ee0ec2
commit d6c9d28d4f
4 changed files with 214 additions and 32 deletions

View file

@ -226,13 +226,13 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
else:
msg += "\nThis event |rhasn't been|n accepted yet."
msg += "\nEvent code:\n "
msg += "\n ".join([l for l in event["code"].splitlines()])
msg += "\nEvent code:\n"
msg += "\n".join([l for l in event["code"].splitlines()])
self.msg(msg)
return
# No parameter has been specified, display the table of events
cols = ["Number", "Author", "Updated"]
cols = ["Number", "Author", "Updated", "Param"]
if self.is_validator:
cols.append("Valid")
@ -251,25 +251,28 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
(now - updated_on).total_seconds(), 1)
else:
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:
row.append("Yes" if event.get("valid") else "No")
table.add_row(*row)
self.msg(table)
else:
names = list(set(list(types.keys()) + list(events.keys())))
table = EvTable("Event name", "Number", "Description",
valign="t", width=78)
table.reformat_column(0, width=20)
table.reformat_column(1, width=10, align="r")
table.reformat_column(2, width=48)
for name, infos in sorted(types.items()):
for name in sorted(names):
number = len(events.get(name, []))
lines = sum(len(e["code"].splitlines()) for e in \
events.get(name, []))
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)
self.msg(table)
@ -281,12 +284,12 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
types = self.handler.get_event_types(obj)
# 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 " \
"typeclass {}.".format(event_name, obj, type(obj)))
return
definition = types[event_name]
definition = types.get(event_name, (None, "Chain event"))
description = definition[1]
self.msg(description)
@ -319,6 +322,7 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
# If there's only one event, just edit it
if len(events[event_name]) == 1:
parameters = 0
event = events[event_name][0]
else:
if not parameters:
@ -343,7 +347,7 @@ class CmdEvent(COMMAND_DEFAULT_CLASS):
return
# Check the definition of the event
definition = types[event_name]
definition = types.get(event_name, (None, "Chained event"))
description = definition[1]
self.msg(description)

View file

@ -30,7 +30,7 @@ def get_event_handler():
return script
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.
@ -41,6 +41,8 @@ def create_event_type(typeclass, event_name, variables, help_text,
help_text (str): a help text of the event.
custom_add (function, default None): a callback to call when adding
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
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__
event_types.append((typeclass_name, event_name, variables, help_text,
custom_add))
custom_add, custom_call))
def del_event_type(typeclass, event_name):
"""
@ -127,8 +129,13 @@ def connect_event_types():
"cannot be found.")
return
for typeclass_name, event_name, variables, help_text, \
custom_add in event_types:
if script.ndb.event_types is None:
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
if typeclass_name not in script.ndb.event_types:
script.ndb.event_types[typeclass_name] = {}
@ -136,7 +143,8 @@ def connect_event_types():
# Add or replace the event
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
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.db.time_format = parameters
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

View file

@ -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
def deny():
@ -51,3 +51,34 @@ def get(**kwargs):
object = None
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)

View file

@ -2,7 +2,7 @@
Scripts for the event system.
"""
from datetime import datetime
from datetime import datetime, timedelta
from Queue import Queue
from django.conf import settings
@ -12,7 +12,8 @@ from evennia.contrib.events.custom import connect_event_types, \
get_next_wait, patch_hooks
from evennia.contrib.events.exceptions import InterruptEvent
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):
@ -27,12 +28,34 @@ class EventHandler(DefaultScript):
self.db.events = {}
self.db.to_valid = []
# Tasks
self.db.task_id = 0
self.db.tasks = {}
def at_start(self):
"""Set up the event system."""
self.ndb.event_types = {}
connect_event_types()
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):
"""
Return a dictionary of the object's events.
@ -98,6 +121,7 @@ class EventHandler(DefaultScript):
"author": author,
"valid": valid,
"code": code,
"parameters": parameters,
})
# If not valid, set it in 'to_valid'
@ -107,7 +131,6 @@ class EventHandler(DefaultScript):
# Call the custom_add if needed
custom_add = self.get_event_types(obj).get(
event_name, [None, None, None])[2]
print "custom_add", custom_add
if custom_add:
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:
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.
@ -182,7 +205,11 @@ class EventHandler(DefaultScript):
obj (Object): the Evennia typeclassed object.
event_name (str): the event name to call.
*args: additional variables for this event.
Kwargs:
number (int, default None): call just a specific event.
parameters (str, default ""): call an event with parameters.
locals (dict): a locals replacement.
Returns:
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
# 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)
if not event_type:
if locals is None and not event_type:
logger.log_err("The event {} for the object {} (typeclass " \
"{}) can't be found".format(event_name, obj, type(obj)))
return False
# Prepare the locals
locals = all_from_module("evennia.contrib.events.helpers")
for i, variable in enumerate(event_type[0]):
try:
locals[variable] = args[i]
except IndexError:
logger.log_err("event {} of {} ({}): need variable " \
"{} in position {}".format(event_name, obj,
type(obj), variable, i))
return False
# Prepare the locals if necessary
if locals is None:
locals = self.ndb.fresh_locals.copy()
for i, variable in enumerate(event_type[0]):
try:
locals[variable] = args[i]
except IndexError:
logger.log_err("event {} of {} ({}): need variable " \
"{} in position {}".format(event_name, obj,
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
events = self.db.events.get(obj, {}).get(event_name, [])
self.ndb.current_locals = locals
for i, event in enumerate(events):
if not event["valid"]:
continue
@ -224,6 +271,45 @@ class EventHandler(DefaultScript):
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
class TimeEventScript(DefaultScript):
@ -271,3 +357,29 @@ class TimeEventScript(DefaultScript):
if self.db.time_format:
seconds, details = get_next_wait(self.db.time_format)
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)