Wiz Programming Tutorials

Hooks are Wyvern's most original, powerful extension mechanism. You can use hooks to do really remarkable things. We'll cover them in a fair amount of detail in this tutorial, and then you can look at the tons of sample code to see it used in real game objects.

Contents

Overview

Every command in the game can be "hooked". A "hook" is an object that implements wyvern.lib.HookCallback. Your hook can do three things:
  • it can be notified when a certain event happens, and respond however you want. For example, you can use hooks to figure out if someone said "Open Sesame" near your magic door.

  • it can change the properties of the event before the event is executed. You could, for example, change what a person shouts (this is the way speech filters are implemented), or the direction they move (this is how confusion is implemented), or just about anything else.

  • it can veto the event entirely. This is how Sokoban prevents you from moving diagonally, and how the Portable Hole prevents you from casting spells inside the hole.

Hopefully that whets your appetite enough to read the rest of this tutorial!

Command Hooks

Example 1 - Notifications

Let's say you want to make an object that says "Ouch!" when you drop it. There are 2 main kinds of hooks: pre-hooks and post-hooks. In this case, you want to say "ouch" after the item is dropped, so you'll use a post-hook.

The way you figure out the name of the hook you want to register on is simple:

  • you start with the name of the command you're hooking - in this case, "drop".

  • you add "postHook" to the command, which gives us "dropPostHook". The capitalization is important.

When the player picks up our object, we'll register for the player's dropPostHook, and when the hook is called, we'll say something.

Here's the code:

from wyvern.lib import HookCallback
from wyvern.lib.classes import DynamicObject
from wyvern.lib.properties import PickupInterest

class hooktest1(DynamicObject, HookCallback, PickupInterest):

    def initialize(self):
        self.super__initialize()
        self.setDefaultCategory("objects")
        self.setDefaultBitmap("skull")

    def toString(self):
        return "Mr. Sensitive"

    def pickedUp(self, agent):
        agent.addHook(self, 'dropPostHook')

    def dropped(self, agent):
        pass

    def hookEvent(self, hookName, event):
        if self is event.getTarget():
            agent = event.getAgent()
            agent.message(self.toString() + ' says:  Ouch!')
            agent.removeHook(self, 'dropPostHook')

To run the example, we did this:

> clone wiz/rhialto/python/hooktest1.py
A new Mr. Sensitive has been placed in your inventory.
> drop Mr. Sensitive
You drop your Mr. Sensitive.
Mr. Sensitive says: Ouch!

Let's take a look at the code for Mr. Sensitive in detail:

  • we subclassed DynamicObject, as usual, since we're a pickup-able object. You'd use StaticObject for an object that can't be picked up, like a statue.

  • we implemented HookCallback to get on the dropPostHook

  • we implemented PickupInterest so we know when we're added to the player's inventory (so we can add the hook).

PickupInterest is an interface that requires you to implement two methods:
  1. pickedUp - tells you when someone picked you up (or was given you, or got you by logging in, or it was added to their inventory in any other way).

  2. dropped - tells you when you left the agent's inventory.

We add our hook to the player in pickedUp. In the dropped method, we just pass, meaning do nothing. We'll talk about this later.

Finally, we get to the meat of the example - the hookEvent method, which is the only method you're required to implement for HookCallback. It does a few things:

  • It checks whether we're the thing that got dropped. DropCommand produces a (wyvern.kernel.commands.)TargetedEvent, which means you can call getTarget() to see what the target was. In this case, it's the thing that was dropped.

    The player could have dropped something else, so we have to do this line to make sure it was us.

  • It tells the agent (conveniently stored in the event) our special message ("Ouch!").

  • It removes the object from the player's dropPostHook, which is important for performance and cleanup reasons. Also, sometimes forgetting to remove a hook will actually make it respond after it's not supposed to anymore, although that's not the case in our example.

Generally speaking, you have to look at the documentation for a class to figure out whether its event is a TargetedEvent or not. DropCommand says in its comments for createEvent() and execute() that it's using a TargetedEvent. You could also do a quick test in Jython:

	targeted = isinstance(event, TargetedEvent)

"targeted" will be set to 1 if the event you're passed is a TargetedEvent.

An Easier Way

You may have been thinking, "If we already know we're dropped, because we're a PickupInterest, why do we need to use the hook?" Good question! You can do it either way. A simpler version of the example looks like this:

from wyvern.lib.classes import DynamicObject
from wyvern.lib.properties import PickupInterest

class hooktest1(DynamicObject, PickupInterest):

    def initialize(self):
        self.super__initialize()
        self.setDefaultCategory("objects")
        self.setDefaultBitmap("skull")

    def toString(self):
        return "Mr. Sensitive"

    def pickedUp(self, agent):
        pass

    def dropped(self, agent):
        agent.message(self.toString() + ' says:  Ouch!')

Why did we bother with the hook, then? Well, Mr. Sensitive seemed like a pretty good example of how to get on a post-hook, so we did it that way. However, wanting to know when you're picked up or dropped is soooo common that we created the PickupInterest interface so you don't have to bother with the hook.

This is a common theme you'll find in the Wyvern Platform APIs:

  • you can do anything using hooks
  • for the most common things, there are often slightly easier alternatives - usually, interfaces you can implement.

Example 2 - Veto

In this example we'll take Mr. Sensitive the Skull and have him prevent you from throwing him. You can drop him or pick him up, or give him away, but he hates to be thrown.

In this case, we want to prevent it before you throw him, so we need to use a pre-hook. You use the same rule: take the command name ("throw") and add "preHook", to get throwPreHook.

Here's the code, very similar to the first example:

from wyvern.lib import HookCallback
from wyvern.lib.classes import DynamicObject
from wyvern.lib.properties import PickupInterest

class hooktest2(DynamicObject, HookCallback, PickupInterest):

    def initialize(self):
        self.super__initialize()

        self.setDefaultCategory("objects")
        self.setDefaultBitmap("skull")

    def toString(self):
        return "Mr. Sensitive"

    def pickedUp(self, agent):
        agent.addHook(self, 'throwPreHook')

    def dropped(self, agent):
        agent.removeHook(self, 'throwPreHook')

    def hookEvent(self, hookName, event):
        if self is event.getTarget():
            msg = self.toString() + " says:  Don't throw me!"
            event.veto(msg)

To run this example, we did this:

> clone wiz/rhialto/python/hooktest2.py
A new Mr. Sensitive has been placed in your inventory.
> throw Mr. Sensitive
Mr. Sensitive says: Don't throw me!

In this case, there's no ThrowInterest, so we have to use a hook to get the behavior we want.

The only real differences in this example are:

  • we clean up the hook in dropped()
  • instead of issuing a message, we veto the event.
Vetoing an event prevents it from happening. You pass a message to the veto() function - this is the message the agent gets when the event fails.

As you can see, the event system is pretty powerful. Now we'll move on to a really fancy example, where we change the parameters of an event before it's executed.

Example 3 - Changing an Event

In this example, we'll make a mini-speech-filter that puts "Ummm..." in front of anything a player says out loud. To do this, we need to modify what's said before they say it, so we use a pre-hook. Using our usual rules, the name of the hook is sayPreHook.

How will we do this one? It's simple:

  • we get the message the player's trying to say, from the event
  • we change the message
  • we stuff the new message back into the event

How do we know how to get and set the message in the say-event? Well, we have to read the documentation for the event. In this case, SayCommand says that its event is a SayCommand.SayEvent (an inner class of SayCommand), and it provides getMessage and setMessage methods for your convenience.

For many commands, they don't use special CommandEvent classes that provide methods like getMessage() or getTarget(). Events are property lists - you can get and set properties on them exactly the way you would for a GameObject, so you don't need to provide getter and setter methods. You just need to document which properties are on the event, which is what most built-in commands do.

In our example, though, SayCommand just happens to provide the getMessage() and setMessage() functions, so we'll use them.

Here's the code:

from wyvern.lib import HookCallback
from wyvern.lib.classes import DynamicObject
from wyvern.lib.properties import PickupInterest

class hooktest3(DynamicObject, HookCallback, PickupInterest):

    def initialize(self):
        self.super__initialize()

        self.setDefaultCategory("objects")
        self.setDefaultBitmap("skull")

    def toString(self):
        return "Mr. Sensitive"

    def pickedUp(self, agent):
        agent.addHook(self, 'sayPreHook')

    def dropped(self, agent):
        agent.removeHook(self, 'sayPreHook')

    def hookEvent(self, hookName, event):
        msg = 'Ummm... ' + event.getMessage()
        event.setMessage(msg)

To run this example, we did this:

> clone wiz/rhialto/python/hooktest3.py
A new Mr. Sensitive has been placed in your inventory.
> say hi there
You say: Ummm... hi there.

Worked like a charm!

How It Works

How do these hooks work, you might ask? To understand them, you just need to look at the wyvern.lib.Command interface, which we cover in detail in the Making New Commands tutorial.

The Command interface has 3 methods:

  • knowsCommand - says whether we know the command or not.

  • createEvent - takes the passed event, which is just a wrapper for the command you're providing, and "stuffs" it with extra information you want to provide to hook callbacks. For simple extenders, it's OK to just return the event that was passed in, with no changes.

  • execute - actually performs the command.
All the built-in Commands in the game have been carefully coded to be good citizens. They take great care in their createEvent methods to populate the events with all the information that could possibly be useful for hook callbacks. Then, in their execute methods, they check whether the event was vetoed, and if so, they issue event.getFailureMessage() and return. The failure message is the string you passed into the veto() method, in Example 2 above.

Then, if the event hasn't been vetoed, they pull all the properties back out of the event and execute it with those properties. The properties may have been changed by a HookCallback, changing the way the event works.

If you're trying to write a command that can be hooked, you should do the same thing.

Event Failures

It's not terribly common, but sometimes you want to know if an event failed - in other words, the player wasn't successful in performing the command. For example, you might want to be notified if they bungled a spell.

The game provides a mechanism for this. It's a special post-hook called the FailedPostHook. You construct the name using the same rules as you do for pre-hooks and (successful) post-hooks, so for the "move" command, you'd use moveFailedPostHook.

We're not going to give an example of this, since:

  • it's not very common
  • the code looks pretty much identical to the examples above, except we use <command>FailedPostHook instead of <command>PostHook.

Failed-post-hooks are only used in a few places in the game engine. One place is in Vehicle, which includes ships - if you try to move the ship, but it bumps into a land mass, it tells you "The ship can't go that way."

I've always wanted to make an Annoying Parrot that sits on your shoulder and laughs at you ("Haw, haw!") whenever one of your commands fails. You'd do this using failed post-hooks, although you'd have to register them for every command.

Map Hooks

The examples above showed ways to hook a command for a single player. It's also possible to hook the commands for an entire map, so that anyone (including monsters) executing the command will pass through your hook function first.

This is the way Sokoban vetoes all diagonal moves in the map, and the way the side-scroller converts moves into jumps (although see the note about moves below.)

Here's an example, just like the previous one, but it makes ALL players say "Ummm..." when they're in the map.

<span class='keyword'></span>from<span class='keyword'></span> <span class='keyword'></span>wyvern<span class='keyword'></span>.<span class='keyword'></span>lib<span class='keyword'></span> <span class='keyword'></span>import<span class='keyword'></span> <span class='keyword'></span>HookCallback<span class='keyword'></span><br><span class='keyword'></span>from<span class='keyword'></span> <span class='keyword'></span>wyvern<span class='keyword'></span>.<span class='keyword'></span>lib<span class='keyword'></span>.<span class='keyword'></span>classes<span class='keyword'></span> <span class='keyword'></span>import<span class='keyword'></span> <span class='keyword'></span>DynamicObject<span class='keyword'></span><br><br><span class='keyword'>class</span> <span class='function'>hooktest4</span>(DynamicObject, HookCallback):<br><br>    <span class='keyword'>def</span> <span class='function'>initialize</span>(self):<br>        <span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>.<span class='keyword'></span>super__initialize<span class='keyword'></span>()<br><br>        <span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>.<span class='keyword'></span>setDefaultCategory<span class='keyword'></span>('<span class='keyword'></span>objects<span class='keyword'></span>')<br>        <span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>.<span class='keyword'></span>setDefaultBitmap<span class='keyword'></span>('<span class='keyword'></span>gold_key<span class='keyword'></span>')<br><br>    <span class='keyword'>def</span> <span class='function'>toString</span>(self):<br>        <span class='keyword'></span>return<span class='keyword'></span> '<span class='keyword'></span>Test<span class='keyword'></span> <span class='keyword'></span>Map<span class='keyword'></span> <span class='keyword'></span>Hook<span class='keyword'></span>'<br><br>    <span class='keyword'>def</span> <span class='function'>setMap</span>(self, map, x, y):<br>        <span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>.<span class='keyword'></span>super__setMap<span class='keyword'></span>(<span class='keyword'></span>map<span class='keyword'></span>, <span class='keyword'></span>x<span class='keyword'></span>, <span class='keyword'></span>y<span class='keyword'></span>)<br>        <span class='keyword'></span>if<span class='keyword'></span> <span class='keyword'></span>not<span class='keyword'></span> <span class='keyword'></span>map<span class='keyword'></span>: <span class='keyword'></span>return<span class='keyword'></span><br>        <span class='keyword'></span>map<span class='keyword'></span>.<span class='keyword'></span>addHook<span class='keyword'></span>(<span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>, '<span class='keyword'></span>sayPreHook<span class='keyword'></span>')<br><br>    <span class='keyword'>def</span> <span class='function'>remove</span>(self):<br>        <span class='keyword'></span>map<span class='keyword'></span> = <span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>.<span class='keyword'></span>getMap<span class='keyword'></span>()<br>        <span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>.<span class='keyword'></span>super__remove<span class='keyword'></span>()<br>        <span class='keyword'></span>map<span class='keyword'></span>.<span class='keyword'></span>removeHook<span class='keyword'></span>(<span class='keyword'></span><span class='instance'>self</span><span class='keyword'></span>, '<span class='keyword'></span>sayPreHook<span class='keyword'></span>')<br><br>    <span class='keyword'>def</span> <span class='function'>hookEvent</span>(self, hookName, event):<br>        <span class='keyword'></span>msg<span class='keyword'></span> = '<span class='keyword'></span>Ummm<span class='keyword'></span>... ' + <span class='keyword'></span>event<span class='keyword'></span>.<span class='keyword'></span>getMessage<span class='keyword'></span>()<br>        <span class='keyword'></span>event<span class='keyword'></span>.<span class='keyword'></span>setMessage<span class='keyword'></span>(<span class='keyword'></span>msg<span class='keyword'></span>)

To execute it:

> clone wiz/rhialto/python/hooktest4.py
A new Test Map Hook has been placed in your inventory.
> drop test
You drop your Test Map Hook.
> say hi.
You say: Ummm... hi.

So a map-hook is just like a regular command hook, except it's for everyone who types that command in the map you register for.

A Note About Moves

Because of some technicalities around the way "move" commands are structured, it's not possible to change the direction of a move using a hook. However, you can do something else that works just as well:
  • veto the original move
  • substitute a different move in the agent's queue
Coming soon: an example that shows how to do this.

Room Hooks

Proximity Hooks

Method Hooks

<< Previous Chapter Next Chapter >>