|
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
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!
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:
-
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).
-
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:
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.
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.
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.
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 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.
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.
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.
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.
|