1. Philip Smith
  2. WatchFox

Wiki

Clone wiki

WatchFox / API / woolpainter

WoolPainter

A simple plugin to change the color of placed wool


Topics:

  • Logging custom events with blockstate
  • Implementing a rollback agent (enabling rollback support)
  • Using the tasking abilities of the rollback engine
  • Understanding the doPreRun() and doPostRun() operations

Goal:

The idea for this plugin is to be able to change the color of wool blocks by right clicking on them with a dye. We will also add WatchFox logging and rollback support.

Basic program flow:

-initialize state
-register StopHandler with WatchFox
-register WoolPainter plugin with WatchFox
-register woolpainter_paint event with WatchFox

-listening for paint events and painting
-logging the custom event

Note:

The mechanics here are very similar to those in HeroFox. Only the rollback and blockstate logging will be explained in-depth. If you have not read the tutorial for HeroFox, it would be wise to do so.

The code:


Skeleton


public class HeroFox extends JavaPlugin implements Listener {
    @Override
    public void onLoad() {
        // registering code here
    }

    @Override
    public void onEnable() {
        // Bukkit listener registration here
    }
}

Element -- initialize state


private Logger logger;
final WatchFoxInterface wf = (WatchFoxInterface) Bukkit.getPluginManager().getPlugin("WatchFox");
this.logger = this.getLogger();

Element -- register StopHandler with WatchFox


Since we want to this plugin to still work even if WatchFox dies, we'll use a volatile boolean to track whether or not we should log.

private volatile boolean doLog = true;
wf.registerStopHandler(new Runnable() {
    @Override
    public void run() {
        WoolPainter.this.logger.log(Level.SEVERE,
                "WoolPainter detected WatchFox is halting. WoolPainter will stop logging.");
        WoolPainter.this.doLog = false;
    }
});

Element -- register WoolPainter plugin with WatchFox


private BukkitPluginToken token;
this.token = new BukkitPluginToken(wf.registerPlugin("woolpainter"));

Element -- register woolpainter_paint event with WatchFox


This event will actually have 5 aspects unlike HeroFox which only had 4. The difference being this event will have a RollbackAgent to facilitate the event being rolled back.

The parameters for registering this action:
-Event names - we'll use woolpainter_paint and wool_paint
-A byte code - 0's still perfectly usable
-Is the event a "block event" - In this case, yes, see the docs for the definition
-An EventTranslator - we'll go with "PLAYER painted a wool block COLOR"
-A RollbackAgent - we'll go into depth with this one

The RollbackAgent:

There are two kinds of RollbackAgents, the plain one used here, and a simplified one that eliminates the manager references and the doPreRun() and doPostRun() methods. Since this is an arbitrary tutorial, we'll unnecessarily use the more complex one.

-String[] getBlacklist()

This method returns an array of event names that will supersede this event. Consider a block_place action; it's superseded if the block is then later broken. Or if fades. Or if it burns.

For this event, we will respond 'true' as the third parameter in the event registration. This says that our event changes the block. WatchFox will compile all events that respond true, and make this list available so events like ours have a simple (and naturally compatible with other plugins) way to determine the blacklist.

For example, saying PlayerA placed a white wool block and then PlayerB painted it. An attempt to revert PlayerA's wool placement will be superseded because a wool_paint event occurred there, it's a 'block event', and built-in RollbackAgents (in WatchFox) honor this designation.

-SimpleBlock previewRollback(SimpleEvent, TaskingRollbackManager)

This method returns the SimpleBlock or null that would be restored IF a rollback were to occur. Null in the event of no change. Like the method name suggests, this is invoked when a moderator uses WatchFox's preview ability.

Our implementation will return a wool block of the color it used to be (which was stored in the miscdata field).

-SimpleBlock rollback(SimpleEvent, TaskingRollbackManager)

This method, like previewing, returns a SimpleBlock or null. It should also rollback any internal state to your plugin. If we ran a bank plugin, transactions should be reversed, for example.

Like any other method, these methods can do anything they want until the return statement. In our bank plugin example, we could reverse the transaction within this method (and it'd be a decent idea to do so). However, if synchronization is an issue or you need to interface with the Bukkit API or you want to process something after the block has been rolled back, you can schedule a task to the manager.
Tasks are executed synchronous to the Bukkit server after the block changes have been made, and in the order they were scheduled.

Here, we immediately log when an event is being rolled back, as well as scheduling a Runnable to increment a session counter. (Don't feel obligated to output every step of the rollback progress, this is just a bad demo)

-SimpleBlock replay(SimpleEvent, TaskingRollbackManager)

This method is the opposite of the above methods, it should return the block as the event 'made' it. Likewise, it should also repeat whatever transaction occurred in your plugin.

In the bank example, the transaction should be repeated. Since funds might no longer be valid, this may require certain balances to go negative.

If this sounds dangerous, well, that's because it is, however, you MUST implement this method. While used in the obvious case of /wf replay it is also used to UNDO rollbacks. That noted, be sure to stay 'safe'; avoid NullPointerErrors and negative indexes and the like, but do allow state to go 'illegal' as in the bank example. No one wants to undo a rollback only to find a plugin ignores the command and makes the state even more fractured!

Here, we simply return a wool block as it was, painted.

-void doPreRun(StoppableRollbackManager)

This method is called, synchronous to Bukkit, once per event TYPE at the beginning of a preview, rollback, or replay. Decently intuitively, this exact method as it implemented is only fired once (for this particular event). If you reuse a RollbackAgent, the pre and post-run methods are fired for as many events that reuse it.

It's simpler than it sounds. This method is fired once per "block_break" and "block_place" and "wool_paint" event, regardless of what the implementation is.

You'll note that this manager is of the Stoppable variety. Should you require a particular state, say you require the player to have an empty session, you can stop the rollback from happening. However, you must give the moderator a detailed error message and explanation of how to quickly fix the problem.

Since errors can occur at anytime, do not rely on scheduled Tasks or a PostRun to make the state "cleanable". At the very least, have a backup command that wipes whatever state is present and makes future action possible.

Here, we initialize a session to count the number of blocks get rolled back, regardless of its current state.

-void doPostRun(RollbackManager)

This method is called, synchronous to Bukkit, once per event type at the end of a preview, rollback, or replay.

If the rollback initiator has anything needing his attention, this is the place to send that message. Or, if your plugin can manage itself, it's the place to clean-up and prepare for further action.

Here, we send a message announcing how many blocks were rolled back (if not zero) and we also kill the session.

this.token.registerAction(new String[]{"woolpainter_paint", "wool_paint"}, (byte) 0, true, new EventTranslator() {
            @Override
            public String translate(final SimpleEvent event) {
                // PLAYER painted a wool block COLOR
                return event.player + " painted a wool block " + DyeColor.getByData(event.itemmeta).name().toLowerCase();
            }
        }, new RollbackAgent() {
            @Override
            public String[] getBlacklist() {
                return wf.getBlockEvents();
            }

            @Override
            public SimpleBlock previewRollback(final SimpleEvent event, final TaskingRollbackManager manager) {
                return new SimpleBlock(35, event.miscdata[0]);
            }

            @Override
            public SimpleBlock rollback(final SimpleEvent event, final TaskingRollbackManager manager) {
                WoolPainter.this.logger.log(Level.INFO, "Rolling back a wool paint event!");
                manager.addTask(new Runnable() {
                    @Override
                    public void run() {
                        WoolPainter.this.sessions.put(manager.getInitiator(), WoolPainter.this.sessions.get(manager
                                .getInitiator()) + 1);
                    }
                });
                return new SimpleBlock(35, event.miscdata[0]);
            }

            @Override
            public SimpleBlock replay(final SimpleEvent event, final TaskingRollbackManager manager) {
                return event.getSimpleBlock();
            }

            @Override
            public void doPreRun(final StoppableRollbackManager manager) {
                // if (badCondition)
                //     manager.stopRollback("something is borked");
                WoolPainter.this.sessions.put(manager.getInitiator(), 0);
            }

            @Override
            public void doPostRun(final RollbackManager manager) {
                if (WoolPainter.this.sessions.get(manager.getInitiator()) != 0) {
                    Bukkit.getPlayerExact(manager.getInitiator()).sendMessage(
                            WoolPainter.this.sessions.get(manager.getInitiator()).toString() + " wool blocks were reverted!"
                    );
                }
                WoolPainter.this.sessions.remove(manager.getInitiator());
            }
        }
);

Element -- listening for paint events and painting


@Override
public void onEnable() {
    Bukkit.getPluginManager().registerEvents(this, this);
}
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
public void onClick(final PlayerInteractEvent event) {
    if (event.getAction() == Action.RIGHT_CLICK_BLOCK && event.hasItem()
            && event.getClickedBlock().getTypeId() == 35) {
        final ItemStack item = event.getItem();
        if (item.getTypeId() == 351 && event.getClickedBlock().getData() != 15 - item.getDurability()) {
            // log event if doLog
            if (this.doLog) {
                // WatchFox logging here
            }
            // change block type
            event.getClickedBlock().setData((byte) (15 - item.getDurability()));
            // decrement player hand inventory if not creative mode
            if (event.getPlayer().getGameMode() != GameMode.CREATIVE) {
                if (item.getAmount() == 1) {
                    event.getPlayer().setItemInHand(null);
                } else {
                    item.setAmount(item.getAmount() - 1);
                }
            }
        }
    }
}

Element -- logging the custom event


The system used to store the actual event detail was glossed over in the RollbackManager section, but if you read the code, you'll have a good idea how this works.

When using blockstates (not necessarily a Bukkit BlockState), there is often a from->to relationship. However, WatchFox only has one item/meta combo. Naturally, it also has no implicit from->to data entry.

Instead, the item/meta combo should be used on what's most intuitive to the event. For example, you place a stone block (prev->STONE), you break a dirt block (DIRT->air). (The words in caps are the blocks WatchFox uses as the primary blockstate)

The lesser important state is stored in miscdata as a byte array. There is a utility function that will transform an int and a byte (a SimpleBlock as it is) into a 5 byte array.

This is somewhat of a bad example since the block will always be a 'wool' (id 35), but I do store the old byte in the misc field.

The new color is stored (and calculated) in the primary metadata field. This allows someone to search for the new colored wool instead of the old color. (I figure you search for Mona Lisa, not blank canvas)

WoolPainter.this.token.logEvent(event.getClickedBlock().getLocation(), (byte) 0, event.getPlayer().getName(),
        35, (byte) (15 - item.getDurability()), new byte[]{event.getClickedBlock().getData()});

The final code

public class WoolPainter extends JavaPlugin implements Listener {
    private Logger logger;
    private BukkitPluginToken token;

    final HashMap<String, Integer> sessions = new HashMap<>();

    private volatile boolean doLog = true;

    @Override
    public void onLoad() {
        final WatchFoxInterface wf = (WatchFoxInterface) Bukkit.getPluginManager().getPlugin("WatchFox");
        this.logger = this.getLogger();
        wf.registerStopHandler(new Runnable() {
            @Override
            public void run() {
                WoolPainter.this.logger.log(Level.SEVERE,
                        "WoolPainter detected WatchFox is halting. WoolPainter will stop logging.");
                WoolPainter.this.doLog = false;
            }
        });
        this.token = new BukkitPluginToken(wf.registerPlugin("woolpainter"));
        this.token.registerAction(new String[]{"woolpainter_paint", "wool_paint"}, (byte) 0, true, new EventTranslator() {
                    @Override
                    public String translate(final SimpleEvent event) {
                        // PLAYER painted a wool block COLOR
                        return event.player + " painted a wool block " + DyeColor.getByData(event.itemmeta).name().toLowerCase();
                    }
                }, new RollbackAgent() {
                    @Override
                    public String[] getBlacklist() {
                        return wf.getBlockEvents();
                    }

                    @Override
                    public SimpleBlock previewRollback(final SimpleEvent event, final TaskingRollbackManager manager) {
                        return new SimpleBlock(35, event.miscdata[0]);
                    }

                    @Override
                    public SimpleBlock rollback(final SimpleEvent event, final TaskingRollbackManager manager) {
                        WoolPainter.this.logger.log(Level.INFO, "Rolling back a wool paint event!");
                        manager.addTask(new Runnable() {
                            @Override
                            public void run() {
                                WoolPainter.this.sessions.put(manager.getInitiator(), WoolPainter.this.sessions.get(manager
                                        .getInitiator()) + 1);
                            }
                        });
                        return new SimpleBlock(35, event.miscdata[0]);
                    }

                    @Override
                    public SimpleBlock replay(final SimpleEvent event, final TaskingRollbackManager manager) {
                        return event.getSimpleBlock();
                    }

                    @Override
                    public void doPreRun(final StoppableRollbackManager manager) {
                        //    if (badCondition)
                        //        manager.stopRollback("something is borked");
                        WoolPainter.this.sessions.put(manager.getInitiator(), 0);
                    }

                    @Override
                    public void doPostRun(final RollbackManager manager) {
                        if (WoolPainter.this.sessions.get(manager.getInitiator()) != 0) {
                            Bukkit.getPlayerExact(manager.getInitiator()).sendMessage(
                                    WoolPainter.this.sessions.get(manager.getInitiator()).toString() + " wool blocks were reverted!"
                            );
                        }
                        WoolPainter.this.sessions.remove(manager.getInitiator());
                    }
                }
        );
    }

    @Override
    public void onEnable() {
        Bukkit.getPluginManager().registerEvents(this, this);
    }

    @EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
    public void onClick(final PlayerInteractEvent event) {
        if (event.getAction() == Action.RIGHT_CLICK_BLOCK && event.hasItem()
                && event.getClickedBlock().getTypeId() == 35) {
            final ItemStack item = event.getItem();
            if (item.getTypeId() == 351 && event.getClickedBlock().getData() != 15 - item.getDurability()) {
                // log event if doLog
                if (this.doLog) {
                    WoolPainter.this.token.logEvent(event.getClickedBlock().getLocation(), (byte) 0, event.getPlayer().getName(),
                            35, (byte) (15 - item.getDurability()), new byte[]{event.getClickedBlock().getData()});
                }
                // change block type
                event.getClickedBlock().setData((byte) (15 - item.getDurability()));
                // decrement player hand inventory if not creative mode
                if (event.getPlayer().getGameMode() != GameMode.CREATIVE) {
                    if (item.getAmount() == 1) {
                        event.getPlayer().setItemInHand(null);
                    } else {
                        item.setAmount(item.getAmount() - 1);
                    }
                }
            }
        }
    }
}

Updated