mirror of
https://github.com/evennia/evennia.git
synced 2026-03-25 01:06:32 +01:00
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:
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue