Improved API and performance.

API changes:
 * The PacketListener now uses a "ListeningWhitelist" class 
   to report which packet ids it wishes to listen in on for
   either the client or the server. This makes it possible to 
   use your plugin class as the listener more easily.
   
 * Added a priority system similar to Bukkit events.
   
Performance changes:
 * Create and maintain a separate list of listeners for each packet
   ID. This uses slightly more memory, but is far more efficient. 
   Especially in light of the priority system.

In addition, the listener lists are (hopefully) concurrent. They're
optimized for read-access, however. Adding or removing a listener
is a O(n) operation.
This commit is contained in:
Kristian S. Stangeland 2012-09-14 19:12:08 +02:00
parent bb8bec907c
commit 88dcf0cb32
9 changed files with 413 additions and 142 deletions

View File

@ -119,11 +119,17 @@ public interface ProtocolManager {
public PacketContainer createPacket(int id, boolean skipDefaults);
/**
* Retieves a set of every enabled packet.
* @return Every packet filter.
* Retrieves a immutable set containing the ID of the sent server packets that will be observed by listeners.
* @return Every filtered server packet.
*/
public Set<Integer> getPacketFilters();
public Set<Integer> getSendingFilters();
/**
* Retrieves a immutable set containing the ID of the recieved client packets that will be observed by listeners.
* @return Every filtered client packet.
*/
public Set<Integer> getReceivingFilters();
/**
* Determines whether or not this protocol mananger has been disabled.
* @return TRUE if it has, FALSE otherwise.

View File

@ -1,3 +1,20 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
/**

View File

@ -0,0 +1,110 @@
/*
* ProtocolLib - Bukkit server library that allows access to the Minecraft protocol.
* Copyright (C) 2012 Kristian S. Stangeland
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 2 of
* the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with this program;
* if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
* 02111-1307 USA
*/
package com.comphenix.protocol.events;
import java.util.Set;
import com.google.common.base.Objects;
import com.google.common.collect.Sets;
/**
* Determines which packets will be observed by a listener, and with what priority.
* @author Kristian
*/
public class ListeningWhitelist {
/**
* A whitelist with no packets - indicates that the listener shouldn't observe any packets.
*/
public static ListeningWhitelist EMPTY_WHITELIST = new ListeningWhitelist(ListenerPriority.LOW);
private ListenerPriority priority;
private Set<Integer> whitelist;
/**
* Creates a packet whitelist for a given priority with a set of packet IDs.
* @param priority - the listener priority.
* @param whitelist - set of IDs to observe/enable.
*/
public ListeningWhitelist(ListenerPriority priority, Set<Integer> whitelist) {
this.priority = priority;
this.whitelist = whitelist;
}
/**
* Creates a packet whitelist of a given priority for a list of packets.
* @param priority - the listener priority.
* @param whitelist - list of packet IDs to observe/enable.
*/
public ListeningWhitelist(ListenerPriority priority, Integer... whitelist) {
this.priority = priority;
this.whitelist = Sets.newHashSet(whitelist);
}
/**
* Whether or not this whitelist has any enabled packets.
* @return TRUE if there are any packets, FALSE otherwise.
*/
public boolean isEnabled() {
return whitelist != null || whitelist.size() > 0;
}
/**
* Retrieve the priority in the execution order of the packet listener. Highest priority will be executed last.
* @return Execution order in terms of priority.
*/
public ListenerPriority getPriority() {
return priority;
}
/**
* Retrieves the list of packets that will be observed by the listeners.
* @return Packet whitelist.
*/
public Set<Integer> getWhitelist() {
return whitelist;
}
@Override
public int hashCode(){
return Objects.hashCode(priority, whitelist);
}
@Override
public boolean equals(final Object obj){
if(obj instanceof ListeningWhitelist){
final ListeningWhitelist other = (ListeningWhitelist) obj;
return Objects.equal(priority, other.priority)
&& Objects.equal(whitelist, other.whitelist);
} else{
return false;
}
}
@Override
public String toString() {
if (this == EMPTY_WHITELIST)
return "EMPTY_WHITELIST";
else
return Objects.toStringHelper(this)
.add("priority", priority)
.add("packets", whitelist).toString();
}
}

View File

@ -17,12 +17,8 @@
package com.comphenix.protocol.events;
import java.util.Set;
import org.bukkit.plugin.Plugin;
import com.google.common.base.Joiner;
import com.google.common.collect.Sets;
/**
* Represents a packet listener with useful constructors.
@ -32,10 +28,10 @@ import com.google.common.collect.Sets;
public abstract class PacketAdapter implements PacketListener {
protected Plugin plugin;
protected Set<Integer> packetsID;
protected ConnectionSide connectionSide;
protected ListenerPriority listenerPriority;
protected ListeningWhitelist receivingWhitelist = ListeningWhitelist.EMPTY_WHITELIST;
protected ListeningWhitelist sendingWhitelist = ListeningWhitelist.EMPTY_WHITELIST;
/**
* Initialize a packet listener.
* @param plugin - the plugin that spawned this listener.
@ -54,10 +50,23 @@ public abstract class PacketAdapter implements PacketListener {
* @param packets - the packet IDs the listener is looking for.
*/
public PacketAdapter(Plugin plugin, ConnectionSide connectionSide, ListenerPriority listenerPriority, Integer... packets) {
if (plugin == null)
throw new IllegalArgumentException("plugin cannot be null");
if (connectionSide == null)
throw new IllegalArgumentException("connectionSide cannot be null");
if (listenerPriority == null)
throw new IllegalArgumentException("listenerPriority cannot be null");
if (packets == null)
throw new IllegalArgumentException("packets cannot be null");
// Add whitelists
if (connectionSide.isForServer())
sendingWhitelist = new ListeningWhitelist(listenerPriority, packets);
if (connectionSide.isForClient())
receivingWhitelist = new ListeningWhitelist(listenerPriority, packets);
this.plugin = plugin;
this.connectionSide = connectionSide;
this.packetsID = Sets.newHashSet(packets);
this.listenerPriority = listenerPriority;
}
@Override
@ -71,13 +80,13 @@ public abstract class PacketAdapter implements PacketListener {
}
@Override
public ConnectionSide getConnectionSide() {
return connectionSide;
public ListeningWhitelist getReceivingWhitelist() {
return receivingWhitelist;
}
@Override
public Set<Integer> getPacketsID() {
return packetsID;
public ListeningWhitelist getSendingWhitelist() {
return sendingWhitelist;
}
@Override
@ -85,11 +94,6 @@ public abstract class PacketAdapter implements PacketListener {
return plugin;
}
@Override
public ListenerPriority getListenerPriority() {
return listenerPriority;
}
/**
* Retrieves the name of the plugin that has been associated with the listener.
* @return Name of the associated plugin.
@ -113,8 +117,9 @@ public abstract class PacketAdapter implements PacketListener {
@Override
public String toString() {
// This is used by the error reporter
return String.format("PacketAdapter[plugin=%s, side=%s, packets=%s]",
getPluginName(this), getConnectionSide().name(),
Joiner.on(", ").join(packetsID));
return String.format("PacketAdapter[plugin=%s, sending=%s, receiving=%s]",
getPluginName(this),
sendingWhitelist,
receivingWhitelist);
}
}

View File

@ -17,8 +17,6 @@
package com.comphenix.protocol.events;
import java.util.Set;
import org.bukkit.plugin.Plugin;
/**
@ -43,22 +41,16 @@ public interface PacketListener {
public void onPacketReceiving(PacketEvent event);
/**
* Retrieve whether or not we're listening for client or server packets.
* @return The type of packets we expect.
* Retrieve which packets sent by the server this listener will observe.
* @return List of server packets to observe, along with the priority.
*/
public ConnectionSide getConnectionSide();
public ListeningWhitelist getSendingWhitelist();
/**
* Retrieve the priority in the execution order of this packet listener. Highest priority will be executed last.
* @return Execution order in terms of priority.
* Retrieve which packets sent by the client this listener will observe.
* @return List of server packets to observe, along with the priority.
*/
public ListenerPriority getListenerPriority();
/**
* Set of packet ids we expect to recieve.
* @return Packets IDs.
*/
public Set<Integer> getPacketsID();
public ListeningWhitelist getReceivingWhitelist();
/**
* Retrieve the plugin that created list packet listener.

View File

@ -1,7 +1,6 @@
package com.comphenix.protocol.injector;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@ -9,10 +8,11 @@ import java.util.concurrent.ConcurrentMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.comphenix.protocol.events.ListenerPriority;
import com.comphenix.protocol.events.ListeningWhitelist;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.events.PacketListener;
import com.google.common.base.Objects;
import com.google.common.primitives.Ints;
/**
@ -23,36 +23,32 @@ import com.google.common.primitives.Ints;
public class ConcurrentListenerMultimap {
// The core of our map
protected ConcurrentMap<Integer, SortedArrayList<PacketListener>> listeners =
new ConcurrentHashMap<Integer, SortedArrayList<PacketListener>>();
protected ConcurrentMap<Integer, SortedCopyOnWriteArray<PrioritizedListener>> listeners =
new ConcurrentHashMap<Integer, SortedCopyOnWriteArray<PrioritizedListener>>();
/**
* Adds a listener to its requested list of packet recievers.
* @param listener - listener with a list of packets to recieve notifcations for.
*/
public void addListener(PacketListener listener) {
for (Integer packetID : listener.getPacketsID()) {
addListener(packetID, listener);
public void addListener(PacketListener listener, ListeningWhitelist whitelist) {
PrioritizedListener prioritized = new PrioritizedListener(listener, whitelist.getPriority());
for (Integer packetID : whitelist.getWhitelist()) {
addListener(packetID, prioritized);
}
}
// Add the listener to a specific packet notifcation list
private void addListener(Integer packetID, PacketListener listener) {
private void addListener(Integer packetID, PrioritizedListener listener) {
SortedArrayList<PacketListener> list = listeners.get(packetID);
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(packetID);
// We don't want to create this for every lookup
if (list == null) {
// It would be nice if we could use a PriorityBlockingQueue, but it doesn't preseve iterator order,
// which is a essential feature for our purposes.
final SortedArrayList<PacketListener> value = new SortedArrayList<PacketListener>(new Comparator<PacketListener>() {
@Override
public int compare(PacketListener o1, PacketListener o2) {
// This ensures that lower priority listeners are executed first
return Ints.compare(o1.getListenerPriority().getSlot(),
o2.getListenerPriority().getSlot());
}
});
final SortedCopyOnWriteArray<PrioritizedListener> value = new SortedCopyOnWriteArray<PrioritizedListener>();
list = listeners.putIfAbsent(packetID, value);
@ -74,21 +70,26 @@ public class ConcurrentListenerMultimap {
* @param listener - listener to remove.
* @return Every packet ID that was removed due to no listeners.
*/
public List<Integer> removeListener(PacketListener listener) {
public List<Integer> removeListener(PacketListener listener, ListeningWhitelist whitelist) {
List<Integer> removedPackets = new ArrayList<Integer>();
// Again, not terribly efficient. But adding or removing listeners should be a rare event.
for (Integer packetID : listener.getPacketsID()) {
for (Integer packetID : whitelist.getWhitelist()) {
SortedArrayList<PacketListener> list = listeners.get(packetID);
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(packetID);
// Remove any listeners
if (list != null) {
synchronized(list) {
// Don't remove from newly created lists
if (list.size() > 0) {
list.removeAll(listener);
// Remove this listener
for (Iterator<PrioritizedListener> it = list.iterator(); it.hasNext(); ) {
if (it.next().getListener().equals(list)) {
it.remove();
}
}
if (list.size() == 0) {
listeners.remove(packetID);
@ -110,20 +111,21 @@ public class ConcurrentListenerMultimap {
* @param event - the packet event to invoke.
*/
public void invokePacketRecieving(Logger logger, PacketEvent event) {
SortedArrayList<PacketListener> list = listeners.get(event.getPacketID());
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(event.getPacketID());
if (list == null)
return;
// We have to be careful. Cannot modify the underlying list when sending notifications.
synchronized (list) {
for (PacketListener listener : list) {
for (PrioritizedListener element : list) {
try {
listener.onPacketReceiving(event);
element.getListener().onPacketReceiving(event);
} catch (Exception e) {
// Minecraft doesn't want your Exception.
logger.log(Level.SEVERE,
"Exception occured in onPacketReceiving() for " + PacketAdapter.getPluginName(listener), e);
"Exception occured in onPacketReceiving() for " +
PacketAdapter.getPluginName(element.getListener()), e);
}
}
}
@ -135,75 +137,50 @@ public class ConcurrentListenerMultimap {
* @param event - the packet event to invoke.
*/
public void invokePacketSending(Logger logger, PacketEvent event) {
SortedArrayList<PacketListener> list = listeners.get(event.getPacketID());
SortedCopyOnWriteArray<PrioritizedListener> list = listeners.get(event.getPacketID());
if (list == null)
return;
synchronized (list) {
for (PacketListener listener : list) {
for (PrioritizedListener element : list) {
try {
listener.onPacketSending(event);
element.getListener().onPacketSending(event);
} catch (Exception e) {
// Minecraft doesn't want your Exception.
logger.log(Level.SEVERE,
"Exception occured in onPacketReceiving() for " + PacketAdapter.getPluginName(listener), e);
"Exception occured in onPacketReceiving() for " +
PacketAdapter.getPluginName(element.getListener()), e);
}
}
}
}
/**
* An implicitly sorted array list that preserves insertion order and maintains duplicates.
*
* Note that only the {@link insertSorted} method will update the list correctly,
* @param <T> - type of the sorted list.
* A listener with an associated priority.
*/
private class SortedArrayList<T> implements Iterable<T> {
private Comparator<T> comparator;
private List<T> list = new ArrayList<T>();
private class PrioritizedListener implements Comparable<PrioritizedListener> {
private PacketListener listener;
private ListenerPriority priority;
public SortedArrayList(Comparator<T> comparator) {
this.comparator = comparator;
public PrioritizedListener(PacketListener listener, ListenerPriority priority) {
this.listener = listener;
this.priority = priority;
}
/**
* Inserts the given element in the proper location.
* @param value - element to insert.
*/
public void insertSorted(T value) {
list.add(value);
for (int i = list.size() - 1; i > 0 && comparator.compare(value, list.get(i-1)) < 0; i--) {
T tmp = list.get(i);
list.set(i, list.get(i-1));
list.set(i-1, tmp);
}
}
/**
* Removes every instance of the given element.
* @param element - element to remove.
*/
public void removeAll(T element) {
for (Iterator<T> it = list.iterator(); it.hasNext(); ) {
if (Objects.equal(it.next(), element)) {
it.remove();
}
}
}
/**
* Retrieve the size of the list.
* @return Size of the list.
*/
public int size() {
return list.size();
}
@Override
public Iterator<T> iterator() {
return list.iterator();
public int compareTo(PrioritizedListener other) {
// This ensures that lower priority listeners are executed first
return Ints.compare(this.getPriority().getSlot(),
other.getPriority().getSlot());
}
public PacketListener getListener() {
return listener;
}
public ListenerPriority getPriority() {
return priority;
}
}
}

View File

@ -26,7 +26,6 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;
@ -50,11 +49,12 @@ import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.FuzzyReflection;
import com.google.common.base.Objects;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
public final class PacketFilterManager implements ProtocolManager {
private Set<PacketListener> packetListeners = new CopyOnWriteArraySet<PacketListener>();
// Create a concurrent set
private Set<PacketListener> packetListeners =
Collections.newSetFromMap(new ConcurrentHashMap<PacketListener, Boolean>());
// Player injection
private Map<DataInputStream, Player> connectionLookup = new ConcurrentHashMap<DataInputStream, Player>();
@ -111,37 +111,52 @@ public final class PacketFilterManager implements ProtocolManager {
public void addPacketListener(PacketListener listener) {
if (listener == null)
throw new IllegalArgumentException("listener cannot be NULL.");
ConnectionSide side = listener.getConnectionSide();
packetListeners.add(listener);
// Add listeners
if (side.isForServer())
sendingListeners.addListener(listener);
if (side.isForClient())
recievedListeners.addListener(listener);
// A listener can only be added once
if (packetListeners.contains(listener))
return;
// Inform our injected hooks
enablePacketFilters(side, listener.getPacketsID());
ListeningWhitelist sending = listener.getSendingWhitelist();
ListeningWhitelist receiving = listener.getReceivingWhitelist();
boolean hasSending = sending != null && sending.isEnabled();
boolean hasReceiving = receiving != null && receiving.isEnabled();
if (hasSending || hasReceiving) {
// Add listeners and hooks
if (hasSending) {
sendingListeners.addListener(listener, sending);
enablePacketFilters(ConnectionSide.SERVER_SIDE, sending.getWhitelist());
}
if (hasReceiving) {
recievedListeners.addListener(listener, receiving);
enablePacketFilters(ConnectionSide.CLIENT_SIDE, receiving.getWhitelist());
}
// Inform our injected hooks
packetListeners.add(listener);
}
}
@Override
public void removePacketListener(PacketListener listener) {
if (listener == null)
throw new IllegalArgumentException("listener cannot be NULL");
ConnectionSide side = listener.getConnectionSide();
List<Integer> sendingRemoved = null;
List<Integer> receivingRemoved = null;
ListeningWhitelist sending = listener.getSendingWhitelist();
ListeningWhitelist receiving = listener.getReceivingWhitelist();
// Remove from the overal list of listeners
packetListeners.remove(listener);
if (!packetListeners.remove(listener))
return;
// Add listeners
if (side.isForServer())
sendingRemoved = sendingListeners.removeListener(listener);
if (side.isForClient())
receivingRemoved = recievedListeners.removeListener(listener);
if (sending != null && sending.isEnabled())
sendingRemoved = sendingListeners.removeListener(listener, sending);
if (receiving != null && receiving.isEnabled())
receivingRemoved = recievedListeners.removeListener(listener, receiving);
// Remove hooks, if needed
if (sendingRemoved != null && sendingRemoved.size() > 0)
@ -154,9 +169,7 @@ public final class PacketFilterManager implements ProtocolManager {
public void removePacketListeners(Plugin plugin) {
// Iterate through every packet listener
for (Object element : packetListeners.toArray()) {
PacketListener listener = (PacketListener) element;
for (PacketListener listener : packetListeners) {
// Remove the listener
if (Objects.equal(listener.getPlugin(), plugin)) {
removePacketListener(listener);
@ -277,11 +290,13 @@ public final class PacketFilterManager implements ProtocolManager {
}
@Override
public Set<Integer> getPacketFilters() {
if (packetInjector != null)
return Sets.union(sendingFilters, packetInjector.getPacketHandlers());
else
return sendingFilters;
public Set<Integer> getSendingFilters() {
return ImmutableSet.copyOf(sendingFilters);
}
@Override
public Set<Integer> getReceivingFilters() {
return ImmutableSet.copyOf(packetInjector.getPacketHandlers());
}
/**
@ -362,7 +377,7 @@ public final class PacketFilterManager implements ProtocolManager {
// Find the register event method
Method registerEvent = FuzzyReflection.fromObject(manager).getMethodByParameters("registerEvent",
eventTypes, Listener.class, eventPriority, Plugin.class);
Enhancer ex = new Enhancer();
ex.setSuperclass(playerListener);
ex.setClassLoader(classLoader);

View File

@ -0,0 +1,73 @@
package com.comphenix.protocol.injector;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* An implicitly sorted array list that preserves insertion order and maintains duplicates.
*
* Note that only the {@link insertSorted} method will update the list correctly,
* @param <T> - type of the sorted list.
*/
class SortedCopyOnWriteArray<T> implements Iterable<T> {
// Prevent reordering
private volatile List<T> list;
public SortedCopyOnWriteArray() {
this(new ArrayList<T>());
}
public SortedCopyOnWriteArray(List<T> wrapped) {
this.list = wrapped;
}
/**
* Inserts the given element in the proper location.
* @param value - element to insert.
*/
@SuppressWarnings("unchecked")
public synchronized void insertSorted(T value) {
// We use NULL as a special marker, so we don't allow it
if (value == null)
throw new IllegalArgumentException("value cannot be NULL");
List<T> copy = new ArrayList<T>();
Comparable<T> compare = (Comparable<T>) value;
for (T element : list) {
// If the value is now greater than the current element, it should be placed right before it
if (value != null && compare.compareTo(element) < 0) {
copy.add(value);
value = null;
}
copy.add(element);
}
// Don't forget to add it
if (value != null)
copy.add(value);
list = copy;
}
public T get(int index) {
return list.get(index);
}
/**
* Retrieve the size of the list.
* @return Size of the list.
*/
public int size() {
return list.size();
}
/**
* Retrieves an iterator over the elements in the given list.
*/
public Iterator<T> iterator() {
return list.iterator();
}
}

View File

@ -0,0 +1,76 @@
package com.comphenix.protocol.injector;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import com.comphenix.protocol.events.ListenerPriority;
import com.google.common.primitives.Ints;
public class SortedCopyOnWriteArrayTest {
@Test
public void testInsertion() {
final int MAX_NUMBER = 50;
SortedCopyOnWriteArray<Integer> test = new SortedCopyOnWriteArray<Integer>();
// Generate some numbers
List<Integer> numbers = new ArrayList<Integer>();
for (int i = 0; i < MAX_NUMBER; i++) {
numbers.add(i);
}
// Random insertion to test it all
Collections.shuffle(numbers);
// O(n^2) of course, so don't use too large numbers
for (int i = 0; i < MAX_NUMBER; i++) {
test.insertSorted(numbers.get(i));
}
// Check that everything is correct
for (int i = 0; i < MAX_NUMBER; i++) {
assertEquals((Integer) i, test.get(i));
}
}
@Test
public void testOrder() {
PriorityStuff a = new PriorityStuff(ListenerPriority.HIGH, 1);
PriorityStuff b = new PriorityStuff(ListenerPriority.NORMAL, 2);
PriorityStuff c = new PriorityStuff(ListenerPriority.NORMAL, 3);
SortedCopyOnWriteArray<PriorityStuff> test = new SortedCopyOnWriteArray<PriorityStuff>();
test.insertSorted(a);
test.insertSorted(b);
test.insertSorted(c);
// Make sure the normal's are in the right order
assertEquals(2, test.get(0).id);
assertEquals(3, test.get(1).id);
}
private class PriorityStuff implements Comparable<PriorityStuff> {
public ListenerPriority priority;
public int id;
public PriorityStuff(ListenerPriority priority, int id) {
this.priority = priority;
this.id = id;
}
@Override
public int compareTo(PriorityStuff other) {
// This ensures that lower priority listeners are executed first
return Ints.compare(priority.getSlot(),
other.priority.getSlot());
}
}
}