From 51bc9ac65a1827516095d0565514e5312e406a42 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Thu, 9 Mar 2017 21:17:41 -0800 Subject: [PATCH] Add the first event on exit.at_traverse --- evennia/contrib/events/extend.py | 38 ++++++++++- evennia/contrib/events/scripts.py | 98 +++++++++++++++++++++++++++ evennia/contrib/events/typeclasses.py | 40 +++++++++++ 3 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 evennia/contrib/events/typeclasses.py diff --git a/evennia/contrib/events/extend.py b/evennia/contrib/events/extend.py index 39e5d8aaf..40242d452 100644 --- a/evennia/contrib/events/extend.py +++ b/evennia/contrib/events/extend.py @@ -9,6 +9,8 @@ and are designed to be used more by developers to add event types. from evennia import logger from evennia import ScriptDB +hooks = [] + def create_event_type(typeclass, event_name, variables, help_text): """ Create a new event type for a specific typeclass. @@ -37,9 +39,9 @@ def create_event_type(typeclass, event_name, variables, help_text): return # Get the event types for this typeclass - event_types = script.db.event_types.get(typeclass_name, {}) - if not event_types: - script.db.event_types[typeclass_name] = event_types + if typeclass_name not in script.db.event_types: + script.db.event_types[typeclass_name] = {} + event_types = script.db.event_types[typeclass_name] # Add or replace the event event_types[event_name] = (variables, help_text) @@ -70,3 +72,33 @@ def del_event_type(typeclass, event_name): event_types = script.db.event_types.get(typeclass_name, {}) if event_name in event_types: del event_types[event_name] + +def patch_hook(typeclass, method_name): + """Decorator to softly patch a hook in a typeclass.""" + hook = getattr(typeclass, method_name) + def wrapper(method): + """Wrapper around the hook.""" + def overridden_hook(*args, **kwargs): + """Function to call the new hook.""" + # Enforce the old hook as a keyword argument + kwargs["hook"] = hook + ret = method(*args, **kwargs) + return ret + hooks.append((typeclass, method_name, overridden_hook)) + return overridden_hook + return wrapper + +def patch_hooks(): + """ + Patch all the configured hooks. + + This function should be called only once when the event system + has loaded, is set and has defined its patched typeclasses. + It will be called internally by the event system, you shouldn't + call this function in your game. + + """ + while hooks: + typeclass, method_name, new_hook = hooks[0] + setattr(typeclass, method_name, new_hook) + del hooks[0] diff --git a/evennia/contrib/events/scripts.py b/evennia/contrib/events/scripts.py index 5f4af422f..6f57c6d41 100644 --- a/evennia/contrib/events/scripts.py +++ b/evennia/contrib/events/scripts.py @@ -2,7 +2,13 @@ Scripts for the event system. """ +from datetime import datetime +from Queue import Queue + from evennia import DefaultScript +from evennia import logger +from evennia.contrib.events.extend import patch_hooks +from evennia.contrib.events import typeclasses class EventHandler(DefaultScript): @@ -16,3 +22,95 @@ class EventHandler(DefaultScript): # Permanent data to be stored self.db.event_types = {} self.db.events = {} + + def at_start(self): + """Set up the event system.""" + patch_hooks() + + def add_event(self, obj, event_name, code, author=None, valid=True): + """ + Add the specified event. + + Args: + obj (Object): the Evennia typeclassed object to be modified. + event_name (str): the name of the event to add. + code (str): the Python code associated with this event. + author (optional, Character, Player): the author of the event. + valid (optional, bool): should the event be connected? + + This method doesn't check that the event type exists. + + """ + obj_events = self.db.events.get(obj, {}) + if not obj_events: + self.db.events[obj] = {} + obj_events = self.db.events[obj] + + events = obj_events.get(event_name, []) + if not events: + obj_events[event_name] = [] + events = obj_events[event_name] + + # Add the event in the list + events.append({ + "created_on": datetime.now(), + "author": author, + "valid": valid, + "code": code, + }) + + def call_event(self, obj, event_name, *args): + """ + Call the event. + + Args: + obj (Object): the Evennia typeclassed object. + event_name (str): the event name to call. + *args: additional variables for this event. + + Returns: + True to report the event was called without interruption, + False otherwise. + + """ + # First, look for the event type corresponding to this name + # To do so, go back the inheritance tree + event_type = None + event_types = self.db.event_types + classes = Queue() + classes.put(type(obj)) + while not classes.empty(): + typeclass = classes.get() + typeclass_name = typeclass.__module__ + "." + typeclass.__name__ + event_type = event_types.get(typeclass_name, {}).get(event_name) + if event_type: + break + else: + # Look for the parent classes + for parent in typeclass.__bases__: + classes.put(parent) + + # If there is still no event_type + if 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 = {} + 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 + + # Now execute all the valid events linked at this address + events = self.db.events.get(obj, {}).get(event_name, []) + for event in events: + if not event["valid"]: + continue + + exec(event["code"], locals, locals) diff --git a/evennia/contrib/events/typeclasses.py b/evennia/contrib/events/typeclasses.py new file mode 100644 index 000000000..94ed67e76 --- /dev/null +++ b/evennia/contrib/events/typeclasses.py @@ -0,0 +1,40 @@ +""" +Patched typeclasses for Evennia. +""" + +from evennia import DefaultCharacter, DefaultExit +from evennia import ScriptDB +from evennia.contrib.events.extend import create_event_type, patch_hook +from evennia.utils.utils import inherits_from + +class PatchedExit(object): + + """Patched exit to patch some hooks of DefaultExit.""" + + @staticmethod + @patch_hook(DefaultExit, "at_traverse") + def at_traverse(exit, traversing_object, target_location, hook=None): + """ + This hook is responsible for handling the actual traversal, + normally by calling + `traversing_object.move_to(target_location)`. It is normally + only implemented by Exit objects. If it returns False (usually + because `move_to` returned False), `at_after_traverse` below + should not be called and instead `at_failed_traverse` should be + called. + + Args: + traversing_object (Object): Object traversing us. + target_location (Object): Where target is going. + + """ + if inherits_from(traversing_object, DefaultCharacter): + script = ScriptDB.objects.get(db_key="event_handler") + script.call_event(exit, "at_traverse", traversing_object, + exit, exit.location) + + hook(exit, traversing_object, target_location) + +# Default events +create_event_type(DefaultExit, "at_traverse", ["character", "exit", "room"], + """When traversing""")