Merge pull request #1350 from vlegoff/ingame_python

Rename the event system into in-game Python system
This commit is contained in:
Griatch 2017-06-27 21:11:25 +02:00 committed by GitHub
commit 2f2d9e308a
9 changed files with 92 additions and 95 deletions

View file

@ -1,9 +1,9 @@
# Evennia event system
# Evennia in-game Python system
Vincent Le Goff 2017
This contrib adds the system of events in Evennia, allowing immortals (or other trusted builders) to
dynamically add features to individual objects. Using events, every immortal or privileged users
This contrib adds the system of in-game Python in Evennia, allowing immortals (or other trusted builders) to
dynamically add features to individual objects. Using custom Python set in-game, every immortal or privileged users
could have a specific room, exit, character, object or something else behave differently from its
"cousins". For these familiar with the use of softcode in MU`*`, like SMAUG MudProgs, the ability to
add arbitrary behavior to individual objects is a step toward freedom. Keep in mind, however, the
@ -11,26 +11,26 @@ warning below, and read it carefully before the rest of the documentation.
## A WARNING REGARDING SECURITY
Evennia's event system will run arbitrary Python code without much restriction. Such a system is as
Evennia's in-game Python system will run arbitrary Python code without much restriction. Such a system is as
powerful as potentially dangerous, and you will have to keep in mind these points before deciding to
install it:
1. Untrusted people can run Python code on your game server with this system. Be careful about who
can use this system (see the permissions below).
2. You can do all of this in Python outside the game. The event system is not to replace all your
2. You can do all of this in Python outside the game. The in-game Python system is not to replace all your
game feature.
## Basic structure and vocabulary
- At the basis of the event system are **events**. An **event** defines the context in which we
would like to call some arbitrary code. For instance, one event is defined on exits and will fire
every time a character traverses through this exit. Events are described on a
[typeclass](https://github.com/evennia/evennia/wiki/Typeclasses) (like
[exits](https://github.com/evennia/evennia/wiki/Objects#exits) in our example). All objects
inheriting from this typeclass will have access to this event.
- At the basis of the in-game Python system are **events**. An **event** defines the context in which we
would like to call some arbitrary code. For instance, one event is
defined on exits and will fire every time a character traverses through this exit. Events are described
on a [typeclass](https://github.com/evennia/evennia/wiki/Typeclasses) (like
[exits](https://github.com/evennia/evennia/wiki/Objects#exits) in our example). All objects inheriting
from this typeclass will have access to this event.
- **Callbacks** can be set on individual objects, on events defined in code. These **callbacks**
can contain arbitrary code and describe a specific behavior for an object. When the event fires,
all callbacks connected to this object's event are executed.
all callbacks connected to this object's event are executed.
To see the system in context, when an object is picked up (using the default `get` command), a
specific event is fired:
@ -41,10 +41,10 @@ specific event is fired:
the "get" event on this object.
4. All callbacks tied to this object's "get" event will be executed in order. These callbacks act
as functions containing Python code that you can write in-game, using specific variables that
will be listed when you edit the callback itself.
will be listed when you edit the callback itself.
5. In individual callbacks, you can add multiple lines of Python code that will be fired at this
point. In this example, the `character` variable will contain the character who has picked up
the object, while `obj` will contain the object that was picked up.
the object, while `obj` will contain the object that was picked up.
Following this example, if you create a callback "get" on the object "a sword", and put in it:
@ -59,11 +59,11 @@ When you pick up this object you should see something like:
## Installation
Being in a separate contrib, the event system isn't installed by default. You need to do it
Being in a separate contrib, the in-game Python system isn't installed by default. You need to do it
manually, following these steps:
1. Launch the main script (important!):
```@py evennia.create_script("evennia.contrib.events.scripts.EventHandler")```
```@py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler")```
2. Set the permissions (optional):
- `EVENTS_WITH_VALIDATION`: a group that can edit callbacks, but will need approval (default to
`None`).
@ -73,23 +73,23 @@ manually, following these steps:
- `EVENTS_CALENDAR`: type of the calendar to be used (either `None`, `"standard"` or `"custom"`,
default to `None`).
3. Add the `@call` command.
4. Inherit from the custom typeclasses of the event system.
- `evennia.contrib.events.typeclasses.EventCharacter`: to replace `DefaultCharacter`.
- `evennia.contrib.events.typeclasses.EventExit`: to replace `DefaultExit`.
- `evennia.contrib.events.typeclasses.EventObject`: to replace `DefaultObject`.
- `evennia.contrib.events.typeclasses.EventRoom`: to replace `DefaultRoom`.
4. Inherit from the custom typeclasses of the in-game Python system.
- `evennia.contrib.ingame_python.typeclasses.EventCharacter`: to replace `DefaultCharacter`.
- `evennia.contrib.ingame_python.typeclasses.EventExit`: to replace `DefaultExit`.
- `evennia.contrib.ingame_python.typeclasses.EventObject`: to replace `DefaultObject`.
- `evennia.contrib.ingame_python.typeclasses.EventRoom`: to replace `DefaultRoom`.
The following sections describe in details each step of the installation.
> Note: If you were to start the game without having started the main script (such as when
> Note: If you were to start the game without having started the main script (such as when
resetting your database) you will most likely face a traceback when logging in, telling you
that a 'callback' property is not defined. After performing step `1` the error will go away.
that a 'callback' property is not defined. After performing step `1` the error will go away.
### Starting the event script
To start the event script, you only need a single command, using `@py`.
@py evennia.create_script("evennia.contrib.events.scripts.EventHandler")
@py evennia.create_script("evennia.contrib.ingame_python.scripts.EventHandler")
This command will create a global script (that is, a script independent from any object). This
script will hold basic configuration, individual callbacks and so on. You may access it directly,
@ -174,7 +174,7 @@ this:
```python
from evennia import default_cmds
from evennia.contrib.events.commands import CmdCallback
from evennia.contrib.ingame_python.commands import CmdCallback
class CharacterCmdSet(default_cmds.CharacterCmdSet):
"""
@ -194,25 +194,25 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet):
### Changing parent classes of typeclasses
Finally, to use the event system, you need to have your typeclasses inherit from the modified event
Finally, to use the in-game Python system, you need to have your typeclasses inherit from the modified event
classes. For instance, in your `typeclasses/characters.py` module, you should change inheritance
like this:
```python
from evennia.contrib.events.typeclasses import EventCharacter
from evennia.contrib.ingame_python.typeclasses import EventCharacter
class Character(EventCharacter):
# ...
```
You should do the same thing for your rooms, exits and objects. Note that the event system works by
You should do the same thing for your rooms, exits and objects. Note that the in-game Python system works by
overriding some hooks. Some of these features might not be accessible in your game if you don't
call the parent methods when overriding hooks.
## Using the `@call` command
The event system relies, to a great extent, on its `@call` command. Who can execute this command,
The in-game Python system relies, to a great extent, on its `@call` command. Who can execute this command,
and who can do what with it, will depend on your set of permissions.
The `@call` command allows to add, edit and delete callbacks on specific objects' events. The event
@ -383,7 +383,7 @@ most complex.
### The eventfuncs
In order to make development a little easier, the event system provides eventfuncs to be used in
In order to make development a little easier, the in-game Python system provides eventfuncs to be used in
callbacks themselves. You don't have to use them, they are just shortcuts. An eventfunc is just a
simple function that can be used inside of your callback code.
@ -473,7 +473,7 @@ And if the character Wilfred takes this exit, others in the room will see:
Wildred falls into a hole in the ground!
In this case, the event system placed the variable "message" in the callback locals, but will read
In this case, the in-game Python system placed the variable "message" in the callback locals, but will read
from it when the event has been executed.
### Callbacks with parameters
@ -661,15 +661,15 @@ specific events fired.
Adding new events should be done in your typeclasses. Events are contained in the `_events` class
variable, a dictionary of event names as keys, and tuples to describe these events as values. You
also need to register this class, to tell the event system that it contains events to be added to
also need to register this class, to tell the in-game Python system that it contains events to be added to
this typeclass.
Here, we want to add a "push" event on objects. In your `typeclasses/objects.py` file, you should
write something like:
```python
from evennia.contrib.events.utils import register_events
from evennia.contrib.events.typeclasses import EventObject
from evennia.contrib.ingame_python.utils import register_events
from evennia.contrib.ingame_python.typeclasses import EventObject
EVENT_PUSH = """
A character push the object.
@ -692,7 +692,7 @@ class Object(EventObject):
}
```
- Line 1-2: we import several things we will need from the event system. Note that we use
- Line 1-2: we import several things we will need from the in-game Python system. Note that we use
`EventObject` as a parent instead of `DefaultObject`, as explained in the installation.
- Line 4-12: we usually define the help of the event in a separate variable, this is more readable,
though there's no rule against doing it another way. Usually, the help should contain a short
@ -714,7 +714,7 @@ fired.
### Calling an event in code
The event system is accessible through a handler on all objects. This handler is named `callbacks`
The in-game Python system is accessible through a handler on all objects. This handler is named `callbacks`
and can be accessed from any typeclassed object (your character, a room, an exit...). This handler
offers several methods to examine and call an event or callback on this object.
@ -825,7 +825,7 @@ this is out of the scope of this documentation).
The "say" command uses phrase parameters (you can set a "say" callback to fires if a phrase
contains one specific word).
In both cases, you need to import a function from `evennia.contrib.events.utils` and use it as third
In both cases, you need to import a function from `evennia.contrib.ingame_python.utils` and use it as third
parameter in your event definition.
- `keyword_event` should be used for keyword parameters.
@ -834,7 +834,7 @@ parameter in your event definition.
For example, here is the definition of the "say" event:
```python
from evennia.contrib.events.utils import register_events, phrase_event
from evennia.contrib.ingame_python.utils import register_events, phrase_event
# ...
@register_events
class SomeTypeclass:
@ -865,5 +865,5 @@ The best way to do this is to use a custom setting, in your setting file
EVENTS_DISABLED = True
```
The event system will still be accessible (you will have access to the `@call` command, to debug),
The in-game Python system will still be accessible (you will have access to the `@call` command, to debug),
but no event will be called automatically.

View file

@ -7,9 +7,9 @@ from collections import namedtuple
class CallbackHandler(object):
"""
The event handler for a specific object.
The callback handler for a specific object.
The script that contains all events will be reached through this
The script that contains all callbacks will be reached through this
handler. This handler is therefore a shortcut to be used by
developers. This handler (accessible through `obj.callbacks`) is a
shortcut to manipulating callbacks within this object, getting,

View file

@ -1,5 +1,5 @@
"""
Module containing the commands of the callback system.
Module containing the commands of the in-game Python system.
"""
from datetime import datetime
@ -10,7 +10,7 @@ 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.utils import get_event_handler
from evennia.contrib.ingame_python.utils import get_event_handler
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -358,9 +358,6 @@ class CmdCallback(COMMAND_DEFAULT_CLASS):
# Open the editor
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="Callback {} of {}".format(

View file

@ -6,14 +6,14 @@ Eventfuncs are just Python functions that can be used inside of calllbacks.
"""
from evennia import ObjectDB, ScriptDB
from evennia.contrib.events.utils import InterruptEvent
from evennia.contrib.ingame_python.utils import InterruptEvent
def deny():
"""
Deny, that is stop, the event here.
Deny, that is stop, the callback here.
Notes:
This function will raise an exception to terminate the event
This function will raise an exception to terminate the callback
in a controlled way. If you use this function in an event called
prior to a command, the command will be cancelled as well. Good
situations to use the `deny()` function are in events that begins

View file

@ -1,5 +1,5 @@
"""
Scripts for the event system.
Scripts for the in-game Python system.
"""
from datetime import datetime, timedelta
@ -15,8 +15,8 @@ from evennia.utils.ansi import raw
from evennia.utils.create import create_channel
from evennia.utils.dbserialize import dbserialize
from evennia.utils.utils import all_from_module, delay, pypath_to_realpath
from evennia.contrib.events.callbackhandler import CallbackHandler
from evennia.contrib.events.utils import get_next_wait, EVENTS, InterruptEvent
from evennia.contrib.ingame_python.callbackhandler import CallbackHandler
from evennia.contrib.ingame_python.utils import get_next_wait, EVENTS, InterruptEvent
# Constants
RE_LINE_ERROR = re.compile(r'^ File "\<string\>", line (\d+)')
@ -29,7 +29,7 @@ class EventHandler(DefaultScript):
This script shouldn't be created more than once. It contains
event (in a non-persistent attribute) and callbacks (in a
persistent attribute). The script method would help adding,
editing and deleting these events.
editing and deleting these events and callbacks.
"""
@ -68,7 +68,7 @@ class EventHandler(DefaultScript):
# Generate locals
self.ndb.current_locals = {}
self.ndb.fresh_locals = {}
addresses = ["evennia.contrib.events.eventfuncs"]
addresses = ["evennia.contrib.ingame_python.eventfuncs"]
addresses.extend(getattr(settings, "EVENTFUNCS_LOCATIONS", ["world.eventfuncs"]))
for address in addresses:
if pypath_to_realpath(address):
@ -85,7 +85,7 @@ class EventHandler(DefaultScript):
delay(seconds, complete_task, task_id)
# Place the script in the CallbackHandler
from evennia.contrib.events import typeclasses
from evennia.contrib.ingame_python import typeclasses
CallbackHandler.script = self
DefaultObject.callbacks = typeclasses.EventObject.callbacks

View file

@ -1,5 +1,5 @@
"""
Module containing the test cases for the event system.
Module containing the test cases for the in-game Python system.
"""
from mock import Mock
@ -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 CmdCallback
from evennia.contrib.events.callbackhandler import CallbackHandler
from evennia.contrib.ingame_python.commands import CmdCallback
from evennia.contrib.ingame_python.callbackhandler import CallbackHandler
# Force settings
settings.EVENTS_CALENDAR = "standard"
@ -31,18 +31,18 @@ class TestEventHandler(EvenniaTest):
"""Create the event handler."""
super(TestEventHandler, self).setUp()
self.handler = create_script(
"evennia.contrib.events.scripts.EventHandler")
"evennia.contrib.ingame_python.scripts.EventHandler")
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit")
self.char1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom")
self.exit.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventExit")
def tearDown(self):
"""Stop the event handler."""
@ -249,18 +249,18 @@ class TestCmdCallback(CommandTest):
"""Create the callback handler."""
super(TestCmdCallback, self).setUp()
self.handler = create_script(
"evennia.contrib.events.scripts.EventHandler")
"evennia.contrib.ingame_python.scripts.EventHandler")
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit")
self.char1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom")
self.exit.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventExit")
def tearDown(self):
"""Stop the callback handler."""
@ -268,7 +268,7 @@ class TestCmdCallback(CommandTest):
OLD_EVENTS.update(self.handler.ndb.events)
self.handler.stop()
for script in ScriptDB.objects.filter(
db_typeclass_path="evennia.contrib.events.scripts.TimeEventScript"):
db_typeclass_path="evennia.contrib.ingame_python.scripts.TimeEventScript"):
script.stop()
CallbackHandler.script = None
@ -414,18 +414,18 @@ class TestDefaultCallbacks(CommandTest):
"""Create the callback handler."""
super(TestDefaultCallbacks, self).setUp()
self.handler = create_script(
"evennia.contrib.events.scripts.EventHandler")
"evennia.contrib.ingame_python.scripts.EventHandler")
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit")
self.char1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventRoom")
self.exit.swap_typeclass("evennia.contrib.ingame_python.typeclasses.EventExit")
def tearDown(self):
"""Stop the callback handler."""

View file

@ -1,5 +1,5 @@
"""
Typeclasses for the event system.
Typeclasses for the in-game Python system.
To use thm, one should inherit from these classes (EventObject,
EventRoom, EventCharacter and EventExit).
@ -9,8 +9,8 @@ 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.callbackhandler import CallbackHandler
from evennia.contrib.events.utils import register_events, time_event, phrase_event
from evennia.contrib.ingame_python.callbackhandler import CallbackHandler
from evennia.contrib.ingame_python.utils import register_events, time_event, phrase_event
# Character help
CHARACTER_CAN_DELETE = """
@ -121,7 +121,7 @@ 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
@call/add ... = say menu, dinner, lunch
Then if one of the words is present in what the character says,
this event will fire.
@ -135,12 +135,12 @@ 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
time). You have to specify the time as an argument to @call/add, like:
@call/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
@call/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).
@ -461,12 +461,12 @@ 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
time). You have to specify the time as an argument to @call/add, like:
@call/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
@call/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).
@ -559,12 +559,12 @@ 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
time). You have to specify the time as an argument to @call/add, like:
@call/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
@call/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).
@ -702,7 +702,7 @@ 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
@call/add ... = say menu, dinner, lunch
Then if one of the words is present in what the character says,
this event will fire.
@ -716,12 +716,12 @@ 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
time). You have to specify the time as an argument to @call/add, like:
@call/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
@call/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).

View file

@ -166,7 +166,7 @@ def time_event(obj, event_name, number, parameters):
"""
seconds, usual, key = get_next_wait(parameters)
script = create_script("evennia.contrib.events.scripts.TimeEventScript", interval=seconds, obj=obj)
script = create_script("evennia.contrib.ingame_python.scripts.TimeEventScript", interval=seconds, obj=obj)
script.key = key
script.desc = "event on {}".format(key)
script.db.time_format = parameters