Tutorials/Tutorial 3

Event Handlers

So now that you know how to make a plugin, and use player input via commands, there is still one critical part missing to give you the freedom to create basically any plugin you want. That is, you need to be able to respond not only to player input, but to anything that happens in the game without explicit player involvement. Bukkit provides a huge number of events for almost anything that can happen ingame, to which your plugin can respond by registering event handlers for these events.

Event Basics

An event handler is a function that is called when a specific event happens. There are a few differences between event handlers and command handlers:

  • An event handler takes only a single argument, the instance of the event
  • When registering an event handler, you specify the priority that your handler has. Priorities can be Lowest, Low, Normal, High, Highest and Monitor. The event traverses all registered handlers in this order, so handlers with priority Lowest get to act first, Monitor is last. The reasoning is, that a handler with Highest priority, needs to have the last word in what happens with the event, so that if a lower priority handler cancels the event, the higher priority one can say, "oh no you don't!". Monitor is a special priority, that should never change anything about the event. It is called after Highest, and therefore when the Monitor handlers are called, the event is finalized, and the Monitor handlers can observe the outcome and act accordingly.
  • The decorator to register an event handler is @hook.event(event_name,priority), where event_name is the full path of the event in the bukkit namespace omitting org.bukkit.event, and priority is one of the aforementioned priorities. For example, to register org.bukkit.event.player.PlayerJoinEvent with priority Normal you would use @hook.event("player.PlayerJoinEvent","Normal")
  • Same as with commands, the event handler registration also doesn't work with bound methods by default, so the modified PythonLoader is again a prerequisite for this tutorial. To use bound methods as event handlers, similar to the @hook.command decorator, True as an additional argument is added to the decorator, and hook.register(self) is added to the class constructor. With the previous example this would result in @hook.event("player.PlayerJoinEvent","Normal",True)

You can find a listing of all events and their methods in the bukkit docs in the package org.bukkit.event

This tutorial will consist of 2 separate plugins demonstrating event handlers

Part 1 - Grenades

This will use no explicit plugin main class

Plugin Specification

  • Apples are usable as explosive grenades after having been primed with /prime x, exploding x seconds after they have touched the ground when thrown, defaulting to 0 (explode on contact) if no argument is given
  • Golden Apples function as cluster grenades, spawning multiple apples that explode on contact

Plugin Foundation

As usual we define a function to be called when the plugin is enabled and disabled, nothing special here.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
__plugin_name__ = "Grenade Demo"
__plugin_version__ = "0.1"

@hook.enable
def onEnable():
    log.info("%s v%s enabled"%(__plugin_name__,__plugin_version__))

@hook.disable
def onDisable():
    log.info("%s disabled"%__plugin_name__)

Exploding Apples

What we want, is to tag an apple a player is carrying as a primed grenade when he types /prime x, so we need some place to save the information which player currently has primed a grenade, and to what time it is set. For this we use a simple global dictionary.

primed = {}

We also add a command handler for the /prime command that enters a player into the dictionary when they use the command.

1
2
3
4
5
6
@hook.command("prime",usage="/<command> [delay] (holding an apple or golden apple)")
def prime(sender,args):
    time = int(args[0])
    log.msg(sender,"Priming grenade for impact")
    primed[sender] = time
    return True

Note that we are making 3 assumptions here that can cause problems and should be handled properly

  • the command could be entered from console too, we want to ignore that
  • the command should only prime a grenade if the player is actually holding an apple or golden apple, we want to ignore any other case too
  • if they player gives an argument that cannot be converted to an integer, or no argument at all, int(args[0]) will break, so we should handle that case (i will just use None as the set time in that case, for reasons that will be clear later on)

To fix that, we add a few more lines

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@hook.command("prime",usage="/<command> [delay] (holding an apple or golden apple)")
def prime(sender,args):
    if sender.getName() == "CONSOLE" or sender.getItemInHand().getType() not in [bukkit.Material.APPLE,bukkit.Material.GOLDEN_APPLE]:
        return False
    try:
        time = max(0,min(int(args[0]),5))
        log.msg(sender,"Priming grenade with timer of  %ss"%time)
    except:
        time = None
        log.msg(sender,"Priming grenade for impact")
    primed[sender] = time
    return True

Here i also added a line to clamp the timer value to between 0 and 5 seconds, since we don't want negative times, and i think 5 is a good maximum, worked for worms, works for us :)

Now we can prime apples, but to actually have our apples function as grenades, we have to countdown the set timer once a player throws the grenade, and explode it when it reaches zero

Scheduled Tasks

We can accomplish a countdown timer by using a scheduled task, some function that will be called by bukkit either after a set number of server ticks (we don't need that) or repeatedly after a certain number of ticks has passed (we will use that to do an update function that checks periodically what our grenades are doing and explodes them if necessary)

To use bukkits builtin task scheduler, you need to define a class that implements the java Runnable interface (it basically just needs to have a method called run that takes no arguments). You also need to specify the ticks (one tick is 1/20th of a second normally) you want to pass before calling the method, we will use 1 tick to have a decent time resolution.

First we have to import the Runnable interface

from java.lang import Runnable

then define our timer class

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
class CountdownTimer(Runnable):
    def __init__(self):
        self.countdowns = {}

    def add(self,item,time):
        self.countdowns[item] = time

    def run(self):
        for item,time in self.countdowns.items():
            time -= 0.05
            if time <= 0:
                explode(item)
                del self.countdowns[item]
            else:
                self.countdowns[item] = time

Nothing special to see here, you can add objects with .add and after the time reaches zero, it will call explode, which we haven't defined yet.

Finally we will instantiate it as a global variable

timer = CountdownTimer()

One crucial piece still missing, is that you need to schedule the Runnable object with the bukkit scheduler. We can do this in the onEnable function, using this line:

server.getScheduler().scheduleSyncRepeatingTask(pyplugin,timer,0,1)

For the meaning of all of the arguments, consult the bukkit API docs, the relevant one here is the 1 that specifies the update interval (1 tick in our case, so 20 times per second)

Now we still need to define the explode function, which in its first iteration will look like this:

1
2
3
4
def explode(item):
    if item.getItemStack().getType() == bukkit.Material.APPLE:
        item.getWorld().createExplosion(item.getLocation(),4)
        item.remove()

It creates an explosion with power 4, and removes the apple. But how do we get the apple into the countdown timer in the first place?

Event Handling 1 - Item Drop Event

What we want, is to be notified if the player drops (throws) an item, and then if it is an apple and the player has previously primed it via /prime, we want to add it to our countdown timer.

1
2
3
4
5
6
7
8
9
@hook.event("player.PlayerDropItemEvent","monitor")
def onItemDrop(event):
    item = event.getItemDrop()
    player = event.getPlayer()
    if player in primed and (item.getItemStack().getType() == bukkit.Material.APPLE or item.getItemStack().getType() == bukkit.Material.GOLDEN_APPLE):
        time = primed[player]
        if time is not None:
            timer.add(item,primed[player])
        del primed[player]

Note that we have to check if the time in primed is None since that was what i used before if the argument is not specified correctly, we will add more to that later.

With all that in place, you should be able to prime and throw an apple, and have it explode. But there are a few things still missing (our golden cluster grenade), and a few edge cases to be sorted out (What should happen if the time is set to None ? What if a player primes an apple and then switches to another item? What will happen if a player picks up a thrown grenade again?), which we will to momentarily.

To be continued

Part 2 - Simple RPG Plugin

This plugin will use multiple classes in an object oriented approach

Plugin Specification

  • Simple attribute system with 2 stats: Attack and Magic
  • Three classes to choose (Warrior, Thief, Mage), each with its own special ability (Warrior can draw enemy attention to himself and use a battle shout to push enemies away from him, Thief has a chance of evading attacks and can steal items from monsters, Mage can cast spells)
  • Players can use the builtin experience levels of MC to purchase class levels (resetting all class levels they had if they choose a different class from their current one)
  • Current level and abilities can be displayed with /status
  • Spells can be selected with /setspell i where i is the number of the spell in the spell list displayed by typing /spells
  • Spells are cast by rightclicking with empty hands with a spell selected

To be continued


Comments

Posts Quoted:
Reply
Clear All Quotes