1

I have a custom event in my bukkit/spigot plugin that extends PlayerInteractEvent which attempts to open chests in a nearby area around the player.

Currently the code uses this event to make sure that no other plugins (Grief prevention, for example) object to the player being able to open the chest. If the player can open the chest, my plugin will attempt to deposit items into the chest. I would like to ignore the setCancelled() if it's called by a certain plugin (ideally) or class (as a work around)

From this question I can see that to get the class I can use

String callerClassName = new Exception().getStackTrace()[1].getClassName();
String calleeClassName = new Exception().getStackTrace()[0].getClassName();

To get the classnames. Alternatively I can use something around this call:

StackTraceElement[] stElements = Thread.currentThread().getStackTrace();

However, all the comments on that question state that there is likely a better way to do it, other than what this is doing.

Does Bukkit have any better way of doing this?

For reference, this is the entirety of my custom player interact event:

public class FakePlayerInteractEvent extends PlayerInteractEvent {
    public FakePlayerInteractEvent(Player player, Action rightClickBlock, ItemStack itemInHand, Block clickedBlock, BlockFace blockFace) {
        super(player, rightClickBlock, itemInHand, clickedBlock, blockFace);
    }
}

And the code surrounding the use of the event:

PlayerInteractEvent fakeEvent = AutomaticInventory.getInstance().new FakePlayerInteractEvent(player, Action.RIGHT_CLICK_BLOCK, player.getInventory().getItemInMainHand(), block, BlockFace.UP);
Bukkit.getServer().getPluginManager().callEvent(fakeEvent);
if(!fakeEvent.isCancelled()){ ... do stuff }
Community
  • 1
  • 1
user3246152
  • 111
  • 6
  • Yes, there is a better solution. I will post more details as an answer as soon as I check a few things out. – Frelling Mar 03 '17 at 19:00

2 Answers2

1

Excellent question! For the moment let me ignore the reason that spurred this question. Bukkit does not “publish” a means for determining the source of event cancellations. However, your approach to “evaluating” an event is on the right track.

As you already know or suspect, using stack traces are not a good solution. They are relatively expensive to generate and depict implementation-specific details that may not necessarily be guaranteed to remain the same. A better approach is to mimic Bukkit’s event firing process used when calling callEvent().

While the event firing process implementation is not guaranteed by the Bukkit API it has been stable for a number of years and hasn't changed much. It's work for us the past 5 years, only requiring one minor refactoring when callEvent() was split into callEvent()/fireEvent().

I wish I could give you the whole EventUtils helper class, but I had to redact it due to copyright concerns. I did verify that this reduced class passed appropriate unit tests. You or anyone else is free to use this code as seen fit. Its comments explain operations in more details. I should note that we use Doxygen, not JavaDoc, for documentation generation.

public class EventUtils {

    /**
     * @brief Determine if the given event will be cancelled.
     * 
     * This method emulates Bukkit's SimplePluginManager.fireEvent() to evaluate whether it will
     * be cancelled. This is preferred over using callEvent() as this method can limit the scope
     * of evaluation to only plugins of interest. Furthermore, this method will terminate as soon
     * as the event is cancelled to minimize any *side effects* from plugins further down the event
     * chain (e.g. mcMMO). No evaluation will be performed for events that do not
     * implement Cancellable.
     * 
     * The given plugin set is interpreted as either an Allow set or a Deny set, as follows:
     * 
     * - \c allowDeny = \c false - Allow mode. Only enabled plugins included in the given plugin
     *   set will be evaluated.
     * - \c allowDeny = \c false - Deny mode. Only enabled plugins *not* included in the given
     *   plugin set will be evaluated.
     * 
     * @warning Care should be taken when using this method from within a plugin's event handler for
     * the same event or event type (e.g. "faked" events). As this may result in an unending
     * recursion that will crash the server. To prevent this situation, the event handler should
     * (given in order of preference): 1) restrict evaluation to a specific Allow set not including
     * its own plugin; or, 2) add its own plugin to a Deny set. See overloaded convenience methods
     * for more details.
     * 
     * @param evt event under test
     * @param plugins Allow/Deny plugin set
     * @param allowDeny \c false - evaluate using an Allow set; or \c true - evaluate using a
     *        Deny set.
     * @return first plugin that cancelled given event; or \c if none found/did
     */

    public static Plugin willCancel( Event evt, Set<Plugin> plugins, boolean allowDeny ) {
        PluginManager piMgr = Bukkit.getPluginManager();

        /*
         * 1. From SimplePluginManager.callEvent(). Check thread-safety and requirements as if this
         * were a normal event call.
         */
        if ( evt.isAsynchronous() ) {
            if ( Thread.holdsLock( piMgr ) ) {
                throw new IllegalStateException( evt.getEventName()
                        + " cannot be triggered asynchronously from inside synchronized code." );
            }
            if ( Bukkit.isPrimaryThread() ) {
                throw new IllegalStateException( evt.getEventName()
                        + " cannot be triggered asynchronously from primary server thread." );
            }
            return fireUntilCancelled( evt, plugins, allowDeny );
        }
        else {
            synchronized ( piMgr ) {
                return fireUntilCancelled( evt, plugins, allowDeny );
            }
        }

    }


    /**
     * @brief See willCancel() for details.
     * 
     * @note Scoped as `protected` method for unit testing without reflection.
     * 
     * @param evt event under test
     * @param plugins Allow/Deny plugin set
     * @param allowDeny \c false - evaluate using an Allow set; or \c true - evaluate using a
     *        Deny set.
     * @return first plugin that cancelled given event; or \c if none found/did
     */
    protected static Plugin fireUntilCancelled( Event evt, Set<Plugin> plugins, boolean allowDeny ) {

        /*
         * 1. If event cannot be canceled, nothing will cancel it.
         */

        if ( !(evt instanceof Cancellable) )
            return null;

        /*
         * 2. Iterate over the event's "baked" event handler list.
         */

        HandlerList handlers = evt.getHandlers();
        for ( RegisteredListener l : handlers.getRegisteredListeners() ) {

            /*
             * A. Is associated plugin applicable? If not, move to next listener.
             */

            if ( !ofInterest( l.getPlugin(), plugins, allowDeny ) )
                continue;

            /*
             * B. Call registered plugin listener. If event is marked cancelled afterwards, return
             * reference to canceling plugin.
             */

            try {
                l.callEvent( evt );
                if ( ((Cancellable) evt).isCancelled() )
                    return l.getPlugin();
            }
            catch ( EventException e ) {

                /*
                 * Can be safely ignored as it is only used to nag developer about legacy events
                 * and similar matters.
                 */
            }
        }
        return null;
    }


    /**
     * @brief Determine whether the given plugin is of interest.
     * 
     * This method determines whether the given plugin is of interest. A plugin is of no interest
     * if any of the following conditions are met:
     * 
     * - the plugin is disabled
     * - \c allowDeny is \c false (allow) and set does not contains plugin
     * - \c allowDeny is \c true (deny) and set contains plugin
     * 
     * @note Scoped as `protected` method for unit testing without reflection.
     * 
     * @param plugin plugin to evaluate
     * @param plugins plugin allow/deny set
     * @param allowDeny \c false validate against allow set; \c true validate against deny set
     * @return \c true plugin is of interest; \c false otherwise
     */

    protected static boolean ofInterest( Plugin plugin, Set<Plugin> plugins, boolean allowDeny ) {
        if ( !plugin.isEnabled() )
            return false;

        return allowDeny ^ plugins.contains( plugin );
    }
}
Frelling
  • 3,287
  • 24
  • 27
0

I would recommend using Priorities


Priority goes in the following order:

  1. LOWEST
  2. LOW
  3. NORMAL
  4. HIGH
  5. HIGHEST
  6. MONITOR

If you set your event Priority to HIGHEST or MONITOR, your events will listen to the event after all the other priorities have listened to it. For instance, you can still listen to the event even if another plugin tried to cancel it.

NOTE: It's not recommended to change the outcome of an event with MONITOR priority. It should be only used for monitoring.

Changing an Event priority (default: NORMAL)

@EventHandler (priority = EventPriority.HIGHEST)
public void onEvent(Event e) {
}

If you want other plugins to act after your plugin on the final-say, for example, if you want World Edit to be "Stronger" than your plugin, set the priority to LOW or LOWEST. If you want your plugin to take the final say, increase that priority.

@EDIT

If you want to do it without priority, and actually need to identify the cancelling plugin, Comprehenix from bukkit forums have a solution for you. Bear in mind it's not recommended

Example on how to do it:

public class ExampleMod extends JavaPlugin implements Listener {

private CancellationDetector<BlockPlaceEvent> detector = new CancellationDetector<BlockPlaceEvent>(BlockPlaceEvent.class);

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

        detector.addListener(new CancelListener<BlockPlaceEvent>() {
            @Override
            public void onCancelled(Plugin plugin, BlockPlaceEvent event) {
                System.out.println(event + " cancelled by " + plugin);
            }
        });
    }

    @Override
    public void onDisable() {
        // Incredibly important!
        detector.close();
    }

    // For testing
    @EventHandler
    public void onBlockPlaceEvent(BlockPlaceEvent e) {
        e.setCancelled(true);
    }
}

You can find CancellationDetector's git here

LeoColman
  • 6,950
  • 7
  • 34
  • 63
  • The event handler is already set to `MONITOR`. The question is more 'how can I tell which class called setCancelled' and less 'how do I tell if it was cancelled' – Mitch Mar 03 '17 at 19:02
  • The point of the answer as to show that it's not really necessary to know which class cancelled the event if you just want to use it anyways – LeoColman Mar 03 '17 at 19:27
  • I want to do it selectively. If plugin A cancels it I want to ignore that cancellation, but if plugin B cancels it I want to obey that cancellation – Mitch Mar 03 '17 at 19:28
  • There's a way to do it, but it's a hacky solution. You probably really don't need this, but I'll edit the answer with what can be done – LeoColman Mar 03 '17 at 19:32
  • Check if the updated answer is enough information for you – LeoColman Mar 03 '17 at 19:37