From 96b90dde1eed3a8b0695f5480c05965aeec1d3f4 Mon Sep 17 00:00:00 2001 From: Vincent Le Goff Date: Sat, 22 Apr 2017 13:15:16 -0700 Subject: [PATCH] Adopt the new names for the event system --- evennia/contrib/events/USERDOC.md | 332 ------ evennia/contrib/events/commands.py | 452 ++++---- evennia/contrib/events/custom.py | 265 ----- .../events/{helpers.py => eventfuncs.py} | 4 +- evennia/contrib/events/handler.py | 166 +-- evennia/contrib/events/scripts.py | 403 +++---- evennia/contrib/events/tests.py | 336 +++--- evennia/contrib/events/typeclasses.py | 980 +++++++++--------- evennia/contrib/events/utils.py | 235 +++++ 9 files changed, 1433 insertions(+), 1740 deletions(-) delete mode 100644 evennia/contrib/events/USERDOC.md delete mode 100644 evennia/contrib/events/custom.py rename evennia/contrib/events/{helpers.py => eventfuncs.py} (95%) create mode 100644 evennia/contrib/events/utils.py diff --git a/evennia/contrib/events/USERDOC.md b/evennia/contrib/events/USERDOC.md deleted file mode 100644 index c333cf5d96..0000000000 --- a/evennia/contrib/events/USERDOC.md +++ /dev/null @@ -1,332 +0,0 @@ -# Evennia's event system, user documentation - -Evennia's event system allows to add dynamic features in your world without editing the source code. These features are placed on individual objects, and can offer opportunities to customize a few objects without customizing all of them. Usages can range from: - -- Adding dialogues to some characters (a NPC greeting player-characters). -- Adding some custom actions at specific in-game moments (a shop-keeper going home at 8 PM and coming back to the shop in the morning). -- Build complex quests (a set of actions with conditions required to obtain some reward or advantage). -- Deny a command from executing based on some conditions (prevent a character from going in some room without completing some quest). -- Have some objects react in specific ways when some action occurs (a character enters the room, a character says something). - -In short, the event system allows what other engines would implement through soft code or "scripting". The event system in Evennia doesn't rely on a homemade language, however, but on Python, and therefore allows almost everything possible through modifications to the source code. It's not necessary to know Evennia to use the event system, although knowing some basis of Evennia (the system of typeclasses and attributes, for instance) will not hurt. - -## Some basic examples - -Before beginning to use this system, it might be worth understanding its possibilities and basic features. The event system allows to create events that can be fired at specific moments. For instance, checking beforehand if a character has some characteristics before allowing him/her to walk through an exit. You will find some examples here (of course, this is only a list of examples, you could do so much more through this system): - - Edit the event 'can_traverse' of a specific exit: - if character.db.health < 30: - character.msg("You are obviously too weak to do that.") - deny() - else: # That's really opional here, but why not? - character.msg("Alrigh, you can go.") - -The `deny()` function denies characters from moving and so, after the message has been sent, the action is cancelled (he/she doesn't move). The `else:` statement and instructions are, as in standard Python, optional here. - - Edit the event 'eat' of a specific object: - if character.db.race != "goblin": - character.msg("This is a nice-tasting apple, as juicy as you'd like.") - else: - character.msg("You bite into the apple... and spit it out! Do people really eat that?!") - character.db.health -= 10 - -This time, we have an event that behaves differently when a character eats an apple... and is a goblin, or something else. Notice that the race system will need to be in your game, the event system just provides ways to access your regular Evennia objects and attributes. - - Edit the event 'time' of a specific NPC with the parameter '19:45': - character.execute_cmd("say Well, it's time to go home, folks!") - exit = character.location.search("up") - - exit.db.lock = False - exit.db.closed = False - move(character, "up") - exit.db.closed = True - exit.db.lock = True - -For this example, at 19:45 sharp (game time), the NPC leaves. It can be useful for a shop-keeper to just go in his/her room to sleep, and comeback in the morning. - -You will find more examples in this documentation, along with clear indications on how to use this feature in context. - -## Basic usage - -The event system relies, to a great extent, on its `@event` command. By default, immortals will be the only ones to have access to this command, for obvious security reasons. - -### The `@event` command - -The event system can be used on most Evennia objects, mostly typeclassed objects (rooms, exits, characters, objects, and the ones you want to add to your game, players don't use this system however). The first argument of the `@event` command is the name of the object you want to edit. - -#### Examining events - -Let's say we are in a room with two exist, north and south. You could see what events are currently linked with the `north` exit by entering: - - @event north - -The object to display or edit is searched in the room, by default, which makes editing rather easy. However, you can also provide its DBREF (a number) after a `#` sign, like this: - - @event #1 - -(In most settings, this will show the events linked with the character 1, the superuser.) - -This command will display a table, containing: - -- The name of each event in the first column. -- The number of events of this name, and the number of total lines of these events in the second column. -- A short help to tell you when the event is triggered in the third column. - -Notice that several events can be linked at the same location. For instance, you can have several events in an exit's "can_traverse" event: each event will be called in the order and each can prevent the character from going elsewhere. - -You can see the list of events of each name by using the same command, specifying the name of the event after an equal sign: - - @event south = can_traverse - -If you have more than one event of this name, they will be shown in a table with numbers starting from 1. You can examine a specific event by providing the number after the event's name: - - @event south = can_traverse 1 - -This command will allow you to examine the event more closely, including seeing its associated code. - -#### Creating a new event - -The `/add` switch should be used to add an event. It takes two arguments beyond the object's name/DBREF: - -1. After an = sign, the event to be edited (if not supplied, will display the list of possible events). -2. The parameters (optional). - -We'll see events with parameters later. For now, let's create an event 'can_traverse' connected to the exit 'north' in this room: - - @event/add north = can_traverse - -This will create a new event connected to this exit. It will be fired before a character traverses this exit. It is possible to prevent the character from moving at this point. - -This command should open a line-editor. This editor is described in greater details in another section. For now, you can write instructions as normal: - - if character.id == 1: - character.msg("You're the superuser, 'course I'll let you pass.") - else: - character.msg("Hold on, what do you think you're doing?") - deny() - -You can now enter `:wq` to leave the editor by saving the event. - -Then try to walk through this exit. Do it with another character if possible, too, to see the difference. - -#### Editing an event - -You can use the `/edit` switch to the `@event` command to edit an event. You should provide, after the name of the object to edit and the equal sign: - -1. The name of the event (as seen above). -2. A number, if several events are connected at this location. - -You can type `@event/edit = ` to see the events that are linked at this location. If there is only one event, it will be opened in the editor; if more are defined, you will be asked for a number to provide (for instance, `@event/edit north = can_traverse 2`). - -#### Removing an event - -The command `@event` also provides a `/del` switch to remove an event. It takes the same arguments as the `/edit` switch: - -1. The name of the object. -2. The name of the event after an = sign. -3. Optionally a number if more than one event are located there. - -When removed, events are logged, so an administrator can retrieve its content, assuming the `/del` was an error and the administrator has access to log files. - -### The event editor - -When adding or editing an event, the event editor should open. It is basically the same as [EvEditor](https://github.com/evennia/evennia/wiki/EvEditor), which so ressemble VI, but it adds a couple of options to handle indentation. - -Python is a programming language that needs correct indentation. It is not an aesthetic concern, but a requirement to differentiate between blocks. The event editor will try to guess the right level of indentation to make your life easier, but it will not be perfect. - -- If you enter an instruction beginning by `if`, `elif`, or `else`, the editor will automatically increase the level of indentation of the next line. -- If the instruction is an `elif` or `else`, the editor will look for the opening block of `if` and match indentation. -- Blocks `while`, `for`, `try`, `except`, 'finally' obey the same rules. - -There are still some cases when you must tell the editor to reduce or increase indentation. The usual use cases are: - -1. When you close a condition or loop, the editor will not be able to tell. -2. When you want to keep the instruction on several lines, the editor will not bother with indentation. - -In both cases, you should use the `:>` command (increase indentation by one level) and `:<` (decrease indentation by one level). Indentation is always shown when you add a new line in your event. - -In all the cases shown above, you don't need to enter your indentation manually. Just change the indentation whenever needed, don't bother to write spaces or tabulations at the beginning of your line. For instance, you could enter the following lines in your client: - -``` -if character.id == 1: -character.msg("You're the big boss.") -else: -character.msg("I don't know who you are.") -:< -character.msg("This is not inside of the condition.") -``` - -This will produce the following code: - -``` -if character.id == 1: - character.msg("You're the big boss.") -else: - character.msg("I don't know who you are.") - -character.msg("This is not inside of the condition.") -``` - -You can also disable the automatic-indentation mode. Just enter the command `:=`. In this mode, you will have to manually type in the spaces or tabulations, the editor will not indent anything without you asking to do it. This mode can be useful if you copy/paste some code and want to keep the original indentation. - -## Using events - -The following sub-sections describe how to use events for various tasks, from the most simple to the most complex. - -### Standard Python code in events - -This might sound superfluous, considering the previous explanations, but remember you can use standard Python code in your events. Everything that you could do in the source code itself, like changing attributes or aliases, creating or removing objects, can be done through this system. What you will see in the following sub-sections doesn't rely on a new syntax of Python: they add functions and some features, at the best. Events aren't written in softcode, and their syntax might, at first glance, be a bit unfriendly to a user without any programming skills. However, you will probably grasp the basic concept very quickly, and will be able to move beyond simple events in good time. Don't overlook examples, in this documentation, or in your game. - -### The helper functions - -In order to make development a little easier, the event system provides helper functions to be used in events themselves. You don't have to use them, they are just shortcuts. - -The `deny()` function is such a helper. It allows to interrupt the event and the action that called it. In the `can_*` events, it can be used to prevent the action from happening. For instance, in `can_traverse` on exits, it can prevent the user from moving in that direction. One could have a `can_eat` event set on food that would prevent this character from eating this food. Or a `can_say` event in a room that would prevent the character from saying something here. - -Behind the scene, the `deny()` function raises an exception that is being intercepted by the handler of events. Calling this function in events that cannot be stopped may result in errors. - -You could easily add other helper functions. This will greatly depend on the objects you have defined in your game, and how often specific features have to be used by event users. - -### Variables in events - -Most events have variables. Variables are just Python variables. As you've seen in the previous example, when we manipulate characters or character actions, we often have a `character` variable that holds the character doing the action. The list of variables can change between events, and is always available in the help of the event. When you edit or add a new event, you'll see the help: read it carefully until you're familiar with this event, since it will give you useful information beyond the list of variables. - -Sometimes, variables in events can also be set to contain new directions. One simple example is the exits' "msg_leave" event, that is called when a character leaves a room through this exit. This event is executed and you can set a custom message when a character walks through this exit, which can sometimes be useful: - - @event/add down = msg_leave - message = "{character} falls into a hole in the ground!" - -Then, if the character Wilfred takes this story, others in the room will see: - - Wildred falls into a hole in the ground! - -### Events with parameters - -Some events are called without parameter. For instance, when a character traverses through an exit, the exit's "traverse" event is called with no argument. In some cases, you can create events that are triggered under only some conditions. A typical example is the room's "say" event. This event is triggered when somebody says something in the room. The event can be configured to fire only when some words are used in the sentence. - -For instance, let's say we want to create a cool voice-operated elevator. You enter into the elevator and say the floor number... and the elevator moves in the right direction. In this case, we could create an event with the parameter "one": - - @event/add here = say one - -This event will only fire when the user says "one" in this room. - -But what if we want to have an event that would fire if the user says 1 or one? We can provide several parameters, separated by a comma. - - @event/add here = say 1, one - -Or, still more keywords: - - @event/add here = say 1, one, ground - -This time, the user could say "ground" or "one" in the room, and it would fire the event. - -Not all events can take parameters, and these who do have a different ways of handling them. There isn't a single meaning to parameters that could apply to all events. Refer to the event documentation for details. - -### Time-related events - -Events are usually linked to commands. As we saw before, however, this is not always the case. Events can be triggered by other actions and, as we'll see later, could even be called from inside other events! - -There is a specific event, on all objects, that can trigger at a specific time. It's an event with a mandatory argument, which is the time you expect this event to fire. - -For instance, let's add an event on this room that should trigger every day, at precisely 12:00 PM (the time is given as game time, not real time): - -``` -@event here = time 12:00 -# This will be called every MUD day at 12:00 PM -room.msg_contents("It's noon, time to have lunch!") -``` - -Now, at noon every MUD day, this event will fire. You can use this event on every kind of typeclassed object, to have a specific action done every MUD day at the same time. - -Time-related events can be much more complex than this. They can trigger every in-game hour or more often (it might not be a good idea to have events trigger that often on a lot of objects). You can have events that run every in-game week or month or year. It will greatly vary depending on the type of calendar used in your game. The number of time units is described in the game configuration. - -With a standard calendar, for instance, you have the following units: minutes, hours, days, months and years. You will specify them as numbers separated by either a colon (:), a space ( ), or a dash (-). Pick whatever feels more appropriate (usually, we separate hours and minutes with a colon, the other units with a dash). - -Some examples of syntax: - -- `18:30`: every day at 6:30 PM. -- `01 12:00`: every month, the first day, at 12 PM. -- `06-15 09:58`: every year, on the 15th of June (month comes before day), at 9:58 AM. -- `2025-01-01 00:00`: January 1st, 2025 at midnight (obviously, this will trigger only once). - -Notice that we specify units in the reverse order (year, month, day, hour and minute) and separate them with logical separators. The smallest unit that is not defined is going to set how often the event should fire. That's why, if you use `12:00`, the smallest unit that is not defined is "day": the event will fire every day at the specific time. - -> You can use chained events (see below) in conjunction with time-related events to create more random or frequent actions in events. - -### Chained events - -Events can call other events, either now or a bit later. It is potentially very powerful. - -To use chained events, just use the `call` helper function. It takes 2-3 arguments: - -- The object containing the event. -- The name of the event to call. -- Optionally, the number of seconds to wait before calling this event. - -All objects have events that are not triggered by commands or game-related operations. They are called "chain_X", like "chain_1", "chain_2", "chain_3" and so on. You can give them more specific names, as long as it begins by "chain_", like "chain_flood_room". - -Rather than a long explanation, let's look at an example: a subway that will go from one place to the next at regular times. Creating exits (opening its doors), waiting a bit, closing them, rolling around and stopping at a different station. That's quite a complex set of events, as it is, but let's only look at the part that opens and closes the doors: - -``` -@event here = time 10:00 -# At 10:00 AM, the subway arrives in the room of ID 22. -# Notice that exit #23 and #24 are respectively the exit leading -# on the platform and back in the subway. -station = get(id=22) -# Open the door -to_exit = get(id=23) -to_exit.name = "platform" -to_exit.aliases = ["p"] -to_exit.location = room -to_exit.destination = station -# Create the return exit -back_exit = get(id=24) -back_exit.name = "subway" -back_exit.location = station -back_exit.destination = room -# Display some messages -room.msg_contents("The doors open and wind gushes in the subway") -station.msg_contents("The doors of the subway open with a dull clank.") -# Set the doors to close in 20 seconds -call(room, "chain_1", 20) -``` - -This event will: - -1. Be called at 10:00 AM (specify 22:00 to say 10:00 PM). -2. Set an exit between the subway and the station. Notice that the exits already exist (you will have to create them), but they don't need to have specific location and destination. -3. Display a message both in the subway and on the platform. -4. Call the event "chain_1" to execute in 20 seconds. - -And now, what should we have in "chain_1"? - -``` -@event here = chain_1 -# Close the doors -to_exit.location = None -to_exit.destination = None -back_exit.location = None -back_exit.destination = None -room.msg_content("After a short warning signal, the doors close and the subway begins moving.") -station.msg_content("After a short warning signal, the doors close and the subway begins moving.") -``` - -Behind the scene, the `call` function freezes all variables ("room", "station", "to_exit, "back_exit" in our example), so you don't need to define them afterward. - -A word of caution on events that call chained events: it isn't impossible for an event to call itself at some recursion level. If `chain_1` calls `chain_2` that calls `chain_3` that calls `chain_`, particularly if there's no pause between them, you might run into an infinite loop. - -Be also careful when it comes to handling characters or objects that may very well move during your pause between event calls. When you use `call()`, the MUD doesn't pause and commands can be entered by players, fortunately. It also means that, a character could start an event that pauses for awhile, but be gone when the chained event is called. You need to check that, even lock the character into place while you are pausing (some actions should require locking) or at least, checking that the character is still in the room, for it might create illogical situations if you don't. - -## Errors in events - -There are a lot of ways to make mistakes while writing events. Once you begin, you might encounter syntax errors very often, but leave them behind as you gain in confidence. However, there are still so many ways to trigger errors: passing the wrong arguments to a helper function is only one of many possible examples. - -When an event encounters an error, it stops abruptly and sends the error on a special channel, named "everror", on which you can connect or disconnect should the amount of information be overwhelming. These error messages will contain: - -- The name and ID of the object that encountered the error. -- The name and number of the event that crashed. -- The line number (and code) that caused the error. -- The short error messages (it might not be that short at times). - -The error will also be logged, so an administrator can still access it more completely, seeing the full traceback, which can help to understand the error sometimes. - diff --git a/evennia/contrib/events/commands.py b/evennia/contrib/events/commands.py index f171e17994..4f92958433 100644 --- a/evennia/contrib/events/commands.py +++ b/evennia/contrib/events/commands.py @@ -1,5 +1,5 @@ """ -Module containing the commands of the event system. +Module containing the commands of the callback system. """ from datetime import datetime @@ -10,77 +10,77 @@ from evennia.utils.ansi import raw from evennia.utils.eveditor import EvEditor from evennia.utils.evtable import EvTable from evennia.utils.utils import class_from_module, time_format -from evennia.contrib.events.custom import get_event_handler +from evennia.contrib.events.utils import get_event_handler COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) # Permissions -WITH_VALIDATION = getattr(settings, "EVENTS_WITH_VALIDATION", None) -WITHOUT_VALIDATION = getattr(settings, "EVENTS_WITHOUT_VALIDATION", +WITH_VALIDATION = getattr(settings, "callbackS_WITH_VALIDATION", None) +WITHOUT_VALIDATION = getattr(settings, "callbackS_WITHOUT_VALIDATION", "immortals") -VALIDATING = getattr(settings, "EVENTS_VALIDATING", "immortals") +VALIDATING = getattr(settings, "callbackS_VALIDATING", "immortals") # Split help text -BASIC_HELP = "Add, edit or delete events." +BASIC_HELP = "Add, edit or delete callbacks." BASIC_USAGES = [ - "@event [= ]", - "@event/add = [parameters]", - "@event/edit = [event number]", - "@event/del = [event number]", - "@event/tasks [object name [= ]]", + "@call [= ]", + "@call/add = [parameters]", + "@call/edit = [callback number]", + "@call/del = [callback number]", + "@call/tasks [object name [= ]]", ] BASIC_SWITCHES = [ - "add - add and edit a new event", - "edit - edit an existing event", - "del - delete an existing event", + "add - add and edit a new callback", + "edit - edit an existing callback", + "del - delete an existing callback", "tasks - show the list of differed tasks", ] VALIDATOR_USAGES = [ - "@event/accept [object name = [event number]]", + "@call/accept [object name = [callback number]]", ] VALIDATOR_SWITCHES = [ - "accept - show events to be validated or accept one", + "accept - show callbacks to be validated or accept one", ] BASIC_TEXT = """ -This command is used to manipulate events. An event can be linked to +This command is used to manipulate callbacks. A callback can be linked to an object, to fire at a specific moment. You can use the command without -switches to see what event are active on an object: - @event self -You can also specify an event name if you want the list of events associated -with this object of this name: - @event north = can_traverse -You can also add a number after the event name to see details on one event: - @event here = say 2 -You can also add, edit or remove events using the add, edit or del switches. -Additionally, you can see the list of differed tasks created by events -(chained events to be called) using the /tasks switch. +switches to see what callbacks are active on an object: + @call self +You can also specify a callback name if you want the list of callbacks +associated with this object of this name: + @call north = can_traverse +You can also add a number after the callback name to see details on one callback: + @call here = say 2 +You can also add, edit or remove callbacks using the add, edit or del switches. +Additionally, you can see the list of differed tasks created by callbacks +(chained callbacks to be called) using the /tasks switch. """ VALIDATOR_TEXT = """ -You can also use this command to validate events. Depending on your game -setting, some users might be allowed to add new events, but these events -will not be fired until you accept them. To see the events needing +You can also use this command to validate callbacks. Depending on your game +setting, some users might be allowed to add new callbacks, but these callbacks +will not be fired until you accept them. To see the callbacks needing validation, enter the /accept switch without argument: - @event/accept -A table will show you the events that are not validated yet, who created -them and when. You can then accept a specific event: - @event here = enter 1 -Use the /del switch to remove events that should not be connected. + @call/accept +A table will show you the callbacks that are not validated yet, who created +them and when. You can then accept a specific callback: + @call here = enter 1 +Use the /del switch to remove callbacks that should not be connected. """ -class CmdEvent(COMMAND_DEFAULT_CLASS): +class CmdCallback(COMMAND_DEFAULT_CLASS): """ - Command to edit events. + Command to edit callbacks. """ - key = "@event" - aliases = ["@events", "@ev"] + key = "@call" + aliases = ["@callback", "@callbacks", "@calls"] locks = "cmd:perm({})".format(VALIDATING) if WITH_VALIDATION: locks += " or perm({})".format(WITH_VALIDATION) @@ -101,7 +101,7 @@ class CmdEvent(COMMAND_DEFAULT_CLASS): docstring (str): the help text to provide the caller for this command. """ - lock = "perm({}) or perm(events_validating)".format(VALIDATING) + lock = "perm({}) or perm(callbacks_validating)".format(VALIDATING) validator = caller.locks.check_lockstring(caller, lock) text = "\n" + BASIC_HELP + "\n\nUsages:\n " @@ -132,12 +132,12 @@ class CmdEvent(COMMAND_DEFAULT_CLASS): WITHOUT_VALIDATION) autovalid = caller.locks.check_lockstring(caller, lock) - # First and foremost, get the event handler and set other variables + # First and foremost, get the callback handler and set other variables self.handler = get_event_handler() self.obj = None rhs = self.rhs or "" - self.event_name, sep, self.parameters = rhs.partition(" ") - self.event_name = self.event_name.lower() + self.callback_name, sep, self.parameters = rhs.partition(" ") + self.callback_name = self.callback_name.lower() self.is_validator = validator self.autovalid = autovalid if self.handler is None: @@ -158,75 +158,73 @@ class CmdEvent(COMMAND_DEFAULT_CLASS): return if switch == "": - self.list_events() + self.list_callbacks() elif switch == "add": - self.add_event() + self.add_callback() elif switch == "edit": - self.edit_event() + self.edit_callback() elif switch == "del": - self.del_event() + self.del_callback() elif switch == "accept" and validator: - self.accept_event() + self.accept_callback() elif switch in ["tasks", "task"]: self.list_tasks() else: caller.msg("Mutually exclusive or invalid switches were " \ "used, cannot proceed.") - def list_events(self): - """Display the list of events connected to the object.""" + def list_callbacks(self): + """Display the list of callbacks connected to the object.""" obj = self.obj - event_name = self.event_name + callback_name = self.callback_name parameters = self.parameters - events = self.handler.get_events(obj) - types = self.handler.get_event_types(obj) + callbacks = self.handler.get_callbacks(obj) + types = self.handler.get_events(obj) - if event_name: - # Check that the event name can be found in this object - created = events.get(event_name) + if callback_name: + # Check that the callback name can be found in this object + created = callbacks.get(callback_name) if created is None: - self.msg("No event {} has been set on {}.".format(event_name, + self.msg("No callback {} has been set on {}.".format(callback_name, obj)) return if parameters: - # Check that the parameter points to an existing event + # Check that the parameter points to an existing callback try: number = int(parameters) - 1 assert number >= 0 - event = events[event_name][number] + callback = callbacks[callback_name][number] except (ValueError, AssertionError, IndexError): - self.msg("The event {} {} cannot be found in {}.".format( - event_name, parameters, obj)) + self.msg("The callback {} {} cannot be found in {}.".format( + callback_name, parameters, obj)) return - # Display the events' details - author = event.get("author") + # Display the callback's details + author = callback.get("author") author = author.key if author else "|gUnknown|n" - updated_by = event.get("updated_by") + updated_by = callback.get("updated_by") updated_by = updated_by.key if updated_by else "|gUnknown|n" - created_on = event.get("created_on") - created_on = created_on.strftime("%Y-%m-%d %H:%M:%S") \ - if created_on else "|gUnknown|n" - updated_on = event.get("updated_on") - updated_on = updated_on.strftime("%Y-%m-%d %H:%M:%S") \ - if updated_on else "|gUnknown|n" - msg = "Event {} {} of {}:".format(event_name, parameters, obj) + created_on = callback.get("created_on") + created_on = created_on.strftime("%Y-%m-%d %H:%M:%S") if created_on else "|gUnknown|n" + updated_on = callback.get("updated_on") + updated_on = updated_on.strftime("%Y-%m-%d %H:%M:%S") if updated_on else "|gUnknown|n" + msg = "Callback {} {} of {}:".format(callback_name, parameters, obj) msg += "\nCreated by {} on {}.".format(author, created_on) msg += "\nUpdated by {} on {}".format(updated_by, updated_on) if self.is_validator: - if event.get("valid"): - msg += "\nThis event is |rconnected|n and active." + if callback.get("valid"): + msg += "\nThis callback is |rconnected|n and active." else: - msg += "\nThis event |rhasn't been|n accepted yet." + msg += "\nThis callback |rhasn't been|n accepted yet." - msg += "\nEvent code:\n" - msg += raw(event["code"]) + msg += "\nCallback code:\n" + msg += raw(callback["code"]) self.msg(msg) return - # No parameter has been specified, display the table of events + # No parameter has been specified, display the table of callbacks cols = ["Number", "Author", "Updated", "Param"] if self.is_validator: cols.append("Valid") @@ -234,12 +232,12 @@ class CmdEvent(COMMAND_DEFAULT_CLASS): table = EvTable(*cols, width=78) table.reformat_column(0, align="r") now = datetime.now() - for i, event in enumerate(created): - author = event.get("author") + for i, callback in enumerate(created): + author = callback.get("author") author = author.key if author else "|gUnknown|n" - updated_on = event.get("updated_on") + updated_on = callback.get("updated_on") if updated_on is None: - updated_on = event.get("created_on") + updated_on = callback.get("created_on") if updated_on: updated_on = "{} ago".format(time_format( @@ -247,211 +245,211 @@ class CmdEvent(COMMAND_DEFAULT_CLASS): 4).capitalize()) else: updated_on = "|gUnknown|n" - parameters = event.get("parameters", "") + parameters = callback.get("parameters", "") row = [str(i + 1), author, updated_on, parameters] if self.is_validator: - row.append("Yes" if event.get("valid") else "No") + row.append("Yes" if callback.get("valid") else "No") table.add_row(*row) self.msg(unicode(table)) else: - names = list(set(list(types.keys()) + list(events.keys()))) - table = EvTable("Event name", "Number", "Description", + names = list(set(list(types.keys()) + list(callbacks.keys()))) + table = EvTable("Callback 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 in sorted(names): - number = len(events.get(name, [])) - lines = sum(len(e["code"].splitlines()) for e in \ - events.get(name, [])) + number = len(callbacks.get(name, [])) + lines = sum(len(e["code"].splitlines()) for e in callbacks.get(name, [])) no = "{} ({})".format(number, lines) - description = types.get(name, (None, "Chained event."))[1] - description = description.splitlines()[0] + description = types.get(name, (None, "Chained callback."))[1] + description = description.strip("\n").splitlines()[0] table.add_row(name, no, description) self.msg(unicode(table)) - def add_event(self): - """Add an event.""" + def add_callback(self): + """Add a callback.""" obj = self.obj - event_name = self.event_name - types = self.handler.get_event_types(obj) + callback_name = self.callback_name + types = self.handler.get_events(obj) - # Check that the event exists - 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))) + # Check that the callback exists + if not callback_name.startswith("chain_") and not callback_name in types: + self.msg("The callback name {} can't be found in {} of " \ + "typeclass {}.".format(callback_name, obj, type(obj))) return - definition = types.get(event_name, (None, "Chain event")) + definition = types.get(callback_name, (None, "Chained callback")) description = definition[1] - self.msg(raw(description)) + self.msg(raw(description.strip("\n"))) # Open the editor - event = self.handler.add_event(obj, event_name, "", + callback = self.handler.add_callback(obj, callback_name, "", self.caller, False, parameters=self.parameters) - # Lock this event right away - self.handler.db.locked.append((obj, event_name, event["number"])) + # Lock this callback right away + self.handler.db.locked.append((obj, callback_name, callback["number"])) - # Open the editor for this event - self.caller.db._event = event + # Open the editor for this callback + self.caller.db._callback = callback EvEditor(self.caller, loadfunc=_ev_load, savefunc=_ev_save, - quitfunc=_ev_quit, key="Event {} of {}".format( - event_name, obj), persistent=True, codefunc=_ev_save) + quitfunc=_ev_quit, key="Callback {} of {}".format( + callback_name, obj), persistent=True, codefunc=_ev_save) - def edit_event(self): - """Edit an event.""" + def edit_callback(self): + """Edit a callback.""" obj = self.obj - event_name = self.event_name + callback_name = self.callback_name parameters = self.parameters - events = self.handler.get_events(obj) - types = self.handler.get_event_types(obj) + callbacks = self.handler.get_callbacks(obj) + types = self.handler.get_events(obj) - # If no event name is specified, display the list of events - if not event_name: - self.list_events() + # If no callback name is specified, display the list of callbacks + if not callback_name: + self.list_callbacks() return - # Check that the event exists - if not event_name in events: - self.msg("The event name {} can't be found in {}.".format( - event_name, obj)) + # Check that the callback exists + if not callback_name in callbacks: + self.msg("The callback name {} can't be found in {}.".format( + callback_name, obj)) return - # If there's only one event, just edit it - if len(events[event_name]) == 1: + # If there's only one callback, just edit it + if len(callbacks[callback_name]) == 1: number = 0 - event = events[event_name][0] + callback = callbacks[callback_name][0] else: if not parameters: - self.msg("Which event do you wish to edit? Specify a number.") - self.list_events() + self.msg("Which callback do you wish to edit? Specify a number.") + self.list_callbacks() return - # Check that the parameter points to an existing event + # Check that the parameter points to an existing callback try: number = int(parameters) - 1 assert number >= 0 - event = events[event_name][number] + callback = callbacks[callback_name][number] except (ValueError, AssertionError, IndexError): - self.msg("The event {} {} cannot be found in {}.".format( - event_name, parameters, obj)) + self.msg("The callback {} {} cannot be found in {}.".format( + callback_name, parameters, obj)) return # If caller can't edit without validation, forbid editing # others' works - if not self.autovalid and event["author"] is not self.caller: - self.msg("You cannot edit this event created by someone else.") + if not self.autovalid and callback["author"] is not self.caller: + self.msg("You cannot edit this callback created by someone else.") return - # If the event is locked (edited by someone else) - if (obj, event_name, number) in self.handler.db.locked: - self.msg("This event is locked, you cannot edit it.") + # If the callback is locked (edited by someone else) + if (obj, callback_name, number) in self.handler.db.locked: + self.msg("This callback is locked, you cannot edit it.") return - self.handler.db.locked.append((obj, event_name, number)) - # Check the definition of the event - definition = types.get(event_name, (None, "Chained event")) + self.handler.db.locked.append((obj, callback_name, number)) + + # Check the definition of the callback + definition = types.get(callback_name, (None, "Chained callback")) description = definition[1] - self.msg(raw(description)) + self.msg(raw(description.strip("\n"))) # Open the editor - event = dict(event) - event["obj"] = obj - event["name"] = event_name - event["number"] = number - self.caller.db._event = event + callback = dict(callback) + callback["obj"] = obj + callback["name"] = callback_name + callback["number"] = number + self.caller.db._callback = callback EvEditor(self.caller, loadfunc=_ev_load, savefunc=_ev_save, - quitfunc=_ev_quit, key="Event {} of {}".format( - event_name, obj), persistent=True, codefunc=_ev_save) + quitfunc=_ev_quit, key="Callback {} of {}".format( + callback_name, obj), persistent=True, codefunc=_ev_save) - def del_event(self): - """Delete an event.""" + def del_callback(self): + """Delete a callback.""" obj = self.obj - event_name = self.event_name + callback_name = self.callback_name parameters = self.parameters - events = self.handler.get_events(obj) - types = self.handler.get_event_types(obj) + callbacks = self.handler.get_callbacks(obj) + types = self.handler.get_events(obj) - # If no event name is specified, display the list of events - if not event_name: - self.list_events() + # If no callback name is specified, display the list of callbacks + if not callback_name: + self.list_callbacks() return - # Check that the event exists - if not event_name in events: - self.msg("The event name {} can't be found in {}.".format( - event_name, obj)) + # Check that the callback exists + if not callback_name in callbacks: + self.msg("The callback name {} can't be found in {}.".format( + callback_name, obj)) return - # If there's only one event, just delete it - if len(events[event_name]) == 1: + # If there's only one callback, just delete it + if len(callbacks[callback_name]) == 1: number = 0 - event = events[event_name][0] + callback = callbacks[callback_name][0] else: if not parameters: - self.msg("Which event do you wish to delete? Specify " \ + self.msg("Which callback do you wish to delete? Specify " \ "a number.") - self.list_events() + self.list_callbacks() return - # Check that the parameter points to an existing event + # Check that the parameter points to an existing callback try: number = int(parameters) - 1 assert number >= 0 - event = events[event_name][number] + callback = callbacks[callback_name][number] except (ValueError, AssertionError, IndexError): - self.msg("The event {} {} cannot be found in {}.".format( - event_name, parameters, obj)) + self.msg("The callback {} {} cannot be found in {}.".format( + callback_name, parameters, obj)) return # If caller can't edit without validation, forbid deleting # others' works - if not self.autovalid and event["author"] is not self.caller: - self.msg("You cannot delete this event created by someone else.") + if not self.autovalid and callback["author"] is not self.caller: + self.msg("You cannot delete this callback created by someone else.") return - # If the event is locked (edited by someone else) - if (obj, event_name, number) in self.handler.db.locked: - self.msg("This event is locked, you cannot delete it.") + # If the callback is locked (edited by someone else) + if (obj, callback_name, number) in self.handler.db.locked: + self.msg("This callback is locked, you cannot delete it.") return - # Delete the event - self.handler.del_event(obj, event_name, number) - self.msg("The event {}[{}] of {} was deleted.".format( - event_name, number + 1, obj)) + # Delete the callback + self.handler.del_callback(obj, callback_name, number) + self.msg("The callback {}[{}] of {} was deleted.".format( + callback_name, number + 1, obj)) - def accept_event(self): - """Accept an event.""" + def accept_callback(self): + """Accept a callback.""" obj = self.obj - event_name = self.event_name + callback_name = self.callback_name parameters = self.parameters - # If no object, display the list of events to be checked + # If no object, display the list of callbacks to be checked if obj is None: table = EvTable("ID", "Type", "Object", "Name", "Updated by", "On", width=78) table.reformat_column(0, align="r") now = datetime.now() for obj, name, number in self.handler.db.to_valid: - events = self.handler.db.events.get(obj, {}).get(name) - if events is None: + callbacks = self.handler.get_callbacks(obj).get(name) + if callbacks is None: continue try: - event = events[number] + callback = callbacks[number] except IndexError: continue type_name = obj.typeclass_path.split(".")[-1] - by = event.get("updated_by") + by = callback.get("updated_by") by = by.key if by else "|gUnknown|n" - updated_on = event.get("updated_on") + updated_on = callback.get("updated_on") if updated_on is None: - updated_on = event.get("created_on") + updated_on = callback.get("created_on") if updated_on: updated_on = "{} ago".format(time_format( @@ -465,100 +463,100 @@ class CmdEvent(COMMAND_DEFAULT_CLASS): return # An object was specified - events = self.handler.get_events(obj) - types = self.handler.get_event_types(obj) + callbacks = self.handler.get_callbacks(obj) + types = self.handler.get_events(obj) - # If no event name is specified, display the list of events - if not event_name: - self.list_events() + # If no callback name is specified, display the list of callbacks + if not callback_name: + self.list_callbacks() return - # Check that the event exists - if not event_name in events: - self.msg("The event name {} can't be found in {}.".format( - event_name, obj)) + # Check that the callback exists + if not callback_name in callbacks: + self.msg("The callback name {} can't be found in {}.".format( + callback_name, obj)) return if not parameters: - self.msg("Which event do you wish to accept? Specify a number.") - self.list_events() + self.msg("Which callback do you wish to accept? Specify a number.") + self.list_callbacks() return - # Check that the parameter points to an existing event + # Check that the parameter points to an existing callback try: number = int(parameters) - 1 assert number >= 0 - event = events[event_name][number] + callback = callbacks[callback_name][number] except (ValueError, AssertionError, IndexError): - self.msg("The event {} {} cannot be found in {}.".format( - event_name, parameters, obj)) + self.msg("The callback {} {} cannot be found in {}.".format( + callback_name, parameters, obj)) return - # Accept the event - if event["valid"]: - self.msg("This event has already been accepted.") + # Accept the callback + if callback["valid"]: + self.msg("This callback has already been accepted.") else: - self.handler.accept_event(obj, event_name, number) - self.msg("The event {} {} of {} has been accepted.".format( - event_name, parameters, obj)) + self.handler.accept_callback(obj, callback_name, number) + self.msg("The callback {} {} of {} has been accepted.".format( + callback_name, parameters, obj)) def list_tasks(self): """List the active tasks.""" obj = self.obj - event_name = self.event_name + callback_name = self.callback_name handler = self.handler tasks = [(k, v[0], v[1], v[2]) for k, v in handler.db.tasks.items()] if obj: tasks = [task for task in tasks if task[2] is obj] - if event_name: - tasks = [task for task in tasks if task[3] == event_name] + if callback_name: + tasks = [task for task in tasks if task[3] == callback_name] tasks.sort() - table = EvTable("ID", "Object", "Event", "In", width=78) + table = EvTable("ID", "Object", "Callback", "In", width=78) table.reformat_column(0, align="r") now = datetime.now() - for task_id, future, obj, event_name in tasks: + for task_id, future, obj, callback_name in tasks: key = obj.get_display_name(self.caller) delta = time_format((future - now).total_seconds(), 1) - table.add_row(task_id, key, event_name, delta) + table.add_row(task_id, key, callback_name, delta) self.msg(unicode(table)) # Private functions to handle editing def _ev_load(caller): - return caller.db._event and caller.db._event.get("code", "") or "" + return caller.db._callback and caller.db._callback.get("code", "") or "" def _ev_save(caller, buf): - """Save and add the event.""" + """Save and add the callback.""" lock = "perm({}) or perm(events_without_validation)".format( WITHOUT_VALIDATION) autovalid = caller.locks.check_lockstring(caller, lock) - event = caller.db._event + callback = caller.db._callback handler = get_event_handler() - if not handler or not event or not all(key in event for key in \ + if not handler or not callback or not all(key in callback for key in \ ("obj", "name", "number", "valid")): - caller.msg("Couldn't save this event.") + caller.msg("Couldn't save this callback.") return False - if (event["obj"], event["name"], event["number"]) in handler.db.locked: - handler.db.locked.remove((event["obj"], event["name"], - event["number"])) + if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked: + handler.db.locked.remove((callback["obj"], callback["name"], + callback["number"])) - handler.edit_event(event["obj"], event["name"], event["number"], buf, + handler.edit_callback(callback["obj"], callback["name"], callback["number"], buf, caller, valid=autovalid) return True def _ev_quit(caller): - event = caller.db._event + callback = caller.db._callback handler = get_event_handler() - if not handler or not event or not all(key in event for key in \ + if not handler or not callback or not all(key in callback for key in \ ("obj", "name", "number", "valid")): - caller.msg("Couldn't save this event.") + caller.msg("Couldn't save this callback.") return False - if (event["obj"], event["name"], event["number"]) in handler.db.locked: - handler.db.locked.remove((event["obj"], event["name"], - event["number"])) + if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked: + handler.db.locked.remove((callback["obj"], callback["name"], + callback["number"])) - del caller.db._event + del caller.db._callback caller.msg("Exited the code editor.") diff --git a/evennia/contrib/events/custom.py b/evennia/contrib/events/custom.py deleted file mode 100644 index a4cef9a6ef..0000000000 --- a/evennia/contrib/events/custom.py +++ /dev/null @@ -1,265 +0,0 @@ -""" -Functions to extend the event system. - -These funcitons are not helpers (helpers are in a separate module) -and are designed to be used more by developers to add event types. - -""" - -from textwrap import dedent - -from django.conf import settings -from evennia import logger -from evennia import ScriptDB -from evennia.utils.create import create_script -from evennia.utils.gametime import real_seconds_until as standard_rsu -from evennia.contrib.custom_gametime import UNITS -from evennia.contrib.custom_gametime import gametime_to_realtime -from evennia.contrib.custom_gametime import real_seconds_until as custom_rsu - -hooks = [] -event_types = [] - -def get_event_handler(): - """Return the event handler or None.""" - try: - script = ScriptDB.objects.get(db_key="event_handler") - except ScriptDB.DoesNotExist: - logger.log_trace("Can't get the event handler.") - script = None - - return script - -def create_event_type(typeclass, event_name, variables, help_text, - custom_add=None, custom_call=None): - """ - Create a new event type for a specific typeclass. - - Args: - typeclass (type): the class defining tye typeclass to be used. - event_name (str): the name of the event to be added. - variables (list of str): a list of variable names. - help_text (str): a help text of the event. - custom_add (function, optional): a callback to call when adding - the new event. - custom_call (function, optional): a callback to call when - preparing to call the event. - - Events obey the inheritance hierarchy: if you set an event on - DefaultRoom, for instance, and if your Room typeclass inherits - from DefaultRoom (the default), the event will be available to - all rooms. Objects of the typeclass set in argument will be - able to set one or more events of that name. - - If the event type already exists in the typeclass, replace it. - - """ - typeclass_name = typeclass.__module__ + "." + typeclass.__name__ - event_types.append((typeclass_name, event_name, variables, help_text, - custom_add, custom_call)) - -def invalidate_event_type(typeclass, event_name): - """ - Invalidate a descending event type defined above in the hierarchy. - - Event types follow the hierarchy of inheritance. Events defined - in DefaultObjects would be accessible in DefaultRooms, for instance. - This can ensure that the event is limited and doesn't apply to - children with instances. - - Args: - typeclass (type): the class describing the typeclass. - event_name (str): the name of the event to invalidate. - - Example: - create_event_type(DefaultObject, "get", ["object"], "Someone gets.") - invalidate_event_type(DefaultRoom, "get") - # room objects won't have the 'get' event - - """ - typeclass_name = typeclass.__module__ + "." + typeclass.__name__ - event_types.append((typeclass_name, event_name, None, "", None, None)) - -def connect_event_types(): - """ - Connect the event types when the script runs. - - This method should be called automatically by the event handler - (the script). It might be useful, however, to call it after adding - new event types in typeclasses. - - """ - try: - script = ScriptDB.objects.get(db_key="event_handler") - except ScriptDB.DoesNotExist: - logger.log_trace("Can't connect event types, the event handler " \ - "cannot be found.") - return - - if script.ndb.event_types is None: - return - - t_event_types = list(event_types) - while t_event_types: - typeclass_name, event_name, variables, help_text, \ - custom_add, custom_call = t_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] = {} - types = script.ndb.event_types[typeclass_name] - - # Add or replace the event - help_text = dedent(help_text.strip("\n")) - types[event_name] = (variables, help_text, custom_add, custom_call) - del t_event_types[0] - -# Custom callbacks for specific event types -def get_next_wait(format): - """ - Get the length of time in seconds before format. - - Args: - format (str): a time format matching the set calendar. - - The time format could be something like "2018-01-08 12:00". The - number of units set in the calendar affects the way seconds are - calculated. - - Returns: - until (int or float): the number of seconds until the event. - usual (int or float): the usual number of seconds between events. - format (str): a string format representing the time. - - """ - calendar = getattr(settings, "EVENTS_CALENDAR", None) - if calendar is None: - logger.log_err("A time-related event has been set whereas " \ - "the gametime calendar has not been set in the settings.") - return - elif calendar == "standard": - rsu = standard_rsu - units = ["min", "hour", "day", "month", "year"] - elif calendar == "custom": - rsu = custom_rsu - back = dict([(value, name) for name, value in UNITS.items()]) - sorted_units = sorted(back.items()) - del sorted_units[0] - units = [n for v, n in sorted_units] - - params = {} - for delimiter in ("-", ":"): - format = format.replace(delimiter, " ") - - pieces = list(reversed(format.split())) - details = [] - i = 0 - for uname in units: - try: - piece = pieces[i] - except IndexError: - break - - if not piece.isdigit(): - logger.log_trace("The time specified '{}' in {} isn't " \ - "a valid number".format(piece, format)) - return - - # Convert the piece to int - piece = int(piece) - params[uname] = piece - details.append("{}={}".format(uname, piece)) - if i < len(units): - next_unit = units[i + 1] - else: - next_unit = None - i += 1 - - params["sec"] = 0 - details = " ".join(details) - until = rsu(**params) - usual = -1 - if next_unit: - kwargs = {next_unit: 1} - usual = gametime_to_realtime(**kwargs) - return until, usual, details - -def create_time_event(obj, event_name, number, parameters): - """ - Create a time-related event. - - Args: - obj (Object): the object on which stands the event. - event_name (str): the event's name. - number (int): the number of the event. - parameters (str): the parameter of the event. - - """ - seconds, usual, key = get_next_wait(parameters) - script = create_script("evennia.contrib.events.scripts.TimeEventScript", interval=seconds, obj=obj) - script.key = key - script.desc = "event on {}".format(key) - script.db.time_format = parameters - script.db.number = number - script.ndb.usual = usual - -def keyword_event(events, parameters): - """ - Custom call for events with keywords (like 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 set 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 - -def phrase_event(events, parameters): - """ - Custom call for events with keywords in sentences (like say or whisper). - - This function should be imported and added as a custom_call - parameter to add the event type when the event supports keywords - in phrase as parameters. Keywords in parameters are one or more - words separated by a comma. For instance, a 'say yes, okay' event - can be set to trigger when the player says something containing - either "yes" or "okay" (maybe 'say I don't like it, but okay'). - - 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. - - """ - phrase = parameters.strip().lower() - # Remove punctuation marks - punctuations = ',.";?!' - for p in punctuations: - phrase = phrase.replace(p, " ") - words = phrase.split() - words = [w.strip("' ") for w in words if w.strip("' ")] - to_call = [] - for event in events: - keys = event["parameters"] - if not keys or any(key.strip().lower() in words for key in keys.split(",")): - to_call.append(event) - - return to_call diff --git a/evennia/contrib/events/helpers.py b/evennia/contrib/events/eventfuncs.py similarity index 95% rename from evennia/contrib/events/helpers.py rename to evennia/contrib/events/eventfuncs.py index a29c26bb7b..b72afff86f 100644 --- a/evennia/contrib/events/helpers.py +++ b/evennia/contrib/events/eventfuncs.py @@ -1,7 +1,7 @@ """ -Module defining basic helpers for the event system. +Module defining basic eventfuncs for the event system. -Hlpers are just Python functions that can be used inside of events. They +Eventfuncs are just Python functions that can be used inside of calllbacks. """ diff --git a/evennia/contrib/events/handler.py b/evennia/contrib/events/handler.py index f6bd7ae986..fdec51e0e0 100644 --- a/evennia/contrib/events/handler.py +++ b/evennia/contrib/events/handler.py @@ -4,15 +4,15 @@ Module containing the EventHandler for individual objects. from collections import namedtuple -class EventsHandler(object): +class CallbackHandler(object): """ The event handler for a specific object. The script that contains all events will be reached through this handler. This handler is therefore a shortcut to be used by - developers. This handler (accessible through `obj.events`) is a - shortcut to manipulating events within this object, getting, + developers. This handler (accessible through `obj.callbacks`) is a + shortcut to manipulating callbacks within this object, getting, adding, editing, deleting and calling them. """ @@ -24,41 +24,45 @@ class EventsHandler(object): def all(self): """ - Return all events linked to this object. + Return all callbacks linked to this object. Returns: - All events in a dictionary event_name: event}. The event - is returned as a namedtuple to simply manipulation. + All callbacks in a dictionary callback_name: callback}. The callback + is returned as a namedtuple to simplify manipulation. """ - events = {} + callbacks = {} handler = type(self).script if handler: - dicts = handler.get_events(self.obj) - for event_name, in_list in dicts.items(): + dicts = handler.get_callbacks(self.obj) + for callback_name, in_list in dicts.items(): new_list = [] - for event in in_list: - event = self.format_event(event) - new_list.append(event) + for callback in in_list: + callback = self.format_callback(callback) + new_list.append(callback) if new_list: - events[event_name] = new_list + callbacks[callback_name] = new_list - return events + return callbacks - def get(self, event_name): + def get(self, callback_name): """ - Return the events associated with this name. + Return the callbacks associated with this name. Args: - event_name (str): the name of the event. + callback_name (str): the name of the callback. - This method returns a list of Event objects (namedtuple - representations). If the event name cannot be found in the - object's events, return an empty list. + Returns: + A list of callbacks associated with this object and of this name. + + Note: + This method returns a list of callback objects (namedtuple + representations). If the callback name cannot be found in the + object's callbacks, return an empty list. """ - return self.all().get(event_name, []) + return self.all().get(callback_name, []) def get_variable(self, variable_name): """ @@ -77,124 +81,124 @@ class EventsHandler(object): return None - def add(self, event_name, code, author=None, valid=False, parameters=""): + def add(self, callback_name, code, author=None, valid=False, parameters=""): """ - Add a new event for this object. + Add a new callback for this object. Args: - event_name (str): the name of the event to add. - code (str): the Python code associated with this event. - author (Character or Player, optional): the author of the event. - valid (bool, optional): should the event be connected? + callback_name (str): the name of the callback to add. + code (str): the Python code associated with this callback. + author (Character or Player, optional): the author of the callback. + valid (bool, optional): should the callback be connected? parameters (str, optional): optional parameters. Returns: - The event definition that was added or None. + The callback definition that was added or None. """ handler = type(self).script if handler: - return self.format_event(handler.add_event(self.obj, event_name, code, + return self.format_callback(handler.add_callback(self.obj, callback_name, code, author=author, valid=valid, parameters=parameters)) - def edit(self, event_name, number, code, author=None, valid=False): + def edit(self, callback_name, number, code, author=None, valid=False): """ - Edit an existing event bound to this object. + Edit an existing callback bound to this object. Args: - event_name (str): the name of the event to edit. - number (int): the event number to be changed. - code (str): the Python code associated with this event. - author (Character or Player, optional): the author of the event. - valid (bool, optional): should the event be connected? + callback_name (str): the name of the callback to edit. + number (int): the callback number to be changed. + code (str): the Python code associated with this callback. + author (Character or Player, optional): the author of the callback. + valid (bool, optional): should the callback be connected? Returns: - The event definition that was edited or None. + The callback definition that was edited or None. Raises: - RuntimeError if the event is locked. + RuntimeError if the callback is locked. """ handler = type(self).script if handler: - return self.format_event(handler.edit_event(self.obj, event_name, + return self.format_callback(handler.edit_callback(self.obj, callback_name, number, code, author=author, valid=valid)) - def remove(self, event_name, number): + def remove(self, callback_name, number): """ - Delete the specified event bound to this object. + Delete the specified callback bound to this object. Args: - event_name (str): the name of the event to delete. - number (int): the number of the event to delete. + callback_name (str): the name of the callback to delete. + number (int): the number of the callback to delete. Raises: - RuntimeError if the event is locked. + RuntimeError if the callback is locked. """ handler = type(self).script if handler: - handler.del_event(self.obj, event_name, number) + handler.del_callback(self.obj, callback_name, number) - def call(self, event_name, *args, **kwargs): + def call(self, callback_name, *args, **kwargs): """ - Call the specified event(s) bound to this object. + Call the specified callback(s) bound to this object. Args: - event_name (str): the event name to call. - *args: additional variables for this event. + callback_name (str): the callback name to call. + *args: additional variables for this callback. Kwargs: - number (int, optional): call just a specific event. - parameters (str, optional): call an event with parameters. + number (int, optional): call just a specific callback. + parameters (str, optional): call a callback with parameters. locals (dict, optional): a locals replacement. Returns: - True to report the event was called without interruption, - False otherwise. If the EventHandler isn't found, return + True to report the callback was called without interruption, + False otherwise. If the callbackHandler isn't found, return None. """ handler = type(self).script if handler: - return handler.call_event(self.obj, event_name, *args, **kwargs) + return handler.call(self.obj, callback_name, *args, **kwargs) return None @staticmethod - def format_event(event): + def format_callback(callback): """ - Return the Event namedtuple to represent the specified event. + Return the callback namedtuple to represent the specified callback. Args: - event (dict): the event definition. + callback (dict): the callback definition. - The event given in argument should be a dictionary containing - the expected fields for an event (code, author, valid...). + The callback given in argument should be a dictionary containing + the expected fields for a callback (code, author, valid...). """ - if "obj" not in event: - event["obj"] = None - if "name" not in event: - event["name"] = "unknown" - if "number" not in event: - event["number"] = -1 - if "code" not in event: - event["code"] = "" - if "author" not in event: - event["author"] = None - if "valid" not in event: - event["valid"] = False - if "parameters" not in event: - event["parameters"] = "" - if "created_on" not in event: - event["created_on"] = None - if "updated_by" not in event: - event["updated_by"] = None - if "updated_on" not in event: - event["updated_on"] = None + if "obj" not in callback: + callback["obj"] = None + if "name" not in callback: + callback["name"] = "unknown" + if "number" not in callback: + callback["number"] = -1 + if "code" not in callback: + callback["code"] = "" + if "author" not in callback: + callback["author"] = None + if "valid" not in callback: + callback["valid"] = False + if "parameters" not in callback: + callback["parameters"] = "" + if "created_on" not in callback: + callback["created_on"] = None + if "updated_by" not in callback: + callback["updated_by"] = None + if "updated_on" not in callback: + callback["updated_on"] = None - return Event(**event) + return Callback(**callback) -Event = namedtuple("Event", ("obj", "name", "number", "code", "author", +Callback = namedtuple("Callback", ("obj", "name", "number", "code", "author", "valid", "parameters", "created_on", "updated_by", "updated_on")) diff --git a/evennia/contrib/events/scripts.py b/evennia/contrib/events/scripts.py index d46909c33e..e63486db62 100644 --- a/evennia/contrib/events/scripts.py +++ b/evennia/contrib/events/scripts.py @@ -14,10 +14,9 @@ from evennia import logger from evennia.utils.create import create_channel from evennia.utils.dbserialize import dbserialize from evennia.utils.utils import all_from_module, delay -from evennia.contrib.events.custom import connect_event_types, get_next_wait from evennia.contrib.events.exceptions import InterruptEvent -from evennia.contrib.events.handler import EventsHandler as Handler -from evennia.contrib.events import typeclasses +from evennia.contrib.events.handler import CallbackHandler +from evennia.contrib.events.utils import get_next_wait, EVENTS # Constants RE_LINE_ERROR = re.compile(r'^ File "\", line (\d+)') @@ -28,7 +27,7 @@ class EventHandler(DefaultScript): The event handler that contains all events in a global script. This script shouldn't be created more than once. It contains - event types (in a non-persistent attribute) and events (in a + event (in a non-persistent attribute) and callbacks (in a persistent attribute). The script method would help adding, editing and deleting these events. @@ -41,7 +40,7 @@ class EventHandler(DefaultScript): self.persistent = True # Permanent data to be stored - self.db.events = {} + self.db.callbacks = {} self.db.to_valid = [] self.db.locked = [] @@ -56,21 +55,22 @@ class EventHandler(DefaultScript): (including when it's reloaded). This hook performs the following tasks: - - Refresh and re-connect event types. + - Create temporarily stored events. - Generate locals (individual events' namespace). - - Load event helpers, including user-defined ones. + - Load eventfuncs, including user-defined ones. - Re-schedule tasks that aren't set to fire anymore. - Effectively connect the handler to the main script. """ - self.ndb.event_types = {} - connect_event_types() + self.ndb.events = {} + for typeclass, name, variables, help_text, custom_call, custom_add in EVENTS: + self.add_event(typeclass, name, variables, help_text, custom_call, custom_add) # Generate locals self.ndb.current_locals = {} self.ndb.fresh_locals = {} - addresses = ["evennia.contrib.events.helpers"] - addresses.extend(getattr(settings, "EVENTS_HELPERS_LOCATIONS", [])) + addresses = ["evennia.contrib.events.eventfuncs"] + addresses.extend(getattr(settings, "EVENTFUNCS_LOCATIONS", [])) for address in addresses: self.ndb.fresh_locals.update(all_from_module(address)) @@ -84,9 +84,10 @@ class EventHandler(DefaultScript): delay(seconds, complete_task, task_id) - # Place the script in the EventsHandler - Handler.script = self - DefaultObject.events = typeclasses.EventObject.events + # Place the script in the CallbackHandler + from evennia.contrib.events import typeclasses + CallbackHandler.script = self + DefaultObject.callbacks = typeclasses.EventObject.callbacks # Create the channel if non-existent try: @@ -97,73 +98,42 @@ class EventHandler(DefaultScript): def get_events(self, obj): """ - Return a dictionary of the object's events. - - Args: - obj (Object): the connected objects. - - Returns: - A dictionary of the object's events. - - Note: - This method can be useful to override in some contexts, - when several objects would share events. - - """ - obj_events = self.db.events.get(obj, {}) - events = {} - for event_name, event_list in obj_events.items(): - new_list = [] - for i, event in enumerate(event_list): - event = dict(event) - event["obj"] = obj - event["name"] = event_name - event["number"] = i - new_list.append(event) - - if new_list: - events[event_name] = new_list - - return events - - def get_event_types(self, obj): - """ - Return a dictionary of event types on this object. + Return a dictionary of events on this object. Args: obj (Object): the connected object. Returns: - A dictionary of the object's event types. + A dictionary of the object's events. Note: - Event types would define what the object can have as - events. Note, however, that chained events will not - appear in event types and are handled separately. + Events would define what the object can have as + callbacks. Note, however, that chained callbacks will not + appear in events and are handled separately. """ - types = {} - event_types = self.ndb.event_types + events = {} + all_events = self.ndb.events classes = Queue() classes.put(type(obj)) invalid = [] while not classes.empty(): typeclass = classes.get() typeclass_name = typeclass.__module__ + "." + typeclass.__name__ - for key, etype in event_types.get(typeclass_name, {}).items(): + for key, etype in all_events.get(typeclass_name, {}).items(): if key in invalid: continue if etype[0] is None: # Invalidate invalid.append(key) continue - if key not in types: - types[key] = etype + if key not in events: + events[key] = etype # Look for the parent classes for parent in typeclass.__bases__: classes.put(parent) - return types + return events def get_variable(self, variable_name): """ @@ -190,34 +160,66 @@ class EventHandler(DefaultScript): """ return self.ndb.current_locals.get(variable_name) - def add_event(self, obj, event_name, code, author=None, valid=False, + def get_callbacks(self, obj): + """ + Return a dictionary of the object's callbacks. + + Args: + obj (Object): the connected objects. + + Returns: + A dictionary of the object's callbacks. + + Note: + This method can be useful to override in some contexts, + when several objects would share callbacks. + + """ + obj_callbacks = self.db.callbacks.get(obj, {}) + callbacks = {} + for callback_name, callback_list in obj_callbacks.items(): + new_list = [] + for i, callback in enumerate(callback_list): + callback = dict(callback) + callback["obj"] = obj + callback["name"] = callback_name + callback["number"] = i + new_list.append(callback) + + if new_list: + callbacks[callback_name] = new_list + + return callbacks + + def add_callback(self, obj, callback_name, code, author=None, valid=False, parameters=""): """ - Add the specified event. + Add the specified callback. Args: obj (Object): the Evennia typeclassed object to be extended. - event_name (str): the name of the event to add. - code (str): the Python code associated with this event. - author (Character or Player, optional): the author of the event. - valid (bool, optional): should the event be connected? + callback_name (str): the name of the callback to add. + code (str): the Python code associated with this callback. + author (Character or Player, optional): the author of the callback. + valid (bool, optional): should the callback be connected? parameters (str, optional): optional parameters. - This method doesn't check that the event type exists. + Note: + This method doesn't check that the callback type exists. """ - obj_events = self.db.events.get(obj, {}) - if not obj_events: - self.db.events[obj] = {} - obj_events = self.db.events[obj] + obj_callbacks = self.db.callbacks.get(obj, {}) + if not obj_callbacks: + self.db.callbacks[obj] = {} + obj_callbacks = self.db.callbacks[obj] - events = obj_events.get(event_name, []) - if not events: - obj_events[event_name] = [] - events = obj_events[event_name] + callbacks = obj_callbacks.get(callback_name, []) + if not callbacks: + obj_callbacks[callback_name] = [] + callbacks = obj_callbacks[callback_name] - # Add the event in the list - events.append({ + # Add the callback in the list + callbacks.append({ "created_on": datetime.now(), "author": author, "valid": valid, @@ -227,56 +229,57 @@ class EventHandler(DefaultScript): # If not valid, set it in 'to_valid' if not valid: - self.db.to_valid.append((obj, event_name, len(events) - 1)) + self.db.to_valid.append((obj, callback_name, len(callbacks) - 1)) # Call the custom_add if needed - custom_add = self.get_event_types(obj).get( - event_name, [None, None, None])[2] + custom_add = self.get_events(obj).get( + callback_name, [None, None, None, None])[3] if custom_add: - custom_add(obj, event_name, len(events) - 1, parameters) + custom_add(obj, callback_name, len(callbacks) - 1, parameters) # Build the definition to return (a dictionary) - definition = dict(events[-1]) + definition = dict(callbacks[-1]) definition["obj"] = obj - definition["name"] = event_name - definition["number"] = len(events) - 1 + definition["name"] = callback_name + definition["number"] = len(callbacks) - 1 return definition - def edit_event(self, obj, event_name, number, code, author=None, + def edit_callback(self, obj, callback_name, number, code, author=None, valid=False): """ - Edit the specified event. + Edit the specified callback. Args: obj (Object): the Evennia typeclassed object to be edited. - event_name (str): the name of the event to edit. - number (int): the event number to be changed. - code (str): the Python code associated with this event. - author (Character or Player, optional): the author of the event. - valid (bool, optional): should the event be connected? + callback_name (str): the name of the callback to edit. + number (int): the callback number to be changed. + code (str): the Python code associated with this callback. + author (Character or Player, optional): the author of the callback. + valid (bool, optional): should the callback be connected? Raises: - RuntimeError if the event is locked. + RuntimeError if the callback is locked. - This method doesn't check that the event type exists. + Note: + This method doesn't check that the callback type exists. """ - obj_events = self.db.events.get(obj, {}) - if not obj_events: - self.db.events[obj] = {} - obj_events = self.db.events[obj] + obj_callbacks = self.db.callbacks.get(obj, {}) + if not obj_callbacks: + self.db.callbacks[obj] = {} + obj_callbacks = self.db.callbacks[obj] - events = obj_events.get(event_name, []) - if not events: - obj_events[event_name] = [] - events = obj_events[event_name] + callbacks = obj_callbacks.get(callback_name, []) + if not callbacks: + obj_callbacks[callback_name] = [] + callbacks = obj_callbacks[callback_name] # If locked, don't edit it - if (obj, event_name, number) in self.db.locked: - raise RuntimeError("this event is locked.") + if (obj, callback_name, number) in self.db.locked: + raise RuntimeError("this callback is locked.") - # Edit the event - events[number].update({ + # Edit the callback + callbacks[number].update({ "updated_on": datetime.now(), "updated_by": author, "valid": valid, @@ -284,118 +287,118 @@ class EventHandler(DefaultScript): }) # If not valid, set it in 'to_valid' - if not valid and (obj, event_name, number) not in self.db.to_valid: - self.db.to_valid.append((obj, event_name, number)) - elif valid and (obj, event_name, number) in self.db.to_valid: - self.db.to_valid.remove((obj, event_name, number)) + if not valid and (obj, callback_name, number) not in self.db.to_valid: + self.db.to_valid.append((obj, callback_name, number)) + elif valid and (obj, callback_name, number) in self.db.to_valid: + self.db.to_valid.remove((obj, callback_name, number)) # Build the definition to return (a dictionary) - definition = dict(events[number]) + definition = dict(callbacks[number]) definition["obj"] = obj - definition["name"] = event_name + definition["name"] = callback_name definition["number"] = number return definition - def del_event(self, obj, event_name, number): + def del_callback(self, obj, callback_name, number): """ - Delete the specified event. + Delete the specified callback. Args: - obj (Object): the typeclassed object containing the event. - event_name (str): the name of the event to delete. - number (int): the number of the event to delete. + obj (Object): the typeclassed object containing the callback. + callback_name (str): the name of the callback to delete. + number (int): the number of the callback to delete. Raises: - RuntimeError if the event is locked. + RuntimeError if the callback is locked. """ - obj_events = self.db.events.get(obj, {}) - events = obj_events.get(event_name, []) + obj_callbacks = self.db.callbacks.get(obj, {}) + callbacks = obj_callbacks.get(callback_name, []) # If locked, don't edit it - if (obj, event_name, number) in self.db.locked: - raise RuntimeError("this event is locked.") + if (obj, callback_name, number) in self.db.locked: + raise RuntimeError("this callback is locked.") - # Delete the event itself + # Delete the callback itself try: - code = events[number]["code"] + code = callbacks[number]["code"] except IndexError: return else: - logger.log_info("Deleting event {} {} of {}:\n{}".format( - event_name, number, obj, code)) - del events[number] + logger.log_info("Deleting callback {} {} of {}:\n{}".format( + callback_name, number, obj, code)) + del callbacks[number] - # Change IDs of events to be validated + # Change IDs of callbacks to be validated i = 0 while i < len(self.db.to_valid): - t_obj, t_event_name, t_number = self.db.to_valid[i] - if obj is t_obj and event_name == t_event_name: + t_obj, t_callback_name, t_number = self.db.to_valid[i] + if obj is t_obj and callback_name == t_callback_name: if t_number == number: - # Strictly equal, delete the event + # Strictly equal, delete the callback del self.db.to_valid[i] i -= 1 elif t_number > number: - # Change the ID for this event - self.db.to_valid.insert(i, (t_obj, t_event_name, + # Change the ID for this callback + self.db.to_valid.insert(i, (t_obj, t_callback_name, t_number - 1)) del self.db.to_valid[i + 1] i += 1 - # Update locked event + # Update locked callback for i, line in enumerate(self.db.locked): - t_obj, t_event_name, t_number = line - if obj is t_obj and event_name == t_event_name: + t_obj, t_callback_name, t_number = line + if obj is t_obj and callback_name == t_callback_name: if number < t_number: - self.db.locked[i] = (t_obj, t_event_name, t_number - 1) + self.db.locked[i] = (t_obj, t_callback_name, t_number - 1) - # Delete time-related events associated with this object + # Delete time-related callbacks associated with this object for script in list(obj.scripts.all()): - if isinstance(script, TimeEventScript): - if script.obj is obj and script.db.event_name == event_name: + if isinstance(script, TimecallbackScript): + if script.obj is obj and script.db.callback_name == callback_name: if script.db.number == number: script.stop() elif script.db.number > number: script.db.number -= 1 - def accept_event(self, obj, event_name, number): + def accept_callback(self, obj, callback_name, number): """ - Valid an event. + Valid a callback. Args: - obj (Object): the object containing the event. - event_name (str): the name of the event. - number (int): the number of the event. + obj (Object): the object containing the callback. + callback_name (str): the name of the callback. + number (int): the number of the callback. """ - obj_events = self.db.events.get(obj, {}) - events = obj_events.get(event_name, []) + obj_callbacks = self.db.callbacks.get(obj, {}) + callbacks = obj_callbacks.get(callback_name, []) - # Accept and connect the event - events[number].update({"valid": True}) - if (obj, event_name, number) in self.db.to_valid: - self.db.to_valid.remove((obj, event_name, number)) + # Accept and connect the callback + callbacks[number].update({"valid": True}) + if (obj, callback_name, number) in self.db.to_valid: + self.db.to_valid.remove((obj, callback_name, number)) - def call_event(self, obj, event_name, *args, **kwargs): + def call(self, obj, callback_name, *args, **kwargs): """ - Call the event. + Call the connected callbacks. Args: obj (Object): the Evennia typeclassed object. - event_name (str): the event name to call. - *args: additional variables for this event. + callback_name (str): the callback name to call. + *args: additional variables for this callback. Kwargs: - number (int, optional): call just a specific event. - parameters (str, optional): call an event with parameters. + number (int, optional): call just a specific callback. + parameters (str, optional): call a callback with parameters. locals (dict, optional): a locals replacement. Returns: - True to report the event was called without interruption, + True to report the callback was called without interruption, False otherwise. """ - # First, look for the event type corresponding to this name + # First, look for the callback type corresponding to this name number = kwargs.get("number") parameters = kwargs.get("parameters") locals = kwargs.get("locals") @@ -404,54 +407,54 @@ class EventHandler(DefaultScript): 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)) + "to call callbacks: {}".format(kwargs)) - event_type = self.get_event_types(obj).get(event_name) - 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))) + event = self.get_events(obj).get(callback_name) + if locals is None and not event: + logger.log_err("The callback {} for the object {} (typeclass " \ + "{}) can't be found".format(callback_name, obj, type(obj))) 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]): + for i, variable in enumerate(event[0]): try: locals[variable] = args[i] except IndexError: - logger.log_trace("event {} of {} ({}): need variable " \ - "{} in position {}".format(event_name, obj, + logger.log_trace("callback {} of {} ({}): need variable " \ + "{} in position {}".format(callback_name, obj, type(obj), variable, i)) return False else: locals = {key: value for key, value in locals.items()} - events = self.get_events(obj).get(event_name, []) - if event_type: - custom_call = event_type[3] + callbacks = self.get_callbacks(obj).get(callback_name, []) + if event: + custom_call = event[2] if custom_call: - events = custom_call(events, parameters) + callbacks = custom_call(callbacks, parameters) - # Now execute all the valid events linked at this address + # Now execute all the valid callbacks linked at this address self.ndb.current_locals = locals - for i, event in enumerate(events): - if not event["valid"]: + for i, callback in enumerate(callbacks): + if not callback["valid"]: continue - if number is not None and event["number"] != number: + if number is not None and callback["number"] != number: continue try: - exec(event["code"], locals, locals) + exec(callback["code"], locals, locals) except InterruptEvent: return False except Exception: etype, evalue, tb = sys.exc_info() trace = traceback.format_exception(etype, evalue, tb) - number = event["number"] + number = callback["number"] oid = obj.id - logger.log_err("An error occurred during the event {} of " \ - "{} (#{}), number {}\n{}".format(event_name, obj, + logger.log_err("An error occurred during the callback {} of " \ + "{} (#{}), number {}\n{}".format(callback_name, obj, oid, number + 1, "\n".join(trace))) # Inform the 'everror' channel @@ -465,33 +468,54 @@ class EventHandler(DefaultScript): # Try to extract the line try: - line = event["code"].splitlines()[lineno - 1] + line = callback["code"].splitlines()[lineno - 1] except IndexError: continue else: break self.ndb.channel.msg("Error in {} of {} (#{})[{}], line {}:" \ - " {}\n {}".format(event_name, obj, + " {}\n {}".format(callback_name, obj, oid, number + 1, lineno, line, repr(evalue))) return True - def set_task(self, seconds, obj, event_name): + def add_event(self, typeclass, name, variables, help_text, custom_call, custom_add): + """ + Add a new event for a defined typeclass. + + Args: + typeclass (str): the path leading to the typeclass. + name (str): the name of the event to add. + variables (list of str): list of variable names for this event. + help_text (str): the long help text of the event. + custom_call (callable or None): the function to be called + when the event fires. + custom_add (callable or None): the function to be called when + a callback is added. + + """ + if typeclass not in self.ndb.events: + self.ndb.events[typeclass] = {} + + events = self.ndb.events[typeclass] + if name not in events: + events[name] = (variables, help_text, custom_call, custom_add) + + def set_task(self, seconds, obj, callback_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. + callback_name (str): the callback's name. - Note: + Notes: + 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. 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, @@ -514,7 +538,7 @@ class EventHandler(DefaultScript): else: locals[key] = value - self.db.tasks[task_id] = (now + delta, obj, event_name, locals) + self.db.tasks[task_id] = (now + delta, obj, callback_name, locals) delay(seconds, complete_task, task_id) @@ -572,11 +596,12 @@ 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. + Note: + This function should be called automatically for individual tasks. + """ try: script = ScriptDB.objects.get(db_key="event_handler") @@ -589,5 +614,5 @@ def complete_task(task_id): "found".format(task_id)) return - delta, obj, event_name, locals = script.db.tasks.pop(task_id) - script.call_event(obj, event_name, locals=locals) + delta, obj, callback_name, locals = script.db.tasks.pop(task_id) + script.call(obj, callback_name, locals=locals) diff --git a/evennia/contrib/events/tests.py b/evennia/contrib/events/tests.py index f5b1a40f5a..300e41e7ad 100644 --- a/evennia/contrib/events/tests.py +++ b/evennia/contrib/events/tests.py @@ -12,8 +12,8 @@ from evennia.objects.objects import ExitCommand from evennia.utils import ansi, utils from evennia.utils.create import create_object, create_script from evennia.utils.test_resources import EvenniaTest -from evennia.contrib.events.commands import CmdEvent -from evennia.contrib.events.handler import EventsHandler +from evennia.contrib.events.commands import CmdCallback +from evennia.contrib.events.handler import CallbackHandler # Force settings settings.EVENTS_CALENDAR = "standard" @@ -40,125 +40,101 @@ class TestEventHandler(EvenniaTest): def tearDown(self): """Stop the event handler.""" self.handler.stop() - EventsHandler.script = None + CallbackHandler.script = None super(TestEventHandler, self).tearDown() def test_start(self): """Simply make sure the handler runs with proper initial values.""" - self.assertEqual(self.handler.db.events, {}) + self.assertEqual(self.handler.db.callbacks, {}) self.assertEqual(self.handler.db.to_valid, []) self.assertEqual(self.handler.db.locked, []) self.assertEqual(self.handler.db.tasks, {}) self.assertEqual(self.handler.db.task_id, 0) - self.assertIsNotNone(self.handler.ndb.event_types) - - def test_add(self): - """Add a single event on room1.""" - author = self.char1 - self.handler.add_event(self.room1, "dummy", - "character.db.strength = 50", author=author, valid=True) - event = self.handler.get_events(self.room1).get("dummy") - event = event[0] - self.assertIsNotNone(event) - self.assertEqual(event["obj"], self.room1) - self.assertEqual(event["name"], "dummy") - self.assertEqual(event["number"], 0) - self.assertEqual(event["author"], author) - self.assertEqual(event["valid"], True) - - # Since this event is valid, it shouldn't appear in 'to_valid' - self.assertNotIn((self.room1, "dummy", 0), self.handler.db.to_valid) - - # Run this dummy event - self.char1.db.strength = 10 - locals = {"character": self.char1} - self.assertTrue(self.handler.call_event( - self.room1, "dummy", locals=locals)) - self.assertEqual(self.char1.db.strength, 50) + self.assertIsNotNone(self.handler.ndb.events) def test_add_validation(self): - """Add an event while needing validation.""" + """Add a callback while needing validation.""" author = self.char1 - self.handler.add_event(self.room1, "dummy", + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 40", author=author, valid=False) - event = self.handler.get_events(self.room1).get("dummy") - event = event[0] - self.assertIsNotNone(event) - self.assertEqual(event["author"], author) - self.assertEqual(event["valid"], False) + callback = self.handler.get_callbacks(self.room1).get("dummy") + callback = callback[0] + self.assertIsNotNone(callback) + self.assertEqual(callback["author"], author) + self.assertEqual(callback["valid"], False) - # Since this event is not valid, it should appear in 'to_valid' + # Since this callback is not valid, it should appear in 'to_valid' self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid) - # Run this dummy event (shouldn't do anything) + # Run this dummy callback (shouldn't do anything) self.char1.db.strength = 10 locals = {"character": self.char1} - self.assertTrue(self.handler.call_event( + self.assertTrue(self.handler.call( self.room1, "dummy", locals=locals)) self.assertEqual(self.char1.db.strength, 10) def test_edit(self): - """Test editing an event.""" + """Test editing a callback.""" author = self.char1 - self.handler.add_event(self.room1, "dummy", + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 60", author=author, valid=True) # Edit it right away - self.handler.edit_event(self.room1, "dummy", 0, + self.handler.edit_callback(self.room1, "dummy", 0, "character.db.strength = 65", author=self.char2, valid=True) - # Check that the event was written - event = self.handler.get_events(self.room1).get("dummy") - event = event[0] - self.assertIsNotNone(event) - self.assertEqual(event["author"], author) - self.assertEqual(event["valid"], True) - self.assertEqual(event["updated_by"], self.char2) + # Check that the callback was written + callback = self.handler.get_callbacks(self.room1).get("dummy") + callback = callback[0] + self.assertIsNotNone(callback) + self.assertEqual(callback["author"], author) + self.assertEqual(callback["valid"], True) + self.assertEqual(callback["updated_by"], self.char2) - # Run this dummy event + # Run this dummy callback self.char1.db.strength = 10 locals = {"character": self.char1} - self.assertTrue(self.handler.call_event( + self.assertTrue(self.handler.call( self.room1, "dummy", locals=locals)) self.assertEqual(self.char1.db.strength, 65) def test_edit_validation(self): - """Edit an event when validation isn't automatic.""" + """Edit a callback when validation isn't automatic.""" author = self.char1 - self.handler.add_event(self.room1, "dummy", + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 70", author=author, valid=True) # Edit it right away - self.handler.edit_event(self.room1, "dummy", 0, + self.handler.edit_callback(self.room1, "dummy", 0, "character.db.strength = 80", author=self.char2, valid=False) - # Run this dummy event (shouldn't do anything) + # Run this dummy callback (shouldn't do anything) self.char1.db.strength = 10 locals = {"character": self.char1} - self.assertTrue(self.handler.call_event( + self.assertTrue(self.handler.call( self.room1, "dummy", locals=locals)) self.assertEqual(self.char1.db.strength, 10) def test_del(self): - """Try to delete an event.""" - # Add 3 events - self.handler.add_event(self.room1, "dummy", + """Try to delete a callback.""" + # Add 3 callbacks + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True) - self.handler.add_event(self.room1, "dummy", + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False) - self.handler.add_event(self.room1, "dummy", + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 9", author=self.char1, valid=True) - # Note that the second event isn't valid + # Note that the second callback isn't valid self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid) - # Lock the third event + # Lock the third callback self.handler.db.locked.append((self.room1, "dummy", 2)) - # Delete the first event - self.handler.del_event(self.room1, "dummy", 0) + # Delete the first callback + self.handler.del_callback(self.room1, "dummy", 0) - # The event #1 that was to valid should be #0 now + # The callback #1 that was to valid should be #0 now self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid) self.assertNotIn((self.room1, "dummy", 1), self.handler.db.to_valid) @@ -166,104 +142,104 @@ class TestEventHandler(EvenniaTest): self.assertIn((self.room1, "dummy", 1), self.handler.db.locked) self.assertNotIn((self.room1, "dummy", 2), self.handler.db.locked) - # Now delete the first (not valid) event - self.handler.del_event(self.room1, "dummy", 0) + # Now delete the first (not valid) callback + self.handler.del_callback(self.room1, "dummy", 0) self.assertEqual(self.handler.db.to_valid, []) self.assertIn((self.room1, "dummy", 0), self.handler.db.locked) self.assertNotIn((self.room1, "dummy", 1), self.handler.db.locked) - # Call the remaining event + # Call the remaining callback self.char1.db.strength = 10 locals = {"character": self.char1} - self.assertTrue(self.handler.call_event( + self.assertTrue(self.handler.call( self.room1, "dummy", locals=locals)) self.assertEqual(self.char1.db.strength, 9) def test_accept(self): - """Accept an event.""" - # Add 2 events - self.handler.add_event(self.room1, "dummy", + """Accept an callback.""" + # Add 2 callbacks + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 5", author=self.char1, valid=True) - self.handler.add_event(self.room1, "dummy", + self.handler.add_callback(self.room1, "dummy", "character.db.strength = 8", author=self.char2, valid=False) - # Note that the second event isn't valid + # Note that the second callback isn't valid self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid) - # Accept the second event - self.handler.accept_event(self.room1, "dummy", 1) - event = self.handler.get_events(self.room1).get("dummy") - event = event[1] - self.assertIsNotNone(event) - self.assertEqual(event["valid"], True) + # Accept the second callback + self.handler.accept_callback(self.room1, "dummy", 1) + callback = self.handler.get_callbacks(self.room1).get("dummy") + callback = callback[1] + self.assertIsNotNone(callback) + self.assertEqual(callback["valid"], True) - # Call the dummy event + # Call the dummy callback self.char1.db.strength = 10 locals = {"character": self.char1} - self.assertTrue(self.handler.call_event( + self.assertTrue(self.handler.call( self.room1, "dummy", locals=locals)) self.assertEqual(self.char1.db.strength, 8) def test_call(self): - """Test to call amore complex event.""" + """Test to call amore complex callback.""" self.char1.key = "one" self.char2.key = "two" - # Add an event + # Add an callback code = dedent(""" if character.key == "one": character.db.health = 50 else: character.db.health = 0 """.strip("\n")) - self.handler.add_event(self.room1, "dummy", code, + self.handler.add_callback(self.room1, "dummy", code, author=self.char1, valid=True) - # Call the dummy event - self.assertTrue(self.handler.call_event( + # Call the dummy callback + self.assertTrue(self.handler.call( self.room1, "dummy", locals={"character": self.char1})) self.assertEqual(self.char1.db.health, 50) - self.assertTrue(self.handler.call_event( + self.assertTrue(self.handler.call( self.room1, "dummy", locals={"character": self.char2})) self.assertEqual(self.char2.db.health, 0) def test_handler(self): """Test the object handler.""" - self.assertIsNotNone(self.char1.events) + self.assertIsNotNone(self.char1.callbacks) - # Add an event - event = self.room1.events.add("dummy", "pass", author=self.char1, + # Add an callback + callback = self.room1.callbacks.add("dummy", "pass", author=self.char1, valid=True) - self.assertEqual(event.obj, self.room1) - self.assertEqual(event.name, "dummy") - self.assertEqual(event.code, "pass") - self.assertEqual(event.author, self.char1) - self.assertEqual(event.valid, True) - self.assertIn([event], self.room1.events.all().values()) + self.assertEqual(callback.obj, self.room1) + self.assertEqual(callback.name, "dummy") + self.assertEqual(callback.code, "pass") + self.assertEqual(callback.author, self.char1) + self.assertEqual(callback.valid, True) + self.assertIn([callback], self.room1.callbacks.all().values()) - # Edit this very event - new = self.room1.events.edit("dummy", 0, "character.db.say = True", + # Edit this very callback + new = self.room1.callbacks.edit("dummy", 0, "character.db.say = True", author=self.char1, valid=True) - self.assertIn([new], self.room1.events.all().values()) - self.assertNotIn([event], self.room1.events.all().values()) + self.assertIn([new], self.room1.callbacks.all().values()) + self.assertNotIn([callback], self.room1.callbacks.all().values()) - # Try to call this event - self.assertTrue(self.room1.events.call("dummy", + # Try to call this callback + self.assertTrue(self.room1.callbacks.call("dummy", locals={"character": self.char2})) self.assertTrue(self.char2.db.say) - # Delete the event - self.room1.events.remove("dummy", 0) - self.assertEqual(self.room1.events.all(), {}) + # Delete the callback + self.room1.callbacks.remove("dummy", 0) + self.assertEqual(self.room1.callbacks.all(), {}) -class TestCmdEvent(CommandTest): +class TestCmdCallback(CommandTest): - """Test the @event command.""" + """Test the @callback command.""" def setUp(self): - """Create the event handler.""" - super(TestCmdEvent, self).setUp() + """Create the callback handler.""" + super(TestCmdCallback, self).setUp() self.handler = create_script( "evennia.contrib.events.scripts.EventHandler") @@ -275,32 +251,32 @@ class TestCmdEvent(CommandTest): self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit") def tearDown(self): - """Stop the event handler.""" + """Stop the callback handler.""" self.handler.stop() for script in ScriptDB.objects.filter( db_typeclass_path="evennia.contrib.events.scripts.TimeEventScript"): script.stop() - EventsHandler.script = None - super(TestCmdEvent, self).tearDown() + CallbackHandler.script = None + super(TestCmdCallback, self).tearDown() def test_list(self): - """Test listing events with different rights.""" - table = self.call(CmdEvent(), "out") + """Test listing callbacks with different rights.""" + table = self.call(CmdCallback(), "out") lines = table.splitlines()[3:-1] self.assertNotEqual(lines, []) - # Check that the second column only contains 0 (0) (no event yet) + # Check that the second column only contains 0 (0) (no callback yet) for line in lines: cols = line.split("|") self.assertIn(cols[2].strip(), ("0 (0)", "")) - # Add some event - self.handler.add_event(self.exit, "traverse", "pass", + # Add some callback + self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True) - # Try to obtain more details on a specific event on exit - table = self.call(CmdEvent(), "out = traverse") + # Try to obtain more details on a specific callback on exit + table = self.call(CmdCallback(), "out = traverse") lines = table.splitlines()[3:-1] self.assertEqual(len(lines), 1) line = lines[0] @@ -311,7 +287,7 @@ class TestCmdEvent(CommandTest): # Run the same command with char2 # char2 shouldn't see the last column (Valid) - table = self.call(CmdEvent(), "out = traverse", caller=self.char2) + table = self.call(CmdCallback(), "out = traverse", caller=self.char2) lines = table.splitlines()[3:-1] self.assertEqual(len(lines), 1) line = lines[0] @@ -319,18 +295,18 @@ class TestCmdEvent(CommandTest): self.assertEqual(cols[1].strip(), "1") self.assertNotIn(cols[-1].strip(), ("Yes", "No")) - # In any case, display the event - # The last line should be "pass" (the event code) - details = self.call(CmdEvent(), "out = traverse 1") + # In any case, display the callback + # The last line should be "pass" (the callback code) + details = self.call(CmdCallback(), "out = traverse 1") self.assertEqual(details.splitlines()[-1], "pass") def test_add(self): - """Test to add an event.""" - self.call(CmdEvent(), "/add out = traverse") + """Test to add an callback.""" + self.call(CmdCallback(), "/add out = traverse") editor = self.char1.ndb._eveditor self.assertIsNotNone(editor) - # Edit the event + # Edit the callback editor.update_buffer(dedent(""" if character.key == "one": character.msg("You can pass.") @@ -340,89 +316,89 @@ class TestCmdEvent(CommandTest): """.strip("\n"))) editor.save_buffer() editor.quit() - event = self.exit.events.get("traverse")[0] - self.assertEqual(event.author, self.char1) - self.assertEqual(event.valid, True) - self.assertTrue(len(event.code) > 0) + callback = self.exit.callbacks.get("traverse")[0] + self.assertEqual(callback.author, self.char1) + self.assertEqual(callback.valid, True) + self.assertTrue(len(callback.code) > 0) # We're going to try the same thing but with char2 - # char2 being a player for our test, the event won't be validated. - er = self.call(CmdEvent(), "/add out = traverse", caller=self.char2) + # char2 being a player for our test, the callback won't be validated. + self.call(CmdCallback(), "/add out = traverse", caller=self.char2) editor = self.char2.ndb._eveditor self.assertIsNotNone(editor) - # Edit the event + # Edit the callback editor.update_buffer(dedent(""" character.msg("No way.") """.strip("\n"))) editor.save_buffer() editor.quit() - event = self.exit.events.get("traverse")[1] - self.assertEqual(event.author, self.char2) - self.assertEqual(event.valid, False) - self.assertTrue(len(event.code) > 0) + callback = self.exit.callbacks.get("traverse")[1] + self.assertEqual(callback.author, self.char2) + self.assertEqual(callback.valid, False) + self.assertTrue(len(callback.code) > 0) def test_del(self): - """Add and remove an event.""" - self.handler.add_event(self.exit, "traverse", "pass", + """Add and remove an callback.""" + self.handler.add_callback(self.exit, "traverse", "pass", author=self.char1, valid=True) - # Try to delete the event - # char2 shouldn't be allowed to do so (that's not HIS event) - self.call(CmdEvent(), "/del out = traverse 1", caller=self.char2) - self.assertTrue(len(self.handler.get_events(self.exit).get( + # Try to delete the callback + # char2 shouldn't be allowed to do so (that's not HIS callback) + self.call(CmdCallback(), "/del out = traverse 1", caller=self.char2) + self.assertTrue(len(self.handler.get_callbacks(self.exit).get( "traverse", [])) == 1) # Now, char1 should be allowed to delete it - self.call(CmdEvent(), "/del out = traverse 1") - self.assertTrue(len(self.handler.get_events(self.exit).get( + self.call(CmdCallback(), "/del out = traverse 1") + self.assertTrue(len(self.handler.get_callbacks(self.exit).get( "traverse", [])) == 0) def test_lock(self): """Test the lock of multiple editing.""" - self.call(CmdEvent(), "/add here = time 8:00", caller=self.char2) + self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2) self.assertIsNotNone(self.char2.ndb._eveditor) # Now ask char1 to edit - line = self.call(CmdEvent(), "/edit here = time 1") + line = self.call(CmdCallback(), "/edit here = time 1") self.assertIsNone(self.char1.ndb._eveditor) - # Try to delete this event while char2 is editing it - line = self.call(CmdEvent(), "/del here = time 1") + # Try to delete this callback while char2 is editing it + line = self.call(CmdCallback(), "/del here = time 1") def test_accept(self): - """Accept an event.""" - self.call(CmdEvent(), "/add here = time 8:00", caller=self.char2) + """Accept an callback.""" + self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2) editor = self.char2.ndb._eveditor self.assertIsNotNone(editor) - # Edit the event + # Edit the callback editor.update_buffer(dedent(""" room.msg_contents("It's 8 PM, everybody up!") """.strip("\n"))) editor.save_buffer() editor.quit() - event = self.room1.events.get("time")[0] - self.assertEqual(event.valid, False) + callback = self.room1.callbacks.get("time")[0] + self.assertEqual(callback.valid, False) - # chars shouldn't be allowed to the event - self.call(CmdEvent(), "/accept here = time 1", caller=self.char2) - event = self.room1.events.get("time")[0] - self.assertEqual(event.valid, False) + # chars shouldn't be allowed to the callback + self.call(CmdCallback(), "/accept here = time 1", caller=self.char2) + callback = self.room1.callbacks.get("time")[0] + self.assertEqual(callback.valid, False) - # char1 will accept the event - self.call(CmdEvent(), "/accept here = time 1") - event = self.room1.events.get("time")[0] - self.assertEqual(event.valid, True) + # char1 will accept the callback + self.call(CmdCallback(), "/accept here = time 1") + callback = self.room1.callbacks.get("time")[0] + self.assertEqual(callback.valid, True) -class TestDefaultEvents(CommandTest): +class TestDefaultCallbacks(CommandTest): - """Test the default events.""" + """Test the default callbacks.""" def setUp(self): - """Create the event handler.""" - super(TestDefaultEvents, self).setUp() + """Create the callback handler.""" + super(TestDefaultCallbacks, self).setUp() self.handler = create_script( "evennia.contrib.events.scripts.EventHandler") @@ -434,13 +410,13 @@ class TestDefaultEvents(CommandTest): self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit") def tearDown(self): - """Stop the event handler.""" + """Stop the callback handler.""" self.handler.stop() - EventsHandler.script = None - super(TestDefaultEvents, self).tearDown() + CallbackHandler.script = None + super(TestDefaultCallbacks, self).tearDown() def test_exit(self): - """Test the events of an exit.""" + """Test the callbacks of an exit.""" self.char1.key = "char1" code = dedent(""" if character.key == "char1": @@ -452,8 +428,8 @@ class TestDefaultEvents(CommandTest): # Enforce self.exit.destination since swapping typeclass lose it self.exit.destination = self.room2 - # Try the can_traverse event - self.handler.add_event(self.exit, "can_traverse", code, + # Try the can_traverse callback + self.handler.add_callback(self.exit, "can_traverse", code, author=self.char1, valid=True) # Have char1 move through the exit @@ -465,15 +441,15 @@ class TestDefaultEvents(CommandTest): caller=self.char2) self.assertIs(self.char2.location, self.room1) - # Try the traverse event - self.handler.del_event(self.exit, "can_traverse", 0) - self.handler.add_event(self.exit, "traverse", "character.msg('Fine!')", + # Try the traverse callback + self.handler.del_callback(self.exit, "can_traverse", 0) + self.handler.add_callback(self.exit, "traverse", "character.msg('Fine!')", author=self.char1, valid=True) # Have char2 move through the exit self.call(ExitCommand(), "", obj=self.exit, caller=self.char2) self.assertIs(self.char2.location, self.room2) - self.handler.del_event(self.exit, "traverse", 0) + self.handler.del_callback(self.exit, "traverse", 0) # Move char1 and char2 back self.char1.location = self.room1 @@ -481,7 +457,7 @@ class TestDefaultEvents(CommandTest): # Test msg_arrive and msg_leave code = 'message = "{character} goes out."' - self.handler.add_event(self.exit, "msg_leave", code, + self.handler.add_callback(self.exit, "msg_leave", code, author=self.char1, valid=True) # Have char1 move through the exit @@ -502,7 +478,7 @@ class TestDefaultEvents(CommandTest): back = create_object("evennia.objects.objects.DefaultExit", key="in", location=self.room2, destination=self.room1) code = 'message = "{character} goes in."' - self.handler.add_event(self.exit, "msg_arrive", code, + self.handler.add_callback(self.exit, "msg_arrive", code, author=self.char1, valid=True) # Have char1 move through the exit diff --git a/evennia/contrib/events/typeclasses.py b/evennia/contrib/events/typeclasses.py index f6b3724197..3e67ef9c98 100644 --- a/evennia/contrib/events/typeclasses.py +++ b/evennia/contrib/events/typeclasses.py @@ -9,14 +9,175 @@ EventRoom, EventCharacter and EventExit). from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom from evennia import ScriptDB from evennia.utils.utils import delay, inherits_from, lazy_property -from evennia.contrib.events.custom import ( - create_event_type, invalidate_event_type, create_time_event, phrase_event) -from evennia.contrib.events.handler import EventsHandler +from evennia.contrib.events.utils import register_events, time_event, phrase_event +from evennia.contrib.events.handler import CallbackHandler +# Character help +CHARACTER_CAN_DELETE = """ +Can the character be deleted? +This event is called before the character is deleted. You can use +'deny()' in this event to prevent this character from being deleted. +If this event doesn't prevent the character from being deleted, its +'delete' event is called right away. + +Variables you can use in this event: + character: the character connected to this event. +""" + +CHARACTER_CAN_MOVE = """ +Can the character move? +This event is called before the character moves into another +location. You can prevent the character from moving +using the 'deny()' function. + +Variables you can use in this event: + character: the character connected to this event. + origin: the current location of the character. + destination: the future location of the character. +""" + +CHARACTER_CAN_PART = """ +Can the departing charaacter leave this room? +This event is called before another character can move from the +location where the current character also is. This event can be +used to prevent someone to leave this room if, for instance, he/she +hasn't paid, or he/she is going to a protected area, past a guard, +and so on. Use 'deny()' to prevent the departing character from +moving. + +Variables you can use in this event: + departing: the character who wants to leave this room. + character: the character connected to this event. +""" + +CHARACTER_CAN_SAY = """ +Before another character can say something in the same location. +This event is called before another character says something in the +character's location. The "something" in question can be modified, +or the action can be prevented by using 'deny()'. To change the +content of what the character says, simply change the variable +'message' to another string of characters. + +Variables you can use in this event: + speaker: the character who is using the say command. + character: the character connected to this event. + message: the text spoken by the character. +""" + +CHARACTER_DELETE = """ +Before deleting the character. +This event is called just before deleting this character. It shouldn't +be prevented (using the `deny()` function at this stage doesn't +have any effect). If you want to prevent deletion of this character, +use the event `can_delete` instead. + +Variables you can use in this event: + character: the character connected to this event. +""" + +CHARACTER_GREET = """ +A new character arrives in the location of this character. +This event is called when another character arrives in the location +where the current character is. For instance, a puppeted character +arrives in the shop of a shopkeeper (assuming the shopkeeper is +a character). As its name suggests, this event can be very useful +to have NPC greeting one another, or players, who come to visit. + +Variables you can use in this event: + character: the character connected to this event. + newcomer: the character arriving in the same location. +""" + +CHARACTER_MOVE = """ +After the character has moved into its new room. +This event is called when the character has moved into a new +room. It is too late to prevent the move at this point. + +Variables you can use in this event: + character: the character connected to this event. + origin: the old location of the character. + destination: the new location of the character. +""" + +CHARACTER_PUPPETED = """ +When the character has been puppeted by a player. +This event is called when a player has just puppeted this character. +This can commonly happen when a player connects onto this character, +or when puppeting to a NPC or free character. + +Variables you can use in this event: + character: the character connected to this event. +""" + +CHARACTER_SAY = """ +After another character has said something in the character's room. +This event is called right after another character has said +something in the same location.. The action cannot be prevented +at this moment. Instead, this event is ideal to create keywords +that would trigger a character (like a NPC) in doing something +if a specific phrase is spoken in the same location. +To use this event, you have to specify a list of keywords as +parameters that should be present, as separate words, in the +spoken phrase. For instance, you can set an event tthat would +fire if the phrase spoken by the character contains "menu" or +"dinner" or "lunch": + @event/add ... = say menu, dinner, lunch +Then if one of the words is present in what the character says, +this event will fire. + +Variables you can use in this event: + speaker: the character speaking in this room. + character: the character connected to this event. + message: the text having been spoken by the character. +""" + +CHARACTER_TIME = """ +A repeated event to be called regularly. +This event is scheduled to repeat at different times, specified +as parameters. You can set it to run every day at 8:00 AM (game +time). You have to specify the time as an argument to @event/add, like: + @event/add here = time 8:00 +The parameter (8:00 here) must be a suite of digits separated by +spaces, colons or dashes. Keep it as close from a recognizable +date format, like this: + @event/add here = time 06-15 12:20 +This event will fire every year on June the 15th at 12 PM (still +game time). Units have to be specified depending on your set calendar +(ask a developer for more details). + +Variables you can use in this event: + character: the character connected to this event. +""" + +CHARACTER_UNPUPPETED = """ +When the character is about to be un-puppeted. +This event is called when a player is about to un-puppet the +character, which can happen if the player is disconnecting or +changing puppets. + +Variables you can use in this event: + character: the character connected to this event. +""" + +@register_events class EventCharacter(DefaultCharacter): """Typeclass to represent a character and call event types.""" + _events = { + "can_delete": (["character"], CHARACTER_CAN_DELETE), + "can_move": (["character", "origin", "destination"], CHARACTER_CAN_MOVE), + "can_part": (["character", "departing"], CHARACTER_CAN_PART), + "can_say": (["speaker", "character", "message"], CHARACTER_CAN_SAY, phrase_event), + "delete": (["character"], CHARACTER_DELETE), + "greet": (["character", "newcomer"], CHARACTER_GREET), + "move": (["character", "origin", "destination"], CHARACTER_MOVE), + "puppeted": (["character"], CHARACTER_PUPPETED), + "say": (["speaker", "character", "message"], CHARACTER_SAY, phrase_event), + "time": (["character"], CHARACTER_TIME, None, time_event), + "unpuppeted": (["character"], CHARACTER_UNPUPPETED), + } + def announce_move_from(self, destination, msg=None, mapping=None): """ Called if the move is to be announced. This is @@ -51,10 +212,10 @@ class EventCharacter(DefaultCharacter): }) if exits: - exits[0].events.call("msg_leave", self, exits[0], + exits[0].callbacks.call("msg_leave", self, exits[0], location, destination, string, mapping) - string = exits[0].events.get_variable("message") - mapping = exits[0].events.get_variable("mapping") + string = exits[0].callbacks.get_variable("message") + mapping = exits[0].callbacks.get_variable("mapping") # If there's no string, don't display anything # It can happen if the "message" variable in events is set to None @@ -106,10 +267,10 @@ class EventCharacter(DefaultCharacter): if origin: exits = [o for o in destination.contents if o.location is destination and o.destination is origin] if exits: - exits[0].events.call("msg_arrive", self, exits[0], + exits[0].callbacks.call("msg_arrive", self, exits[0], origin, destination, string, mapping) - string = exits[0].events.get_variable("message") - mapping = exits[0].events.get_variable("mapping") + string = exits[0].callbacks.get_variable("message") + mapping = exits[0].callbacks.get_variable("mapping") # If there's no string, don't display anything # It can happen if the "message" variable in events is set to None @@ -137,15 +298,15 @@ class EventCharacter(DefaultCharacter): origin = self.location Room = DefaultRoom if isinstance(origin, Room) and isinstance(destination, Room): - can = self.events.call("can_move", self, + can = self.callbacks.call("can_move", self, origin, destination) if can: - can = origin.events.call("can_move", self, origin) + can = origin.callbacks.call("can_move", self, origin) if can: # Call other character's 'can_part' event for present in [o for o in origin.contents if isinstance( o, DefaultCharacter) and o is not self]: - can = present.events.call("can_part", present, self) + can = present.callbacks.call("can_part", present, self) if not can: break @@ -172,13 +333,13 @@ class EventCharacter(DefaultCharacter): destination = self.location Room = DefaultRoom if isinstance(origin, Room) and isinstance(destination, Room): - self.events.call("move", self, origin, destination) - destination.events.call("move", self, origin, destination) + self.callbacks.call("move", self, origin, destination) + destination.callbacks.call("move", self, origin, destination) # Call the 'greet' event of characters in the location for present in [o for o in destination.contents if isinstance( o, DefaultCharacter) and o is not self]: - present.events.call("greet", present, self) + present.callbacks.call("greet", present, self) def at_object_delete(self): """ @@ -187,10 +348,10 @@ class EventCharacter(DefaultCharacter): deletion is aborted. """ - if not self.events.call("can_delete", self): + if not self.callbacks.call("can_delete", self): return False - self.events.call("delete", self) + self.callbacks.call("delete", self) return True def at_post_puppet(self): @@ -207,12 +368,12 @@ class EventCharacter(DefaultCharacter): """ super(EventCharacter, self).at_post_puppet() - self.events.call("puppeted", self) + self.callbacks.call("puppeted", self) # Call the room's puppeted_in event location = self.location if location and isinstance(location, DefaultRoom): - location.events.call("puppeted_in", self, location) + location.callbacks.call("puppeted_in", self, location) def at_pre_unpuppet(self): """ @@ -226,20 +387,121 @@ class EventCharacter(DefaultCharacter): puppeting this Object. """ - self.events.call("unpuppeted", self) + self.callbacks.call("unpuppeted", self) # Call the room's unpuppeted_in event location = self.location if location and isinstance(location, DefaultRoom): - location.events.call("unpuppeted_in", self, location) + location.callbacks.call("unpuppeted_in", self, location) super(EventCharacter, self).at_pre_unpuppet() +# Exit help +EXIT_CAN_TRAVERSE = """ +Can the character traverse through this exit? +This event is called when a character is about to traverse this +exit. You can use the deny() function to deny the character from +exitting for this time. + +Variables you can use in this event: + character: the character that wants to traverse this exit. + exit: the exit to be traversed. + room: the room in which stands the character before moving. +""" + +EXIT_MSG_ARRIVE = """ +Customize the message when a character arrives through this exit. +This event is called when a character arrives through this exit. +To customize the message that will be sent to the room where the +character arrives, change the value of the variable "message" +to give it your custom message. The character itself will not be +notified. You can use mapping between braces, like this: + message = "{character} climbs out of a hole." +In your mapping, you can use {character} (the character who has +arrived), {exit} (the exit), {origin} (the room in which +the character was), and {destination} (the room in which the character +now is). If you need to customize the message with other information, +you can also set "message" to None and send something else instead. + +Variables you can use in this event: + character: the character who is arriving through this exit. + exit: the exit having been traversed. + origin: the past location of the character. + destination: the current location of the character. + message: the message to be displayed in the destination. + mapping: a dictionary containing the mapping of the message. +""" + +EXIT_MSG_LEAVE = """ +Customize the message when a character leaves through this exit. +This event is called when a character leaves through this exit. +To customize the message that will be sent to the room where the +character came from, change the value of the variable "message" +to give it your custom message. The character itself will not be +notified. You can use mapping between braces, like this: + message = "{character} falls into a hole!" +In your mapping, you can use {character} (the character who is +about to leave), {exit} (the exit), {origin} (the room in which +the character is), and {destination} (the room in which the character +is heading for). If you need to customize the message with other +information, you can also set "message" to None and send something +else instead. + +Variables you can use in this event: + character: the character who is leaving through this exit. + exit: the exit being traversed. + origin: the location of the character. + destination: the destination of the character. + message: the message to be displayed in the location. + mapping: a dictionary containing additional mapping. +""" + +EXIT_TIME = """ +A repeated event to be called regularly. +This event is scheduled to repeat at different times, specified +as parameters. You can set it to run every day at 8:00 AM (game +time). You have to specify the time as an argument to @event/add, like: + @event/add north = time 8:00 +The parameter (8:00 here) must be a suite of digits separated by +spaces, colons or dashes. Keep it as close from a recognizable +date format, like this: + @event/add south = time 06-15 12:20 +This event will fire every year on June the 15th at 12 PM (still +game time). Units have to be specified depending on your set calendar +(ask a developer for more details). + +Variables you can use in this event: + exit: the exit connected to this event. +""" + +EXIT_TRAVERSE = """ +After the characer has traversed through this exit. +This event is called after a character has traversed through this +exit. Traversing cannot be prevented using 'deny()' at this +point. The character will be in a different room and she will +have received the room's description when this event is called. + +Variables you can use in this event: + character: the character who has traversed through this exit. + exit: the exit that was just traversed through. + origin: the exit's location (where the character was before moving). + destination: the character's location after moving. +""" + +@register_events class EventExit(DefaultExit): """Modified exit including management of events.""" + _events = { + "can_traverse": (["character", "exit", "room"], EXIT_CAN_TRAVERSE), + "msg_arrive": (["character", "exit", "origin", "destination", "message", "mapping"], EXIT_MSG_ARRIVE), + "msg_leave": (["character", "exit", "origin", "destination", "message", "mapping"], EXIT_MSG_LEAVE), + "time": (["exit"], EXIT_TIME, None, time_event), + "traverse": (["character", "exit", "origin", "destination"], EXIT_TRAVERSE), + } + def at_traverse(self, traversing_object, target_location): """ This hook is responsible for handling the actual traversal, @@ -257,7 +519,7 @@ class EventExit(DefaultExit): """ is_character = inherits_from(traversing_object, DefaultCharacter) if is_character: - allow = self.events.call("can_traverse", traversing_object, + allow = self.callbacks.call("can_traverse", traversing_object, self, self.location) if not allow: return @@ -266,14 +528,237 @@ class EventExit(DefaultExit): # After traversing if is_character: - self.events.call("traverse", traversing_object, + self.callbacks.call("traverse", traversing_object, self, self.location, self.destination) +# Object help +OBJECT_DROP = """ +When a character drops this object. +This event is called when a character drops this object. It is +called after the command has ended and displayed its message, and +the action cannot be prevented at this time. + +Variables you can use in this event: + character: the character having dropped the object. + obj: the object connected to this event. +""" + +OBJECT_GET = """ +When a character gets this object. +This event is called when a character gets this object. It is +called after the command has ended and displayed its message, and +the action cannot be prevented at this time. + +Variables you can use in this event: + character: the character having picked up the object. + obj: the object connected to this event. +""" + +OBJECT_TIME = """ +A repeated event to be called regularly. +This event is scheduled to repeat at different times, specified +as parameters. You can set it to run every day at 8:00 AM (game +time). You have to specify the time as an argument to @event/add, like: + @event/add here = time 8:00 +The parameter (8:00 here) must be a suite of digits separated by +spaces, colons or dashes. Keep it as close from a recognizable +date format, like this: + @event/add here = time 06-15 12:20 +This event will fire every year on June the 15th at 12 PM (still +game time). Units have to be specified depending on your set calendar +(ask a developer for more details). + +Variables you can use in this event: + object: the object connected to this event. +""" + +@register_events +class EventObject(DefaultObject): + + """Default object with management of events.""" + + _events = { + "drop": (["character", "obj"], OBJECT_DROP), + "get": (["character", "obj"], OBJECT_GET), + "time": (["object"], OBJECT_TIME, None, time_event), + } + + @lazy_property + def callbacks(self): + """Return the CallbackHandler.""" + return CallbackHandler(self) + + def at_get(self, getter): + """ + Called by the default `get` command when this object has been + picked up. + + Args: + getter (Object): The object getting this object. + + Notes: + This hook cannot stop the pickup from happening. Use + permissions for that. + + """ + super(EventObject, self).at_get(getter) + self.callbacks.call("get", getter, self) + + def at_drop(self, dropper): + """ + Called by the default `drop` command when this object has been + dropped. + + Args: + dropper (Object): The object which just dropped this object. + + Notes: + This hook cannot stop the drop from happening. Use + permissions from that. + + """ + super(EventObject, self).at_drop(dropper) + self.callbacks.call("drop", dropper, self) + +# Room help +ROOM_CAN_DELETE = """ +Can the room be deleted? +This event is called before the room is deleted. You can use +'deny()' in this event to prevent this room from being deleted. +If this event doesn't prevent the room from being deleted, its +'delete' event is called right away. + +Variables you can use in this event: + room: the room connected to this event. +""" + +ROOM_CAN_MOVE = """ +Can the character move into this room? +This event is called before the character can move into this +specific room. You can prevent the move by using the 'deny()' +function. + +Variables you can use in this event: + character: the character who wants to move in this room. + room: the room connected to this event. +""" + +ROOM_CAN_SAY = """ +Before a character can say something in this room. +This event is called before a character says something in this +room. The "something" in question can be modified, or the action +can be prevented by using 'deny()'. To change the content of what +the character says, simply change the variable 'message' to another +string of characters. + +Variables you can use in this event: + character: the character who is using the say command. + room: the room connected to this event. + message: the text spoken by the character. +""" + +ROOM_DELETE = """ +Before deleting the room. +This event is called just before deleting this room. It shouldn't +be prevented (using the `deny()` function at this stage doesn't +have any effect). If you want to prevent deletion of this room, +use the event `can_delete` instead. + +Variables you can use in this event: + room: the room connected to this event. +""" + +ROOM_MOVE = """ +After the character has moved into this room. +This event is called when the character has moved into this +room. It is too late to prevent the move at this point. + +Variables you can use in this event: + character: the character connected to this event. + origin: the old location of the character. + destination: the new location of the character. +""" + +ROOM_PUPPETED_IN = """ +After the character has been puppeted in this room. +This event is called after a character has been puppeted in this +room. This can happen when a player, having connected, begins +to puppet a character. The character's location at this point, +if it's a room, will see this event fire. + +Variables you can use in this event: + character: the character who have just been puppeted in this room. + room: the room connected to this event. +""" + +ROOM_SAY = """ +After the character has said something in the room. +This event is called right after a character has said something +in this room. The action cannot be prevented at this moment. +Instead, this event is ideal to create actions that will respond +to something being said aloud. To use this event, you have to +specify a list of keywords as parameters that should be present, +as separate words, in the spoken phrase. For instance, you can +set an event tthat would fire if the phrase spoken by the character +contains "menu" or "dinner" or "lunch": + @event/add ... = say menu, dinner, lunch +Then if one of the words is present in what the character says, +this event will fire. + +Variables you can use in this event: + character: the character having spoken in this room. + room: the room connected to this event. + message: the text having been spoken by the character. +""" + +ROOM_TIME = """ +A repeated event to be called regularly. +This event is scheduled to repeat at different times, specified +as parameters. You can set it to run every day at 8:00 AM (game +time). You have to specify the time as an argument to @event/add, like: + @event/add here = time 8:00 +The parameter (8:00 here) must be a suite of digits separated by +spaces, colons or dashes. Keep it as close from a recognizable +date format, like this: + @event/add here = time 06-15 12:20 +This event will fire every year on June the 15th at 12 PM (still +game time). Units have to be specified depending on your set calendar +(ask a developer for more details). + +Variables you can use in this event: + room: the room connected to this event. +""" + +ROOM_UNPUPPETED_IN = """ +Before the character is un-puppeted in this room. +This event is called before a character is un-puppeted in this +room. This can happen when a player, puppeting a character, is +disconnecting. The character's location at this point, if it's a +room, will see this event fire. + +Variables you can use in this event: + character: the character who is about to be un-puppeted in this room. + room: the room connected to this event. +""" + +@register_events class EventRoom(DefaultRoom): """Default room with management of events.""" + _events = { + "can_delete": (["room"], ROOM_CAN_DELETE), + "can_move": (["character", "room"], ROOM_CAN_MOVE), + "can_say": (["character", "room", "message"], ROOM_CAN_SAY, phrase_event), + "delete": (["room"], ROOM_DELETE), + "move": (["character", "origin", "destination"], ROOM_MOVE), + "puppeted_in": (["character", "room"], ROOM_PUPPETED_IN), + "say": (["character", "room", "message"], ROOM_SAY, phrase_event), + "time": (["room"], ROOM_TIME, None, time_event), + "unpuppeted_in": (["character", "room"], ROOM_UNPUPPETED_IN), + } + def at_object_delete(self): """ Called just before the database object is permanently @@ -281,10 +766,10 @@ class EventRoom(DefaultRoom): deletion is aborted. """ - if not self.events.call("can_delete", self): + if not self.callbacks.call("can_delete", self): return False - self.events.call("delete", self) + self.callbacks.call("delete", self) return True def at_say(self, speaker, message): @@ -303,463 +788,30 @@ class EventRoom(DefaultRoom): this. """ - allow = self.events.call("can_say", speaker, self, message, + allow = self.callbacks.call("can_say", speaker, self, message, parameters=message) if not allow: return - message = self.events.get_variable("message") + message = self.callbacks.get_variable("message") # Call the event "can_say" of other characters in the location for present in [o for o in self.contents if isinstance( o, DefaultCharacter) and o is not speaker]: - allow = present.events.call("can_say", speaker, present, + allow = present.callbacks.call("can_say", speaker, present, message, parameters=message) if not allow: return - message = present.events.get_variable("message") + message = present.callbacks.get_variable("message") # We force the next event to be called after the message # This will have to change when the Evennia API adds new hooks - delay(0, self.events.call, "say", speaker, self, message, + delay(0, self.callbacks.call, "say", speaker, self, message, parameters=message) for present in [o for o in self.contents if isinstance( o, DefaultCharacter) and o is not speaker]: - delay(0, present.events.call, "say", speaker, present, message, + delay(0, present.callbacks.call, "say", speaker, present, message, parameters=message) return message - - -class EventObject(DefaultObject): - - """Default object with management of events.""" - - @lazy_property - def events(self): - """Return the EventsHandler.""" - return EventsHandler(self) - - def at_get(self, getter): - """ - Called by the default `get` command when this object has been - picked up. - - Args: - getter (Object): The object getting this object. - - Notes: - This hook cannot stop the pickup from happening. Use - permissions for that. - - """ - super(EventObject, self).at_get(getter) - self.events.call("get", getter, self) - - def at_drop(self, dropper): - """ - Called by the default `drop` command when this object has been - dropped. - - Args: - dropper (Object): The object which just dropped this object. - - Notes: - This hook cannot stop the drop from happening. Use - permissions from that. - - """ - super(EventObject, self).at_drop(dropper) - self.events.call("drop", dropper, self) - -## Default events -# Character events -create_event_type(DefaultCharacter, "can_move", ["character", - "origin", "destination"], """ - Can the character move? - This event is called before the character moves into another - location. You can prevent the character from moving - using the 'deny()' function. - - Variables you can use in this event: - character: the character connected to this event. - origin: the current location of the character. - destination: the future location of the character. - """) -create_event_type(DefaultCharacter, "can_delete", ["character"], """ - Can the character be deleted? - This event is called before the character is deleted. You can use - 'deny()' in this event to prevent this character from being deleted. - If this event doesn't prevent the character from being deleted, its - 'delete' event is called right away. - - Variables you can use in this event: - character: the character connected to this event. - """) -create_event_type(DefaultCharacter, "can_part", ["character", "departing"], """ - Can the departing charaacter leave this room? - This event is called before another character can move from the - location where the current character also is. This event can be - used to prevent someone to leave this room if, for instance, he/she - hasn't paid, or he/she is going to a protected area, past a guard, - and so on. Use 'deny()' to prevent the departing character from - moving. - - Variables you can use in this event: - departing: the character who wants to leave this room. - character: the character connected to this event. - """) -create_event_type(DefaultCharacter, "can_say", ["speaker", "character", "message"], """ - Before another character can say something in the same location. - This event is called before another character says something in the - character's location. The "something" in question can be modified, - or the action can be prevented by using 'deny()'. To change the - content of what the character says, simply change the variable - 'message' to another string of characters. - - Variables you can use in this event: - speaker: the character who is using the say command. - character: the character connected to this event. - message: the text spoken by the character. - """, custom_call=phrase_event) -create_event_type(DefaultCharacter, "delete", ["character"], """ - Before deleting the character. - This event is called just before deleting this character. It shouldn't - be prevented (using the `deny()` function at this stage doesn't - have any effect). If you want to prevent deletion of this character, - use the event `can_delete` instead. - - Variables you can use in this event: - character: the character connected to this event. - """) -invalidate_event_type(DefaultCharacter, "drop") -invalidate_event_type(DefaultCharacter, "get") -create_event_type(DefaultCharacter, "greet", ["character", "newcomer"], """ - A new character arrives in the location of this character. - This event is called when another character arrives in the location - where the current character is. For instance, a puppeted character - arrives in the shop of a shopkeeper (assuming the shopkeeper is - a character). As its name suggests, this event can be very useful - to have NPC greeting one another, or players, who come to visit. - - Variables you can use in this event: - character: the character connected to this event. - newcomer: the character arriving in the same location. - """) -create_event_type(DefaultCharacter, "move", ["character", - "origin", "destination"], """ - After the character has moved into its new room. - This event is called when the character has moved into a new - room. It is too late to prevent the move at this point. - - Variables you can use in this event: - character: the character connected to this event. - origin: the old location of the character. - destination: the new location of the character. - """) -create_event_type(DefaultCharacter, "puppeted", ["character"], """ - When the character has been puppeted by a player. - This event is called when a player has just puppeted this character. - This can commonly happen when a player connects onto this character, - or when puppeting to a NPC or free character. - - Variables you can use in this event: - character: the character connected to this event. - """) -create_event_type(DefaultCharacter, "say", ["speaker", "character", "message"], """ - After another character has said something in the character's room. - This event is called right after another character has said - something in the same location.. The action cannot be prevented - at this moment. Instead, this event is ideal to create keywords - that would trigger a character (like a NPC) in doing something - if a specific phrase is spoken in the same location. - To use this event, you have to specify a list of keywords as - parameters that should be present, as separate words, in the - spoken phrase. For instance, you can set an event tthat would - fire if the phrase spoken by the character contains "menu" or - "dinner" or "lunch": - @event/add ... = say menu, dinner, lunch - Then if one of the words is present in what the character says, - this event will fire. - - Variables you can use in this event: - speaker: the character speaking in this room. - character: the character connected to this event. - message: the text having been spoken by the character. - """, custom_call=phrase_event) -create_event_type(DefaultCharacter, "time", ["character"], """ - A repeated event to be called regularly. - This event is scheduled to repeat at different times, specified - as parameters. You can set it to run every day at 8:00 AM (game - time). You have to specify the time as an argument to @event/add, like: - @event/add here = time 8:00 - The parameter (8:00 here) must be a suite of digits separated by - spaces, colons or dashes. Keep it as close from a recognizable - date format, like this: - @event/add here = time 06-15 12:20 - This event will fire every year on June the 15th at 12 PM (still - game time). Units have to be specified depending on your set calendar - (ask a developer for more details). - - Variables you can use in this event: - character: the character connected to this event. - """, create_time_event) -create_event_type(DefaultCharacter, "unpuppeted", ["character"], """ - When the character is about to be un-puppeted. - This event is called when a player is about to un-puppet the - character, which can happen if the player is disconnecting or - changing puppets. - - Variables you can use in this event: - character: the character connected to this event. - """) - -# Object events -create_event_type(DefaultObject, "drop", ["character", "obj"], """ - When a character drops this object. - This event is called when a character drops this object. It is - called after the command has ended and displayed its message, and - the action cannot be prevented at this time. - - Variables you can use in this event: - character: the character having dropped the object. - obj: the object connected to this event. - """) -create_event_type(DefaultObject, "get", ["character", "obj"], """ - When a character gets this object. - This event is called when a character gets this object. It is - called after the command has ended and displayed its message, and - the action cannot be prevented at this time. - - Variables you can use in this event: - character: the character having picked up the object. - obj: the object connected to this event. - """) -create_event_type(DefaultObject, "time", ["object"], """ - A repeated event to be called regularly. - This event is scheduled to repeat at different times, specified - as parameters. You can set it to run every day at 8:00 AM (game - time). You have to specify the time as an argument to @event/add, like: - @event/add here = time 8:00 - The parameter (8:00 here) must be a suite of digits separated by - spaces, colons or dashes. Keep it as close from a recognizable - date format, like this: - @event/add here = time 06-15 12:20 - This event will fire every year on June the 15th at 12 PM (still - game time). Units have to be specified depending on your set calendar - (ask a developer for more details). - - Variables you can use in this event: - object: the object connected to this event. - """, create_time_event) - -# Exit events -create_event_type(DefaultExit, "can_traverse", ["character", "exit", "room"], - """ - Can the character traverse through this exit? - This event is called when a character is about to traverse this - exit. You can use the deny() function to deny the character from - exitting for this time. - - Variables you can use in this event: - character: the character that wants to traverse this exit. - exit: the exit to be traversed. - room: the room in which stands the character before moving. - """) -invalidate_event_type(DefaultExit, "drop") -invalidate_event_type(DefaultExit, "get") -create_event_type(DefaultExit, "msg_arrive", ["character", "exit", - "origin", "destination", "message", "mapping"], """ - Customize the message when a character arrives through this exit. - This event is called when a character arrives through this exit. - To customize the message that will be sent to the room where the - character arrives, change the value of the variable "message" - to give it your custom message. The character itself will not be - notified. You can use mapping between braces, like this: - message = "{character} climbs out of a hole." - In your mapping, you can use {character} (the character who has - arrived), {exit} (the exit), {origin} (the room in which - the character was), and {destination} (the room in which the character - now is). If you need to customize the message with other information, - you can also set "message" to None and send something else instead. - - Variables you can use in this event: - character: the character who is arriving through this exit. - exit: the exit having been traversed. - origin: the past location of the character. - destination: the current location of the character. - message: the message to be displayed in the destination. - mapping: a dictionary containing the mapping of the message. - """) -create_event_type(DefaultExit, "msg_leave", ["character", "exit", - "origin", "destination", "message", "mapping"], """ - Customize the message when a character leaves through this exit. - This event is called when a character leaves through this exit. - To customize the message that will be sent to the room where the - character came from, change the value of the variable "message" - to give it your custom message. The character itself will not be - notified. You can use mapping between braces, like this: - message = "{character} falls into a hole!" - In your mapping, you can use {character} (the character who is - about to leave), {exit} (the exit), {origin} (the room in which - the character is), and {destination} (the room in which the character - is heading for). If you need to customize the message with other - information, you can also set "message" to None and send something - else instead. - - Variables you can use in this event: - character: the character who is leaving through this exit. - exit: the exit being traversed. - origin: the location of the character. - destination: the destination of the character. - message: the message to be displayed in the location. - mapping: a dictionary containing additional mapping. - """) -create_event_type(DefaultExit, "time", ["exit"], """ - A repeated event to be called regularly. - This event is scheduled to repeat at different times, specified - as parameters. You can set it to run every day at 8:00 AM (game - time). You have to specify the time as an argument to @event/add, like: - @event/add north = time 8:00 - The parameter (8:00 here) must be a suite of digits separated by - spaces, colons or dashes. Keep it as close from a recognizable - date format, like this: - @event/add south = time 06-15 12:20 - This event will fire every year on June the 15th at 12 PM (still - game time). Units have to be specified depending on your set calendar - (ask a developer for more details). - - Variables you can use in this event: - exit: the exit connected to this event. - """, create_time_event) -create_event_type(DefaultExit, "traverse", ["character", "exit", - "origin", "destination"], """ - After the characer has traversed through this exit. - This event is called after a character has traversed through this - exit. Traversing cannot be prevented using 'deny()' at this - point. The character will be in a different room and she will - have received the room's description when this event is called. - - Variables you can use in this event: - character: the character who has traversed through this exit. - exit: the exit that was just traversed through. - origin: the exit's location (where the character was before moving). - destination: the character's location after moving. - """) - -# Room events -create_event_type(DefaultRoom, "can_delete", ["room"], """ - Can the room be deleted? - This event is called before the room is deleted. You can use - 'deny()' in this event to prevent this room from being deleted. - If this event doesn't prevent the room from being deleted, its - 'delete' event is called right away. - - Variables you can use in this event: - room: the room connected to this event. - """) -create_event_type(DefaultRoom, "can_move", ["character", "room"], """ - Can the character move into this room? - This event is called before the character can move into this - specific room. You can prevent the move by using the 'deny()' - function. - - Variables you can use in this event: - character: the character who wants to move in this room. - room: the room connected to this event. - """) -create_event_type(DefaultRoom, "can_say", ["character", "room", "message"], """ - Before a character can say something in this room. - This event is called before a character says something in this - room. The "something" in question can be modified, or the action - can be prevented by using 'deny()'. To change the content of what - the character says, simply change the variable 'message' to another - string of characters. - - Variables you can use in this event: - character: the character who is using the say command. - room: the room connected to this event. - message: the text spoken by the character. - """, custom_call=phrase_event) -create_event_type(DefaultRoom, "delete", ["room"], """ - Before deleting the room. - This event is called just before deleting this room. It shouldn't - be prevented (using the `deny()` function at this stage doesn't - have any effect). If you want to prevent deletion of this room, - use the event `can_delete` instead. - - Variables you can use in this event: - room: the room connected to this event. - """) -invalidate_event_type(DefaultRoom, "drop") -invalidate_event_type(DefaultRoom, "get") -create_event_type(DefaultRoom, "move", ["character", - "origin", "destination"], """ - After the character has moved into this room. - This event is called when the character has moved into this - room. It is too late to prevent the move at this point. - - Variables you can use in this event: - character: the character connected to this event. - origin: the old location of the character. - destination: the new location of the character. - """) -create_event_type(DefaultRoom, "puppeted_in", ["character", "room"], """ - After the character has been puppeted in this room. - This event is called after a character has been puppeted in this - room. This can happen when a player, having connected, begins - to puppet a character. The character's location at this point, - if it's a room, will see this event fire. - - Variables you can use in this event: - character: the character who have just been puppeted in this room. - room: the room connected to this event. - """) -create_event_type(DefaultRoom, "say", ["character", "room", "message"], """ - After the character has said something in the room. - This event is called right after a character has said something - in this room. The action cannot be prevented at this moment. - Instead, this event is ideal to create actions that will respond - to something being said aloud. To use this event, you have to - specify a list of keywords as parameters that should be present, - as separate words, in the spoken phrase. For instance, you can - set an event tthat would fire if the phrase spoken by the character - contains "menu" or "dinner" or "lunch": - @event/add ... = say menu, dinner, lunch - Then if one of the words is present in what the character says, - this event will fire. - - Variables you can use in this event: - character: the character having spoken in this room. - room: the room connected to this event. - message: the text having been spoken by the character. - """, custom_call=phrase_event) -create_event_type(DefaultRoom, "time", ["room"], """ - A repeated event to be called regularly. - This event is scheduled to repeat at different times, specified - as parameters. You can set it to run every day at 8:00 AM (game - time). You have to specify the time as an argument to @event/add, like: - @event/add here = time 8:00 - The parameter (8:00 here) must be a suite of digits separated by - spaces, colons or dashes. Keep it as close from a recognizable - date format, like this: - @event/add here = time 06-15 12:20 - This event will fire every year on June the 15th at 12 PM (still - game time). Units have to be specified depending on your set calendar - (ask a developer for more details). - - Variables you can use in this event: - room: the room connected to this event. - """, create_time_event) -create_event_type(DefaultRoom, "unpuppeted_in", ["character", "room"], """ - Before the character is un-puppeted in this room. - This event is called before a character is un-puppeted in this - room. This can happen when a player, puppeting a character, is - disconnecting. The character's location at this point, if it's a - room, will see this event fire. - - Variables you can use in this event: - character: the character who is about to be un-puppeted in this room. - room: the room connected to this event. - """) diff --git a/evennia/contrib/events/utils.py b/evennia/contrib/events/utils.py new file mode 100644 index 0000000000..bc485b1529 --- /dev/null +++ b/evennia/contrib/events/utils.py @@ -0,0 +1,235 @@ +""" +Functions to extend the event system. + +These functions are to be used by developers to customize events and callbacks. + +""" + +from textwrap import dedent + +from django.conf import settings +from evennia import logger +from evennia import ScriptDB +from evennia.utils.create import create_script +from evennia.utils.gametime import real_seconds_until as standard_rsu +from evennia.utils.utils import class_from_module +from evennia.contrib.custom_gametime import UNITS +from evennia.contrib.custom_gametime import gametime_to_realtime +from evennia.contrib.custom_gametime import real_seconds_until as custom_rsu + +# Temporary storage for events waiting for the script to be started +EVENTS = [] + +def get_event_handler(): + """Return the event handler or None.""" + try: + script = ScriptDB.objects.get(db_key="event_handler") + except ScriptDB.DoesNotExist: + logger.log_trace("Can't get the event handler.") + script = None + + return script + +def register_events(path_or_typeclass): + """ + Register the events in this typeclass. + + Args: + path_or_typeclass (str or type): the Python path leading to the + class containing events, or the class itself. + + Returns: + The typeclass itself. + + Notes: + This function will read events from the `_events` class variable + defined in the typeclass given in parameters. It will add + the events, either to the script if it exists, or to some + temporary storage, waiting for the script to be initialized. + + """ + if isinstance(path_or_typeclass, basestring): + typeclass = class_from_module(path_or_typeclass) + else: + typeclass = path_or_typeclass + + typeclass_name = typeclass.__module__ + "." + typeclass.__name__ + try: + storage = ScriptDB.objects.get(db_key="event_handler") + assert storage.is_active + except (ScriptDB.DoesNotExist, AssertionError): + storage = EVENTS + + # If the script is started, add the event directly. + # Otherwise, add it to the temporary storage. + for name, tup in getattr(typeclass, "_events", {}).items(): + if len(tup) == 4: + variables, help_text, custom_call, custom_add = tup + elif len(tup) == 3: + variables, help_text, custom_call = tup + custom_add = None + elif len(tup) == 2: + variables, help_text = tup + custom_call = None + custom_add = None + else: + variables = help_text = custom_call = custom_add = None + + if isinstance(storage, list): + storage.append((typeclass_name, name, variables, help_text, custom_call, custom_add)) + else: + storage.add_event(typeclass_name, name, variables, help_text, custom_call, custom_add) + + return typeclass + +# Custom callbacks for specific event types +def get_next_wait(format): + """ + Get the length of time in seconds before format. + + Args: + format (str): a time format matching the set calendar. + + The time format could be something like "2018-01-08 12:00". The + number of units set in the calendar affects the way seconds are + calculated. + + Returns: + until (int or float): the number of seconds until the event. + usual (int or float): the usual number of seconds between events. + format (str): a string format representing the time. + + """ + calendar = getattr(settings, "EVENTS_CALENDAR", None) + if calendar is None: + logger.log_err("A time-related event has been set whereas " \ + "the gametime calendar has not been set in the settings.") + return + elif calendar == "standard": + rsu = standard_rsu + units = ["min", "hour", "day", "month", "year"] + elif calendar == "custom": + rsu = custom_rsu + back = dict([(value, name) for name, value in UNITS.items()]) + sorted_units = sorted(back.items()) + del sorted_units[0] + units = [n for v, n in sorted_units] + + params = {} + for delimiter in ("-", ":"): + format = format.replace(delimiter, " ") + + pieces = list(reversed(format.split())) + details = [] + i = 0 + for uname in units: + try: + piece = pieces[i] + except IndexError: + break + + if not piece.isdigit(): + logger.log_trace("The time specified '{}' in {} isn't " \ + "a valid number".format(piece, format)) + return + + # Convert the piece to int + piece = int(piece) + params[uname] = piece + details.append("{}={}".format(uname, piece)) + if i < len(units): + next_unit = units[i + 1] + else: + next_unit = None + i += 1 + + params["sec"] = 0 + details = " ".join(details) + until = rsu(**params) + usual = -1 + if next_unit: + kwargs = {next_unit: 1} + usual = gametime_to_realtime(**kwargs) + return until, usual, details + +def time_event(obj, event_name, number, parameters): + """ + Create a time-related event. + + Args: + obj (Object): the object on which stands the event. + event_name (str): the event's name. + number (int): the number of the event. + parameters (str): the parameter of the event. + + """ + seconds, usual, key = get_next_wait(parameters) + script = create_script("evennia.contrib.events.scripts.TimeEventScript", interval=seconds, obj=obj) + script.key = key + script.desc = "event on {}".format(key) + script.db.time_format = parameters + script.db.number = number + script.ndb.usual = usual + +def keyword_event(callbacks, parameters): + """ + Custom call for events with keywords (like push, or pull, or turn...). + + Args: + callbacks (list of dict): the list of callbacks to be called. + parameters (str): the actual parameters entered to trigger the callback. + + Returns: + A list containing the callback dictionaries to be called. + + Notes: + This function should be imported and added as a custom_call + parameter to add the event 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' callback can + be set to trigger when the player 'push 1' or 'push one'. + + """ + key = parameters.strip().lower() + to_call = [] + for callback in callbacks: + keys = callback["parameters"] + if not keys or key in [p.strip().lower() for p in keys.split(",")]: + to_call.append(callback) + + return to_call + +def phrase_event(callbacks, parameters): + """ + Custom call for events with keywords in sentences (like say or whisper). + + Args: + callbacks (list of dict): the list of callbacks to be called. + parameters (str): the actual parameters entered to trigger the callback. + + Returns: + A list containing the callback dictionaries to be called. + + Notes: + This function should be imported and added as a custom_call + parameter to add the event when the event supports keywords + in phrases as parameters. Keywords in parameters are one or more + words separated by a comma. For instance, a 'say yes, okay' callback + can be set to trigger when the player says something containing + either "yes" or "okay" (maybe 'say I don't like it, but okay'). + + """ + phrase = parameters.strip().lower() + # Remove punctuation marks + punctuations = ',.";?!' + for p in punctuations: + phrase = phrase.replace(p, " ") + words = phrase.split() + words = [w.strip("' ") for w in words if w.strip("' ")] + to_call = [] + for callback in callbacks: + keys = callback["parameters"] + if not keys or any(key.strip().lower() in words for key in keys.split(",")): + to_call.append(callback) + + return to_call