mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
910 lines
40 KiB
Markdown
910 lines
40 KiB
Markdown
# Evennia in-game Python system
|
|
|
|
Contrib by Vincent Le Goff 2017
|
|
|
|
This contrib adds the ability to script with Python in-game. It allows trusted
|
|
staff/builders to dynamically add features and triggers to individual objects
|
|
without needing to do it in external Python modules. Using custom Python in-game,
|
|
specific rooms, exits, characters, objects etc can be made to behave differently from
|
|
its "cousins". This is similar to how softcode works for MU or MudProgs for DIKU.
|
|
Keep in mind, however, that allowing Python in-game comes with _severe_
|
|
security concerns (you must trust your builders deeply), so read the warnings in
|
|
this module carefully before continuing.
|
|
|
|
## A WARNING REGARDING SECURITY
|
|
|
|
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 in-game Python system
|
|
is not to replace all your game feature.
|
|
|
|
## Extra tutorials
|
|
|
|
These tutorials cover examples of using ingame python. Once you have the system
|
|
installed (see below) they may be an easier way to learn than reading the full
|
|
documentation from beginning to end.
|
|
|
|
- [Dialogue events](./Contrib-Ingame-Python-Tutorial-Dialogue.md), where
|
|
NPCs react to things said.
|
|
- [A voice operated elevator](./Contrib-Ingame-Python-Tutorial-Elevator.md)
|
|
using ingame-python events.
|
|
|
|
## Basic structure and vocabulary
|
|
|
|
- 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](../Components/Typeclasses.md)
|
|
([exits](../Components/Exits.md) 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.
|
|
|
|
To see the system in context, when an object is picked up (using the default
|
|
`get` command), a specific event is fired:
|
|
|
|
1. The event "get" is set on objects (on the `Object` typeclass).
|
|
2. When using the "get" command to pick up an object, this object's `at_get`
|
|
hook is called.
|
|
3. A modified hook of DefaultObject is set by the event system. This hook will
|
|
execute (or call) 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.
|
|
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.
|
|
|
|
Following this example, if you create a callback "get" on the object "a sword",
|
|
and put in it:
|
|
|
|
```python
|
|
character.msg("You have picked up {} and have completed this quest!".format(obj.get_display_name(character)))
|
|
|
|
```
|
|
|
|
When you pick up this object you should see something like:
|
|
|
|
You pick up a sword.
|
|
You have picked up a sword and have completed this quest!
|
|
|
|
## Installation
|
|
|
|
Being in a separate contrib, the in-game Python system isn't installed by
|
|
default. You need to do it manually, following these steps:
|
|
|
|
This is the quick summary. Scroll down for more detailed help on each step.
|
|
|
|
1. Launch the main script (important!):
|
|
|
|
py evennia.create_script("evennia.contrib.base_systems.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`).
|
|
- `EVENTS_WITHOUT_VALIDATION`: a group with permission to edit callbacks without need of
|
|
validation (default to `"immortals"`).
|
|
- `EVENTS_VALIDATING`: a group that can validate callbacks (default to `"immortals"`).
|
|
- `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 in-game Python system.
|
|
- `evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter`: to replace `DefaultCharacter`.
|
|
- `evennia.contrib.base_systems.ingame_python.typeclasses.EventExit`: to replace `DefaultExit`.
|
|
- `evennia.contrib.base_systems.ingame_python.typeclasses.EventObject`: to replace `DefaultObject`.
|
|
- `evennia.contrib.base_systems.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
|
|
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.
|
|
|
|
### Starting the event script
|
|
|
|
To start the event script, you only need a single command, using `@py`.
|
|
|
|
py evennia.create_script("evennia.contrib.base_systems.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,
|
|
but you will probably use the callback handler. Creating this script will also create a `callback`
|
|
handler on all objects (see below for details).
|
|
|
|
### Editing permissions
|
|
|
|
This contrib comes with its own set of permissions. They define who can edit callbacks without
|
|
validation, and who can edit callbacks but needs validation. Validation is a process in which an
|
|
administrator (or somebody trusted as such) will check the callbacks produced by others and will
|
|
accept or reject them. If accepted, the callbacks are connected, otherwise they are never run.
|
|
|
|
By default, callbacks can only be created by immortals: no one except the immortals can edit
|
|
callbacks, and immortals don't need validation. It can easily be changed, either through settings
|
|
or dynamically by changing permissions of users.
|
|
|
|
The ingame-python contrib adds three [permissions](../Components/Permissions.md)) in the settings. You can
|
|
override them by changing the settings into your `server/conf/settings.py` file (see below for an
|
|
example). The settings defined in the events contrib are:
|
|
|
|
- `EVENTS_WITH_VALIDATION`: this defines a permission that can edit callbacks, but will need
|
|
approval. If you set this to `"wizards"`, for instance, users with the permission `"wizards"`
|
|
will be able to edit callbacks. These callbacks will not be connected, though, and will need to be
|
|
checked and approved by an administrator. This setting can contain `None`, meaning that no user is
|
|
allowed to edit callbacks with validation.
|
|
- `EVENTS_WITHOUT_VALIDATION`: this setting defines a permission allowing editing of callbacks
|
|
without needing validation. By default, this setting is set to `"immortals"`. It means that
|
|
immortals can edit callbacks, and they will be connected when they leave the editor, without needing
|
|
approval.
|
|
- `EVENTS_VALIDATING`: this last setting defines who can validate callbacks. By default, this is
|
|
set to `"immortals"`, meaning only immortals can see callbacks needing validation, accept or
|
|
reject them.
|
|
|
|
You can override all these settings in your `server/conf/settings.py` file. For instance:
|
|
|
|
```python
|
|
# ... other settings ...
|
|
|
|
# Event settings
|
|
EVENTS_WITH_VALIDATION = "wizards"
|
|
EVENTS_WITHOUT_VALIDATION = "immortals"
|
|
EVENTS_VALIDATING = "immortals"
|
|
```
|
|
|
|
In addition, there is another setting that must be set if you plan on using the time-related events
|
|
(events that are scheduled at specific, in-game times). You would need to specify the type of
|
|
calendar you are using. By default, time-related events are disabled. You can change the
|
|
`EVENTS_CALENDAR` to set it to:
|
|
|
|
- `"standard"`: the standard calendar, with standard days, months, years and so on.
|
|
- `"custom"`: a custom calendar that will use the `custom_gametime` contrib to schedule events.
|
|
|
|
This contrib defines two additional permissions that can be set on individual users:
|
|
|
|
- `events_without_validation`: this would give this user the rights to edit callbacks but not
|
|
require validation before they are connected.
|
|
- `events_validating`: this permission allows this user to run validation checks on callbacks
|
|
needing to be validated.
|
|
|
|
For instance, to give the right to edit callbacks without needing approval to the player 'kaldara',
|
|
you might do something like:
|
|
|
|
perm *kaldara = events_without_validation
|
|
|
|
To remove this same permission, just use the `/del` switch:
|
|
|
|
perm/del *kaldara = events_without_validation
|
|
|
|
The rights to use the `call` command are directly related to these permissions: by default, only
|
|
users who have the `events_without_validation` permission or are in (or above) the group defined in
|
|
the `EVENTS_WITH_VALIDATION` setting will be able to call the command (with different switches).
|
|
|
|
### Adding the `call` command
|
|
|
|
You also have to add the `@call` command to your Character CmdSet. This command allows your users
|
|
to add, edit and delete callbacks in-game. In your `commands/default_cmdsets`, it might look like
|
|
this:
|
|
|
|
```python
|
|
from evennia import default_cmds
|
|
from evennia.contrib.base_systems.ingame_python.commands import CmdCallback
|
|
|
|
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
|
"""
|
|
The `CharacterCmdSet` contains general in-game commands like `look`,
|
|
`get`, etc available on in-game Character objects. It is merged with
|
|
the `PlayerCmdSet` when a Player puppets a Character.
|
|
"""
|
|
key = "DefaultCharacter"
|
|
|
|
def at_cmdset_creation(self):
|
|
"""
|
|
Populates the cmdset
|
|
"""
|
|
super().at_cmdset_creation()
|
|
self.add(CmdCallback())
|
|
```
|
|
|
|
### Changing parent classes of typeclasses
|
|
|
|
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.base_systems.ingame_python.typeclasses import EventCharacter
|
|
|
|
class Character(EventCharacter):
|
|
|
|
# ...
|
|
```
|
|
|
|
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 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
|
|
system can be used on most Evennia objects, mostly typeclassed objects (excluding players). The
|
|
first argument of the `call` command is the name of the object you want to edit. It can also be
|
|
used to know what events are available for this specific object.
|
|
|
|
### Examining callbacks and events
|
|
|
|
To see the events connected to an object, use the `call` command and give the name or ID of the
|
|
object to examine. For instance, `call here` to examine the events on your current location. Or
|
|
`call self` to see the events on yourself.
|
|
|
|
This command will display a table, containing:
|
|
|
|
- The name of each event in the first column.
|
|
- The number of callbacks of this name, and the number of total lines of these callbacks in the
|
|
second column.
|
|
- A short help to tell you when the event is triggered in the third column.
|
|
|
|
If you execute `call #1` for instance, you might see a table like this:
|
|
|
|
```
|
|
+------------------+---------+-----------------------------------------------+
|
|
| Event name | Number | Description |
|
|
+~~~~~~~~~~~~~~~~~~+~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
|
|
| can_delete | 0 (0) | Can the character be deleted? |
|
|
| can_move | 0 (0) | Can the character move? |
|
|
| can_part | 0 (0) | Can the departing character leave this room? |
|
|
| delete | 0 (0) | Before deleting the character. |
|
|
| greet | 0 (0) | A new character arrives in the location of |
|
|
| | | this character. |
|
|
| move | 0 (0) | After the character has moved into its new |
|
|
| | | room. |
|
|
| puppeted | 0 (0) | When the character has been puppeted by a |
|
|
| | | player. |
|
|
| time | 0 (0) | A repeated event to be called regularly. |
|
|
| unpuppeted | 0 (0) | When the character is about to be un- |
|
|
| | | puppeted. |
|
|
+------------------+---------+-----------------------------------------------+
|
|
```
|
|
|
|
### Creating a new callback
|
|
|
|
The `/add` switch should be used to add a callback. It takes two arguments beyond the object's
|
|
name/DBREF:
|
|
|
|
1. After an = sign, the name of the event to be edited (if not supplied, will display the list of
|
|
possible events, like above).
|
|
2. The parameters (optional).
|
|
|
|
We'll see callbacks with parameters later. For the time being, let's try to prevent a character
|
|
from going through the "north" exit of this room:
|
|
|
|
```
|
|
call north
|
|
+------------------+---------+-----------------------------------------------+
|
|
| Event name | Number | Description |
|
|
+~~~~~~~~~~~~~~~~~~+~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
|
|
| can_traverse | 0 (0) | Can the character traverse through this exit? |
|
|
| msg_arrive | 0 (0) | Customize the message when a character |
|
|
| | | arrives through this exit. |
|
|
| msg_leave | 0 (0) | Customize the message when a character leaves |
|
|
| | | through this exit. |
|
|
| time | 0 (0) | A repeated event to be called regularly. |
|
|
| traverse | 0 (0) | After the character has traversed through |
|
|
| | | this exit. |
|
|
+------------------+---------+-----------------------------------------------+
|
|
```
|
|
|
|
If we want to prevent a character from traversing through this exit, the best event for us would be
|
|
"can_traverse".
|
|
|
|
> Why not "traverse"? If you read the description of both events, you will see "traverse" is called
|
|
**after** the character has traversed through this exit. It would be too late to prevent it. On
|
|
> the other hand, "can_traverse" is obviously checked before the character traverses.
|
|
|
|
When we edit the event, we have some more information:
|
|
|
|
call/add north = 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() eventfunc to deny the character from
|
|
exiting 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.
|
|
|
|
The section dedicated to [eventfuncs](#the-eventfuncs) will elaborate on the `deny()` function and
|
|
other eventfuncs. Let us say, for the time being, that it can prevent an action (in this case, it
|
|
can prevent the character from traversing through this exit). In the editor that opened when you
|
|
used `call/add`, you can type something like:
|
|
|
|
```python
|
|
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 callback.
|
|
|
|
If you enter `call north`, you should see that "can_traverse" now has an active callback. You can
|
|
use `call north = can_traverse` to see more details on the connected callbacks:
|
|
|
|
```
|
|
call north = can_traverse
|
|
+--------------+--------------+----------------+--------------+--------------+
|
|
| Number | Author | Updated | Param | Valid |
|
|
+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+
|
|
| 1 | XXXXX | 5 seconds ago | | Yes |
|
|
+--------------+--------------+----------------+--------------+--------------+
|
|
```
|
|
|
|
The left column contains callback numbers. You can use them to have even more information on a
|
|
specific event. Here, for instance:
|
|
|
|
```
|
|
call north = can_traverse 1
|
|
Callback can_traverse 1 of north:
|
|
Created by XXXXX on 2017-04-02 17:58:05.
|
|
Updated by XXXXX on 2017-04-02 18:02:50
|
|
This callback is connected and active.
|
|
Callback code:
|
|
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()
|
|
```
|
|
|
|
Then try to walk through this exit. Do it with another character if possible, too, to see the
|
|
difference.
|
|
|
|
### Editing and removing a callback
|
|
|
|
You can use the `/edit` switch to the `@call` command to edit a callback. 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 callbacks are connected at this location.
|
|
|
|
You can type `call/edit <object> = <event name>` to see the callbacks that are linked at this
|
|
location. If there is only one callback, it will be opened in the editor; if more are defined, you
|
|
will be asked for a number to provide (for instance, `call/edit north = can_traverse 2`).
|
|
|
|
The command `call` also provides a `/del` switch to remove a callback. It takes the same arguments
|
|
as the `/edit` switch.
|
|
|
|
When removed, callbacks are logged, so an administrator can retrieve its content, assuming the
|
|
`/del` was an error.
|
|
|
|
### The code editor
|
|
|
|
When adding or editing a callback, the event editor should open in code mode. The additional
|
|
options supported by the editor in this mode are describe in [a dedicated section of the EvEditor's
|
|
documentation](https://github.com/evennia/evennia/wiki/EvEditor#the-eveditor-to-edit-code).
|
|
|
|
## Using events
|
|
|
|
The following sections describe how to use events for various tasks, from the most simple to the
|
|
most complex.
|
|
|
|
### The eventfuncs
|
|
|
|
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.
|
|
|
|
Function | Argument | Description | Example
|
|
-----------|--------------------------|-----------------------------------|--------
|
|
deny | `()` | Prevent an action from happening. | `deny()`
|
|
get | `(**kwargs)` | Get a single object. | `char = get(id=1)`
|
|
call_event | `(obj, name, seconds=0)` | Call another event. | `call_event(char, "chain_1", 20)`
|
|
|
|
#### deny
|
|
|
|
The `deny()` function allows to interrupt the callback and the action that called it. In the
|
|
`can_*` events, it can be used to prevent the action from happening. For instance, in `can_say` on
|
|
rooms, it can prevent the character from saying something in the room. One could have a `can_eat`
|
|
event set on food that would prevent this character from eating this food.
|
|
|
|
Behind the scenes, the `deny()` function raises an exception that is being intercepted by the
|
|
handler of events. The handler will then report that the action was cancelled.
|
|
|
|
#### get
|
|
|
|
The `get` eventfunc is a shortcut to get a single object with a specific identity. It's often used
|
|
to retrieve an object with a given ID. In the section dedicated to [chained
|
|
events](#chained-events), you will see a concrete example of this function in action.
|
|
|
|
#### call_event
|
|
|
|
Some callbacks will call other events. It is particularly useful for [chained
|
|
events](#chained-events) that are described in a dedicated section. This eventfunc is used to call
|
|
another event, immediately or in a defined time.
|
|
|
|
You need to specify as first parameter the object containing the event. The second parameter is the
|
|
name of the event to call. The third parameter is the number of seconds before calling this event.
|
|
By default, this parameter is set to 0 (the event is called immediately).
|
|
|
|
### Variables in callbacks
|
|
|
|
In the Python code you will enter in individual callbacks, you will have access to variables in your
|
|
locals. These variables will depend on the event, and will be clearly listed when you add or edit a
|
|
callback. 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.
|
|
|
|
In most cases, when an event is fired, all callbacks from this event are called. Variables are
|
|
created for each event. Sometimes, however, the callback will execute and then ask for a variable
|
|
in your locals: in other words, some callbacks can alter the actions being performed by changing
|
|
values of variables. This is always clearly specified in the help of the event.
|
|
|
|
One example that will illustrate this system is the "msg_leave" event that can be set on exits.
|
|
This event can alter the message that will be sent to other characters when someone leaves through
|
|
this exit.
|
|
|
|
call/add down = msg_leave
|
|
|
|
Which should display:
|
|
|
|
```
|
|
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.
|
|
```
|
|
|
|
If you write something like this in your event:
|
|
|
|
```python
|
|
message = "{character} falls into a hole in the ground!"
|
|
|
|
```
|
|
|
|
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 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
|
|
|
|
Some callbacks are called without parameter. It has been the case for all examples we have seen
|
|
before. In some cases, you can create callbacks 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. Individual callbacks set on this 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 callback with the parameter "one":
|
|
|
|
call/add here = say one
|
|
|
|
This callback will only fire when the user says a sentence that contains "one".
|
|
|
|
But what if we want to have a callback that would fire if the user says 1 or one? We can provide
|
|
several parameters, separated by a comma.
|
|
|
|
call/add here = say 1, one
|
|
|
|
Or, still more keywords:
|
|
|
|
call/add here = say 1, one, ground
|
|
|
|
This time, the user could say something like "take me to the ground floor" ("ground" is one of our
|
|
keywords defined in the above callback).
|
|
|
|
Not all events can take parameters, and these who do have 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.
|
|
|
|
> If you get confused between callback variables and parameters, think of parameters as checks
|
|
> performed before the callback is run. Event with parameters will only fire some specific
|
|
> callbacks, not all of them.
|
|
|
|
### 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 parameter, 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):
|
|
|
|
call here = time 12:00
|
|
|
|
```python
|
|
# 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 and this callback will be executed. 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 specified 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
|
|
|
|
Callbacks can call other events, either now or a bit later. It is potentially very powerful.
|
|
|
|
To use chained events, just use the `call_event` eventfunc. 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. Connecting exits (opening its doors), waiting a bit, closing them,
|
|
rolling around and stopping at a different station. That's quite a complex set of callbacks, as it
|
|
is, but let's only look at the part that opens and closes the doors:
|
|
|
|
call/add here = time 10:00
|
|
|
|
```python
|
|
# 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)
|
|
to_exit = get(id=23)
|
|
back_exit = get(id=24)
|
|
|
|
# Open the door
|
|
to_exit.name = "platform"
|
|
to_exit.aliases = ["p"]
|
|
to_exit.location = room
|
|
to_exit.destination = station
|
|
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_event(room, "chain_1", 20)
|
|
```
|
|
|
|
This callback will:
|
|
|
|
1. Be called at 10:00 AM (specify 22:00 to set it to 10:00 PM).
|
|
2. Set an exit between the subway and the station. Notice that the exits already exist (you will
|
|
not 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"?
|
|
|
|
call/add here = chain_1
|
|
|
|
```python
|
|
# 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 scenes, the `call_event` function freezes all variables ("room", "station", "to_exit",
|
|
"back_exit" in our example), so you don't need to define them again.
|
|
|
|
A word of caution on callbacks that call chained events: it isn't impossible for a callback 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_event()`, 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.
|
|
|
|
> Chained events are a special case: contrary to standard events, they are created in-game, not
|
|
through code. They usually contain only one callback, although nothing prevents you from creating
|
|
several chained events in the same object.
|
|
|
|
## Using events in code
|
|
|
|
This section describes callbacks and events from code, how to create new events, how to call them in
|
|
a command, and how to handle specific cases like parameters.
|
|
|
|
Along this section, we will see how to implement the following example: we would like to create a
|
|
"push" command that could be used to push objects. Objects could react to this command and have
|
|
specific events fired.
|
|
|
|
### Adding new events
|
|
|
|
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 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.base_systems.ingame_python.utils import register_events
|
|
from evennia.contrib.base_systems.ingame_python.typeclasses import EventObject
|
|
|
|
EVENT_PUSH = """
|
|
A character push the object.
|
|
This event is called when a character uses the "push" command on
|
|
an object in the same room.
|
|
|
|
Variables you can use in this event:
|
|
character: the character that pushes this object.
|
|
obj: the object connected to this event.
|
|
"""
|
|
|
|
@register_events
|
|
class Object(EventObject):
|
|
"""
|
|
Class representing objects.
|
|
"""
|
|
|
|
_events = {
|
|
"push": (["character", "obj"], EVENT_PUSH),
|
|
}
|
|
```
|
|
|
|
- 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
|
|
explanation on a single line, a longer explanation on several lines, and then the list of variables
|
|
with explanations.
|
|
- Line 14: we call a decorator on the class to indicate it contains events. If you're not familiar
|
|
with decorators, you don't really have to worry about it, just remember to put this line just
|
|
above the class definition if your class contains events.
|
|
- Line 15: we create the class inheriting from `EventObject`.
|
|
- Line 20-22: we define the events of our objects in an `_events` class variable. It is a
|
|
dictionary. Keys are event names. Values are a tuple containing:
|
|
- The list of variable names (list of str). This will determine what variables are needed when
|
|
the event triggers. These variables will be used in callbacks (as we'll see below).
|
|
- The event help (a str, the one we have defined above).
|
|
|
|
If you add this code and reload your game, create an object and examine its events with `@call`, you
|
|
should see the "push" event with its help. Of course, right now, the event exists, but it's not
|
|
fired.
|
|
|
|
### Calling an event in code
|
|
|
|
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.
|
|
|
|
To call an event, use the `callbacks.call` method in an object. It takes as argument:
|
|
|
|
- The name of the event to call.
|
|
- All variables that will be accessible in the event as positional arguments. They should be
|
|
specified in the order chosen when [creating new events](#adding-new-events).
|
|
|
|
Following the same example, so far, we have created an event on all objects, called "push". This
|
|
event is never fired for the time being. We could add a "push" command, taking as argument the name
|
|
of an object. If this object is valid, it will call its "push" event.
|
|
|
|
```python
|
|
from commands.command import Command
|
|
|
|
class CmdPush(Command):
|
|
|
|
"""
|
|
Push something.
|
|
|
|
Usage:
|
|
push <something>
|
|
|
|
Push something where you are, like an elevator button.
|
|
|
|
"""
|
|
|
|
key = "push"
|
|
|
|
def func(self):
|
|
"""Called when pushing something."""
|
|
if not self.args.strip():
|
|
self.msg("Usage: push <something>")
|
|
return
|
|
|
|
# Search for this object
|
|
obj = self.caller.search(self.args)
|
|
if not obj:
|
|
return
|
|
|
|
self.msg("You push {}.".format(obj.get_display_name(self.caller)))
|
|
|
|
# Call the "push" event of this object
|
|
obj.callbacks.call("push", self.caller, obj)
|
|
```
|
|
|
|
Here we use `callbacks.call` with the following arguments:
|
|
|
|
- `"push"`: the name of the event to be called.
|
|
- `self.caller`: the one who pushed the button (this is our first variable, `character`).
|
|
- `obj`: the object being pushed (our second variable, `obj`).
|
|
|
|
In the "push" callbacks of our objects, we then can use the "character" variable (containing the one
|
|
who pushed the object), and the "obj" variable (containing the object that was pushed).
|
|
|
|
### See it all work
|
|
|
|
To see the effect of the two modifications above (the added event and the "push" command), let us
|
|
create a simple object:
|
|
|
|
@create/drop rock
|
|
@desc rock = It's a single rock, apparently pretty heavy. Perhaps you can try to push it though.
|
|
@call/add rock = push
|
|
|
|
In the callback you could write:
|
|
|
|
```python
|
|
from random import randint
|
|
number = randint(1, 6)
|
|
character.msg("You push a rock... is... it... going... to... move?")
|
|
if number == 6:
|
|
character.msg("The rock topples over to reveal a beautiful ant-hill!")
|
|
```
|
|
|
|
You can now try to "push rock". You'll try to push the rock, and once out of six times, you will
|
|
see a message about a "beautiful ant-hill".
|
|
|
|
### Adding new eventfuncs
|
|
|
|
Eventfuncs, like `deny()`, are defined in
|
|
`contrib/base_systesm/ingame_python/eventfuncs.py`. You can add your own
|
|
eventfuncs by creating a file named `eventfuncs.py` in your `world` directory.
|
|
The functions defined in this file will be added as helpers.
|
|
|
|
You can also decide to create your eventfuncs in another location, or even in
|
|
several locations. To do so, edit the `EVENTFUNCS_LOCATION` setting in your
|
|
`server/conf/settings.py` file, specifying either a python path or a list of
|
|
Python paths in which your helper functions are defined. For instance:
|
|
|
|
```python
|
|
EVENTFUNCS_LOCATIONS = [
|
|
"world.events.functions",
|
|
]
|
|
```
|
|
|
|
### Creating events with parameters
|
|
|
|
If you want to create events with parameters (if you create a "whisper" or "ask" command, for
|
|
instance, and need to have some characters automatically react to words), you can set an additional
|
|
argument in the tuple of events in your typeclass' `_events` class variable. This third argument
|
|
must contain a callback that will be called to filter through the list of callbacks when the event
|
|
fires. Two types of parameters are commonly used (but you can define more parameter types, although
|
|
this is out of the scope of this documentation).
|
|
|
|
- Keyword parameters: callbacks of this event will be filtered based on specific keywords. This is
|
|
useful if you want the user to specify a word and compare this word to a list.
|
|
- Phrase parameters: callbacks will be filtered using an entire phrase and checking all its words.
|
|
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.base_systems.ingame_python.utils` and use it as third parameter in your
|
|
event definition.
|
|
|
|
- `keyword_event` should be used for keyword parameters.
|
|
- `phrase_event` should be used for phrase parameters.
|
|
|
|
For example, here is the definition of the "say" event:
|
|
|
|
```python
|
|
from evennia.contrib.base_systems.ingame_python.utils import register_events, phrase_event
|
|
# ...
|
|
@register_events
|
|
class SomeTypeclass:
|
|
_events = {
|
|
"say": (["speaker", "character", "message"], CHARACTER_SAY, phrase_event),
|
|
}
|
|
```
|
|
|
|
When you call an event using the `obj.callbacks.call` method, you should also provide the parameter,
|
|
using the `parameters` keyword:
|
|
|
|
```python
|
|
obj.callbacks.call(..., parameters="<put parameters here>")
|
|
```
|
|
|
|
It is necessary to specifically call the event with parameters, otherwise the system will not be
|
|
able to know how to filter down the list of callbacks.
|
|
|
|
## Disabling all events at once
|
|
|
|
When callbacks are running in an infinite loop, for instance, or sending unwanted information to
|
|
players or other sources, you, as the game administrator, have the power to restart without events.
|
|
The best way to do this is to use a custom setting, in your setting file
|
|
(`server/conf/settings.py`):
|
|
|
|
```python
|
|
# Disable all events
|
|
EVENTS_DISABLED = True
|
|
```
|
|
|
|
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.
|
|
|
|
|
|
```{toctree}
|
|
:hidden:
|
|
|
|
Contrib-Ingame-Python-Tutorial-Dialogue
|
|
Contrib-Ingame-Python-Tutorial-Elevator
|
|
|
|
```
|
|
|
|
|
|
----
|
|
|
|
<small>This document page is generated from `evennia/contrib/base_systems/ingame_python/README.md`. Changes to this
|
|
file will be overwritten, so edit that file rather than this one.</small>
|