# A voice operated elevator using events

This tutorial will walk you through the steps to create a voice-operated elevator, using the [in-
game Python system](./Contrib-Ingame-Python.md). This tutorial assumes the in-game Python
system is installed per the instructions in that doc. **You do not need to read** the entire
documentation, it's a good reference, but not the easiest way to learn about it.  Hence these
tutorials.

The in-game Python system allows to run code on individual objects in some situations.  You don't
have to modify the source code to add these features, past the installation.  The entire system
makes it easy to add specific features to some objects, but not all.

> What will we try to do?

In this tutorial, we are going to create a simple voice-operated elevator.  In terms of features, we
will:

- Explore events with parameters.
- Work on more interesting callbacks.
- Learn about chained events.
- Play with variable modification in callbacks.

## Our study case

Let's summarize what we want to achieve first.  We would like to create a room that will represent
the inside of our elevator.  In this room, a character could just say "1", "2" or "3", and the
elevator will start moving.  The doors will close and open on the new floor (the exits leading in
and out of the elevator will be modified).

We will work on basic features first, and then will adjust some, showing you how easy and powerfully
independent actions can be configured through the in-game Python system.

## Creating the rooms and exits we need

We'll create an elevator right in our room (generally called "Limbo", of ID 2).  You could easily
adapt the following instructions if you already have some rooms and exits, of course, just remember
to check the IDs.

> Note: the in-game Python system uses IDs for a lot of things.  While it is not mandatory, it is
good practice to know the IDs you have for your callbacks, because it will make manipulation much
quicker.  There are other ways to identify objects, but as they depend on many factors, IDs are
usually the safest path in our callbacks.

Let's go into limbo (`#2`) to add our elevator.  We'll add it to the north.  To create this room,
in-game you could type:

    tunnel n = Inside of an elevator

The game should respond by telling you:

    Created room Inside of an elevator(#3) of type typeclasses.rooms.Room.
    Created Exit from Limbo to Inside of an elevator: north(#4) (n).
    Created Exit back from Inside of an elevator to Limbo: south(#5) (s).

Note the given IDs:

- `#2` is limbo, the first room the system created.
- `#3` is our room inside of an elevator.
- `#4` is the north exit from Limbo to our elevator.
- `#5` is the south exit from an elevator to Limbo.

Keep these IDs somewhere for the demonstration.  You will shortly see why they are important.

> Why have we created exits to our elevator and back to Limbo?  Isn't the elevator supposed to move?

It is.  But we need to have exits that will represent the way inside the elevator and out.  What we
will do, at every floor, will be to change these exits so they become connected to the right room.
You'll see this process a bit later.

We have two more rooms to create: our floor 2 and 3.  This time, we'll use `dig`, because we don't
need exits leading there, not yet anyway.

    dig The second floor
    dig The third floor

Evennia should answer with:

    Created room The second floor(#6) of type typeclasses.rooms.Room.
    Created room The third floor(#7) of type typeclasses.rooms.Room.

Add these IDs to your list, we will use them too.

## Our first callback in the elevator

Let's go to the elevator (you could use `tel #3` if you have the same IDs I have).

This is our elevator room.  It looks a bit empty, feel free to add a prettier description or other
things to decorate it a bit.

But what we want now is to be able to say "1", "2" or "3" and have the elevator move in that
direction.

If you have read
[the other in-game Python tutorial about adding dialogues in events](./Contrib-Ingame-Python-Tutorial-Dialogue.md), you
may remember what we need to do.  If not, here's a summary: we need to run some code when somebody
speaks in the room.  So we need to create a callback (the callback will contain our lines of code).
We just need to know on which event this should be set.  You can enter `call here` to see the
possible events in this room.

In the table, you should see the "say" event, which is called when somebody says something in the
room.  So we'll need to add a callback to this event.  Don't worry if you're a bit lost, just follow
the following steps, the way they connect together will become more obvious.

    call/add here = say 1, 2, 3

1. We need to add a callback.  A callback contains the code that will be executed at a given time.
So we use the `call/add` command and switch.
2. `here` is our object, the room in which we are.
3. An equal sign.
4. The name of the event to which the callback should be connected.  Here, the event is "say".
Meaning this callback will be executed every time somebody says something in the room.
5. But we add an event parameter to indicate the keywords said in the room that should execute our
callback.  Otherwise, our callback would be called every time somebody speaks, no matter what.  Here
we limit, indicating our callback should be executed only if the spoken message contains "1", "2" or
"3".

An editor should open, inviting you to enter the Python code that should be executed.  The first
thing to remember is to read the text provided (it can contain important information) and, most of
all, the list of variables that are available in this callback:

```
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.

----------Line Editor [Callback say of Inside of an elevator]---------------------
01|
----------[l:01 w:000 c:0000]------------(:h for help)----------------------------
```

This is important, in order to know what variables we can use in our callback out-of-the-box.  Let's
write a single line to be sure our callback is called when we expect it to:

```python
character.msg(f"You just said {message}.")
```

You can paste this line in-game, then type the `:wq` command to exit the editor and save your
modifications.

Let's check.  Try to say "hello" in the room.  You should see the standard message, but nothing
more.  Now try to say "1".  Below the standard message, you should see:

    You just said 1.

You can try it.  Our callback is only called when we say "1", "2" or "3".  Which is just what we
want.

Let's go back in our code editor and add something more useful.

    call/edit here = say

> Notice that we used the "edit" switch this time, since the callback exists, we just want to edit
it.

The editor opens again.  Let's empty it first:

    :DD

And turn off automatic indentation, which will help us:

    :=

> Auto-indentation is an interesting feature of the code editor, but we'd better not use it at this
point, it will make copy/pasting more complicated.

## Our entire callback in the elevator

So here's the time to truly code our callback in-game.  Here's a little reminder:

1. We have all the IDs of our three rooms and two exits.
2. When we say "1", "2" or "3", the elevator should move to the right room, that is change the
exits.  Remember, we already have the exits, we just need to change their location and destination.

It's a good idea to try to write this callback yourself, but don't feel bad about checking the
solution right now.  Here's a possible code that you could paste in the code editor:

```python
# First let's have some constants
ELEVATOR = get(id=3)
FLOORS = {
    "1": get(id=2),
    "2": get(id=6),
    "3": get(id=7),
}
TO_EXIT = get(id=4)
BACK_EXIT = get(id=5)

# Now we check that the elevator isn't already at this floor
floor = FLOORS.get(message)
if floor is None:
    character.msg("Which floor do you want?")
elif TO_EXIT.location is floor:
    character.msg("The elevator already is at this floor.")
else:
    # 'floor' contains the new room where the elevator should be
    room.msg_contents("The doors of the elevator close with a clank.")
    TO_EXIT.location = floor
    BACK_EXIT.destination = floor
    room.msg_contents("The doors of the elevator open to {floor}.",
            mapping=dict(floor=floor))
```

Let's review this longer callback:

1. We first obtain the objects of both exits and our three floors.  We use the `get()` eventfunc,
which is a shortcut to obtaining objects.  We usually use it to retrieve specific objects with an
ID.  We put the floors in a dictionary.  The keys of the dictionary are the floor number (as str),
the values are room objects.
2. Remember, the `message` variable contains the message spoken in the room.  So either "1", "2", or
"3".  We still need to check it, however, because if the character says something like "1 2" in the
room, our callback will be executed.  Let's be sure what she says is a floor number.
3. We then check if the elevator is already at this floor.  Notice that we use `TO_EXIT.location`.
`TO_EXIT` contains our "north" exit, leading inside of our elevator.  Therefore, its `location` will
be the room where the elevator currently is.
4. If the floor is a different one, have the elevator "move", changing just the location and
destination of both exits.
   - The `BACK_EXIT` (that is "north") should change its location.  The elevator shouldn't be
accessible through our old floor.
   - The `TO_EXIT` (that is "south", the exit leading out of the elevator) should have a different
destination.  When we go out of the elevator, we should find ourselves in the new floor, not the old
one.

Feel free to expand on this example, changing messages, making further checks.  Usage and practice
are keys.

You can quit the editor as usual with `:wq` and test it out.

## Adding a pause in our callback

Let's improve our callback.  One thing that's worth adding would be a pause: for the time being,
when we say the floor number in the elevator, the doors close and open right away.  It would be
better to have a pause of several seconds.  More logical.

This is a great opportunity to learn about chained events.  Chained events are very useful to create
pauses.  Contrary to the events we have seen so far, chained events aren't called automatically.
They must be called by you, and can be called after some time.

- Chained events always have the name `"chain_X"`.  Usually, X is a number, but you can give the
chained event a more explicit name.
- In our original callback, we will call our chained events in, say, 15 seconds.
- We'll also have to make sure the elevator isn't already moving.

Other than that, a chained event can be connected to a callback as usual.  We'll create a chained
event in our elevator, that will only contain the code necessary to open the doors to the new floor.

    call/add here = chain_1

The callback is added to the `"chain_1"` event, an event that will not be automatically called by the
system when something happens.  Inside this event, you can paste the code to open the doors at the
new floor.  You can notice a few differences:

```python
TO_EXIT.location = floor
TO_EXIT.destination = ELEVATOR
BACK_EXIT.location = ELEVATOR
BACK_EXIT.destination = floor
room.msg_contents("The doors of the elevator open to {floor}.",
        mapping=dict(floor=floor))
```

Paste this code into the editor, then use `:wq` to save and quit the editor.

Now let's edit our callback in the "say" event.  We'll have to change it a bit:

- The callback will have to check the elevator isn't already moving.
- It must change the exits when the elevator move.
- It has to call the `"chain_1"` event we have defined.  It should call it 15 seconds later.

Let's see the code in our callback.

    call/edit here = say

Remove the current code and disable auto-indentation again:

    :DD
    :=

And you can paste instead the following code.  Notice the differences with our first attempt:

```python
# First let's have some constants
ELEVATOR = get(id=3)
FLOORS = {
    "1": get(id=2),
    "2": get(id=6),
    "3": get(id=7),
}
TO_EXIT = get(id=4)
BACK_EXIT = get(id=5)

# Now we check that the elevator isn't already at this floor
floor = FLOORS.get(message)
if floor is None:
    character.msg("Which floor do you want?")
elif BACK_EXIT.location is None:
    character.msg("The elevator is between floors.")
elif TO_EXIT.location is floor:
    character.msg("The elevator already is at this floor.")
else:
    # 'floor' contains the new room where the elevator should be
    room.msg_contents("The doors of the elevator close with a clank.")
    TO_EXIT.location = None
    BACK_EXIT.location = None
    call_event(room, "chain_1", 15)
```

What changed?

1. We added a little test to make sure the elevator wasn't already moving.  If it is, the
`BACK_EXIT.location` (the "south" exit leading out of the elevator) should be `None`.  We'll remove
the exit while the elevator is moving.
2. When the doors close, we set both exits' `location` to `None`.  Which "removes" them from their
room but doesn't destroy them.  The exits still exist but they don't connect anything.  If you say
"2" in the elevator and look around while the elevator is moving, you won't see any exits.
3. Instead of opening the doors immediately, we call `call_event`.  We give it the object containing
the event to be called (here, our elevator), the name of the event to be called (here, "chain_1")
and the number of seconds from now when the event should be called (here, `15`).
4. The `chain_1` callback we have created contains the code to "re-open" the elevator doors.  That
is, besides displaying a message, it reset the exits' `location` and `destination`.

If you try to say "3" in the elevator, you should see the doors closing.  Look around you and you
won't see any exit.  Then, 15 seconds later, the doors should open, and you can leave the elevator
to go to the third floor.  While the elevator is moving, the exit leading to it will be
inaccessible.

> Note: we don't define the variables again in our chained event, we just call them.  When we
execute `call_event`, a copy of our current variables is placed in the database.  These variables
will be restored and accessible again when the chained event is called.

You can use the `call/tasks` command to see the tasks waiting to be executed.  For instance, say "2"
in the room, notice the doors closing, and then type the `call/tasks` command.  You will see a task
in the elevator, waiting to call the `chain_1` event.

## Changing exit messages

Here's another nice little feature of events: you can modify the message of a single exit without
altering the others.  In this case, when someone goes north into our elevator, we'd like to see
something like: "someone walks into the elevator." Something similar for the back exit would be
great too.

Inside of the elevator, you can look at the available events on the exit leading outside (south).

    call south

You should see two interesting rows in this table:

```
| 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.                            |
```

So we can change the message others see when a character leaves, by editing the "msg_leave" event.
Let's do that:

    call/add south = msg_leave

Take the time to read the help.  It gives you all the information you should need.  We'll need to
change the "message" variable, and use custom mapping (between braces) to alter the message.  We're
given an example, let's use it.  In the code editor, you can paste the following line:

```python
message = "{character} walks out of the elevator."
```

Again, save and quit the editor by entering `:wq`.  You can create a new character to see it leave.

    charcreate A beggar
    tel #8 = here

(Obviously, adapt the ID if necessary.)

    py self.search("beggar").move_to(self.search("south"))

This is a crude way to force our beggar out of the elevator, but it allows us to test.  You should
see:

    A beggar(#8) walks out of the elevator.

Great!  Let's do the same thing for the exit leading inside of the elevator.  Follow the beggar,
then edit "msg_leave" of "north":

    call/add north = msg_leave

```python
message = "{character} walks into the elevator."
```

Again, you can force our beggar to move and see the message we have just set.  This modification
applies to these two exits, obviously: the custom message won't be used for other exits.  Since we
use the same exits for every floor, this will be available no matter at what floor the elevator is,
which is pretty neat!

## Tutorial F.A.Q.

- **Q:** what happens if the game reloads or shuts down while a task is waiting to happen?
- **A:** if your game reloads while a task is in pause (like our elevator between floors), when the
game is accessible again, the task will be called (if necessary, with a new time difference to take
into account the reload).  If the server shuts down, obviously, the task will not be called, but
will be stored and executed when the server is up again.
- **Q:** can I use all kinds of variables in my callback?  Whether chained or not?
- **A:** you can use every variable type you like in your original callback.  However, if you
execute `call_event`, since your variables are stored in the database, they will need to respect the
constraints on persistent attributes.  A callback will not be stored in this way, for instance.
This variable will not be available in your chained event.
- **Q:** when you say I can call my chained events something else than "chain_1", "chain_2" and
such, what is the naming convention?
- **A:** chained events have names beginning by `"chain_"`.  This is useful for you and for the
system.  But after the underscore, you can give a more useful name, like `"chain_open_doors"` in our
case.
- **Q:** do I have to pause several seconds to call a chained event?
- **A:** no, you can call it right away.  Just leave the third parameter of `call_event` out (it
will default to 0, meaning the chained event will be called right away).  This will not create a
task.
- **Q:** can I have chained events calling themselves?
- **A:** you can.  There's no limitation.  Just be careful, a callback that calls itself,
particularly without delay, might be a good recipe for an infinite loop. However, in some cases, it
is useful to have chained events calling themselves, to do the same repeated action every X seconds
for instance.
- **Q:** what if I need several elevators, do I need to copy/paste these callbacks each time?
- **A:** not advisable.  There are definitely better ways to handle this situation.  One of them is
to consider adding the code in the source itself.  Another possibility is to call chained events
with the expected behavior, which makes porting code very easy. This side of chained events will be
shown in the next tutorial.
