2019-03-24 22:47:23 +01:00
From 70e2fb3da9e5ab207150021fb5f3787c2dd216e8 Mon Sep 17 00:00:00 2001
2016-03-03 21:26:03 +01:00
From: Techcable <Techcable@outlook.com>
Date: Thu, 3 Mar 2016 13:20:33 -0700
Subject: [PATCH] Use ASM for event executors.
2016-03-12 17:43:39 +01:00
Uses method handles for private or static methods.
2016-03-03 21:26:03 +01:00
diff --git a/pom.xml b/pom.xml
2019-03-20 01:28:15 +01:00
index 524231ecc..082f2f0c5 100644
2016-03-03 21:26:03 +01:00
--- a/pom.xml
+++ b/pom.xml
2019-03-20 01:28:15 +01:00
@@ -128,6 +128,17 @@
<version>7.1</version>
2016-03-03 21:26:03 +01:00
<scope>test</scope>
</dependency>
+ <!-- ASM -->
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
2018-05-23 05:07:25 +02:00
+ <artifactId>asm</artifactId>
2019-03-20 01:28:15 +01:00
+ <version>7.1</version>
2018-05-23 05:07:25 +02:00
+ </dependency>
+ <dependency>
+ <groupId>org.ow2.asm</groupId>
+ <artifactId>asm-commons</artifactId>
2019-03-20 01:28:15 +01:00
+ <version>7.1</version>
2016-03-03 21:26:03 +01:00
+ </dependency>
</dependencies>
2016-03-12 07:40:16 +01:00
2016-03-03 21:26:03 +01:00
<build>
diff --git a/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java b/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java
new file mode 100644
2019-03-24 22:47:23 +01:00
index 000000000..5b28e9b1d
2016-03-03 21:26:03 +01:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java
2019-03-24 22:47:23 +01:00
@@ -0,0 +1,42 @@
2016-03-03 21:26:03 +01:00
+package com.destroystokyo.paper.event.executor;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+
2019-03-24 22:47:23 +01:00
+import com.destroystokyo.paper.util.SneakyThrow;
2016-03-03 21:26:03 +01:00
+import org.bukkit.event.Event;
+import org.bukkit.event.EventException;
+import org.bukkit.event.Listener;
+import org.bukkit.plugin.EventExecutor;
2019-03-20 01:28:15 +01:00
+import org.jetbrains.annotations.NotNull;
2016-03-03 21:26:03 +01:00
+
+public class MethodHandleEventExecutor implements EventExecutor {
+ private final Class<? extends Event> eventClass;
+ private final MethodHandle handle;
+
2019-03-20 01:28:15 +01:00
+ public MethodHandleEventExecutor(@NotNull Class<? extends Event> eventClass, @NotNull MethodHandle handle) {
2016-03-03 21:26:03 +01:00
+ this.eventClass = eventClass;
+ this.handle = handle;
+ }
+
2019-03-20 01:28:15 +01:00
+ public MethodHandleEventExecutor(@NotNull Class<? extends Event> eventClass, @NotNull Method m) {
2016-03-03 21:26:03 +01:00
+ this.eventClass = eventClass;
+ try {
+ m.setAccessible(true);
+ this.handle = MethodHandles.lookup().unreflect(m);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError("Unable to set accessible", e);
+ }
+ }
+
+ @Override
2019-03-20 01:28:15 +01:00
+ public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException {
2016-03-03 21:26:03 +01:00
+ if (!eventClass.isInstance(event)) return;
+ try {
+ handle.invoke(listener, event);
+ } catch (Throwable t) {
2019-03-24 22:47:23 +01:00
+ SneakyThrow.sneaky(t);
2016-03-03 21:26:03 +01:00
+ }
+ }
+}
2016-03-12 17:43:39 +01:00
diff --git a/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java b/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java
new file mode 100644
2019-03-20 01:28:15 +01:00
index 000000000..c83672427
2016-03-12 17:43:39 +01:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java
2019-03-20 01:28:15 +01:00
@@ -0,0 +1,43 @@
2016-03-12 17:43:39 +01:00
+package com.destroystokyo.paper.event.executor;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
2019-02-23 18:17:41 +01:00
+import com.destroystokyo.paper.util.SneakyThrow;
2016-03-12 17:43:39 +01:00
+import com.google.common.base.Preconditions;
+
2019-02-23 18:17:41 +01:00
+import org.bukkit.Bukkit;
2016-03-12 17:43:39 +01:00
+import org.bukkit.event.Event;
+import org.bukkit.event.EventException;
+import org.bukkit.event.Listener;
+import org.bukkit.plugin.EventExecutor;
2019-03-20 01:28:15 +01:00
+import org.jetbrains.annotations.NotNull;
2016-03-12 17:43:39 +01:00
+
+public class StaticMethodHandleEventExecutor implements EventExecutor {
+ private final Class<? extends Event> eventClass;
+ private final MethodHandle handle;
+
2019-03-20 01:28:15 +01:00
+ public StaticMethodHandleEventExecutor(@NotNull Class<? extends Event> eventClass, @NotNull Method m) {
2016-03-12 17:43:39 +01:00
+ Preconditions.checkArgument(Modifier.isStatic(m.getModifiers()), "Not a static method: %s", m);
2019-03-20 01:28:15 +01:00
+ Preconditions.checkArgument(eventClass != null, "eventClass is null");
2016-03-12 17:43:39 +01:00
+ this.eventClass = eventClass;
+ try {
+ m.setAccessible(true);
+ this.handle = MethodHandles.lookup().unreflect(m);
+ } catch (IllegalAccessException e) {
+ throw new AssertionError("Unable to set accessible", e);
+ }
+ }
+
+ @Override
2019-03-20 01:28:15 +01:00
+ public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException {
2016-03-12 17:43:39 +01:00
+ if (!eventClass.isInstance(event)) return;
+ try {
+ handle.invoke(event);
2019-02-23 18:17:41 +01:00
+ } catch (Throwable throwable) {
+ SneakyThrow.sneaky(throwable);
2016-03-12 17:43:39 +01:00
+ }
+ }
+}
2016-03-03 21:26:03 +01:00
diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java
new file mode 100644
2019-03-20 01:28:15 +01:00
index 000000000..b6e7d8ee8
2016-03-03 21:26:03 +01:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java
2019-03-20 01:28:15 +01:00
@@ -0,0 +1,47 @@
2016-03-03 21:26:03 +01:00
+package com.destroystokyo.paper.event.executor.asm;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.bukkit.plugin.EventExecutor;
2019-03-20 01:28:15 +01:00
+import org.jetbrains.annotations.NotNull;
2016-03-03 21:26:03 +01:00
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Type;
+import org.objectweb.asm.commons.GeneratorAdapter;
+
+import static org.objectweb.asm.Opcodes.*;
+
+public class ASMEventExecutorGenerator {
2019-03-20 01:28:15 +01:00
+ @NotNull
+ public static byte[] generateEventExecutor(@NotNull Method m, @NotNull String name) {
2016-03-03 21:26:03 +01:00
+ ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
+ writer.visit(V1_8, ACC_PUBLIC, name.replace('.', '/'), null, Type.getInternalName(Object.class), new String[] {Type.getInternalName(EventExecutor.class)});
+ // Generate constructor
+ GeneratorAdapter methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null), ACC_PUBLIC, "<init>", "()V");
+ methodGenerator.loadThis();
+ methodGenerator.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "<init>", "()V", false); // Invoke the super class (Object) constructor
+ methodGenerator.returnValue();
+ methodGenerator.endMethod();
+ // Generate the execute method
+ methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "execute", "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Event;)V", null, null), ACC_PUBLIC, "execute", "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Listener;)V");;
+ methodGenerator.loadArg(0);
+ methodGenerator.checkCast(Type.getType(m.getDeclaringClass()));
+ methodGenerator.loadArg(1);
+ methodGenerator.checkCast(Type.getType(m.getParameterTypes()[0]));
2017-02-14 12:04:20 +01:00
+ methodGenerator.visitMethodInsn(m.getDeclaringClass().isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL, Type.getInternalName(m.getDeclaringClass()), m.getName(), Type.getMethodDescriptor(m), m.getDeclaringClass().isInterface());
2016-03-03 21:26:03 +01:00
+ if (m.getReturnType() != void.class) {
+ methodGenerator.pop();
+ }
+ methodGenerator.returnValue();
+ methodGenerator.endMethod();
+ writer.visitEnd();
+ return writer.toByteArray();
+ }
+
+ public static AtomicInteger NEXT_ID = new AtomicInteger(1);
2019-03-20 01:28:15 +01:00
+ @NotNull
2016-03-03 21:26:03 +01:00
+ public static String generateName() {
+ int id = NEXT_ID.getAndIncrement();
+ return "com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor" + id;
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java
new file mode 100644
2019-03-20 01:28:15 +01:00
index 000000000..beed9e6e0
2016-03-03 21:26:03 +01:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java
2019-03-20 01:28:15 +01:00
@@ -0,0 +1,35 @@
2016-03-03 21:26:03 +01:00
+package com.destroystokyo.paper.event.executor.asm;
+
+import com.destroystokyo.paper.utils.UnsafeUtils;
2019-03-20 01:28:15 +01:00
+import org.jetbrains.annotations.NotNull;
2016-03-03 21:26:03 +01:00
+
+public interface ClassDefiner {
+
+ /**
+ * Returns if the defined classes can bypass access checks
+ *
+ * @return if classes bypass access checks
+ */
+ public default boolean isBypassAccessChecks() {
+ return false;
+ }
+
+ /**
+ * Define a class
+ *
+ * @param parentLoader the parent classloader
+ * @param name the name of the class
+ * @param data the class data to load
+ * @return the defined class
+ * @throws ClassFormatError if the class data is invalid
+ * @throws NullPointerException if any of the arguments are null
+ */
2019-03-20 01:28:15 +01:00
+ @NotNull
+ public Class<?> defineClass(@NotNull ClassLoader parentLoader, @NotNull String name, @NotNull byte[] data);
2016-03-03 21:26:03 +01:00
+
2019-03-20 01:28:15 +01:00
+ @NotNull
2016-03-03 21:26:03 +01:00
+ public static ClassDefiner getInstance() {
+ return SafeClassDefiner.INSTANCE;
+ }
+
+}
diff --git a/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java b/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java
new file mode 100644
2019-03-20 01:28:15 +01:00
index 000000000..ac99477e9
2016-03-03 21:26:03 +01:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java
2019-03-20 01:28:15 +01:00
@@ -0,0 +1,66 @@
2016-03-03 21:26:03 +01:00
+package com.destroystokyo.paper.event.executor.asm;
+
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import com.google.common.base.Preconditions;
+
2017-09-06 22:18:36 +02:00
+import com.google.common.collect.MapMaker;
2019-03-20 01:28:15 +01:00
+import org.jetbrains.annotations.NotNull;
2016-03-03 21:26:03 +01:00
+import org.objectweb.asm.Type;
+
+public class SafeClassDefiner implements ClassDefiner {
+ /* default */ static final SafeClassDefiner INSTANCE = new SafeClassDefiner();
+
+ private SafeClassDefiner() {}
+
2017-09-06 22:18:36 +02:00
+ private final ConcurrentMap<ClassLoader, GeneratedClassLoader> loaders = new MapMaker().weakKeys().makeMap();
2016-03-03 21:26:03 +01:00
+
2019-03-20 01:28:15 +01:00
+ @NotNull
2016-03-03 21:26:03 +01:00
+ @Override
2019-03-20 01:28:15 +01:00
+ public Class<?> defineClass(@NotNull ClassLoader parentLoader, @NotNull String name, @NotNull byte[] data) {
2016-03-03 21:26:03 +01:00
+ GeneratedClassLoader loader = loaders.computeIfAbsent(parentLoader, GeneratedClassLoader::new);
+ synchronized (loader.getClassLoadingLock(name)) {
+ Preconditions.checkState(!loader.hasClass(name), "%s already defined", name);
+ Class<?> c = loader.define(name, data);
+ assert c.getName().equals(name);
+ return c;
+ }
+ }
+
+ private static class GeneratedClassLoader extends ClassLoader {
+ static {
+ ClassLoader.registerAsParallelCapable();
+ }
+
2019-03-20 01:28:15 +01:00
+ protected GeneratedClassLoader(@NotNull ClassLoader parent) {
2016-03-03 21:26:03 +01:00
+ super(parent);
+ }
+
2019-03-20 01:28:15 +01:00
+ private Class<?> define(@NotNull String name, byte[] data) {
2016-03-03 21:26:03 +01:00
+ synchronized (getClassLoadingLock(name)) {
+ assert !hasClass(name);
+ Class<?> c = defineClass(name, data, 0, data.length);
+ resolveClass(c);
+ return c;
+ }
+ }
+
+ @Override
2019-03-20 01:28:15 +01:00
+ @NotNull
+ public Object getClassLoadingLock(@NotNull String name) {
2016-03-03 21:26:03 +01:00
+ return super.getClassLoadingLock(name);
+ }
+
2019-03-20 01:28:15 +01:00
+ public boolean hasClass(@NotNull String name) {
2016-03-03 21:26:03 +01:00
+ synchronized (getClassLoadingLock(name)) {
+ try {
+ Class.forName(name);
+ return true;
+ } catch (ClassNotFoundException e) {
+ return false;
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/utils/UnsafeUtils.java b/src/main/java/com/destroystokyo/paper/utils/UnsafeUtils.java
new file mode 100644
2019-03-20 01:28:15 +01:00
index 000000000..72e48e8ef
2016-03-03 21:26:03 +01:00
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/utils/UnsafeUtils.java
2019-03-20 01:28:15 +01:00
@@ -0,0 +1,35 @@
2016-03-03 21:26:03 +01:00
+package com.destroystokyo.paper.utils;
+
2019-03-20 01:28:15 +01:00
+import org.jetbrains.annotations.Nullable;
2016-03-03 21:26:03 +01:00
+import sun.misc.Unsafe;
+
+import java.lang.reflect.Field;
+
+public class UnsafeUtils {
+ private UnsafeUtils() {}
+
+ private static final Unsafe UNSAFE;
+ static {
+ Unsafe unsafe;
+ try {
+ Class c = Class.forName("sun.misc.Unsafe");
+ Field f = c.getDeclaredField("theUnsafe");
+ f.setAccessible(true);
+ unsafe = (Unsafe) f.get(null);
+ } catch (ClassNotFoundException | NoSuchFieldException | SecurityException e) {
+ unsafe = null;
+ } catch (IllegalAccessException e) {
+ throw new AssertionError(e);
+ }
+ UNSAFE = unsafe;
+ }
+
+ public static boolean isUnsafeSupported() {
+ return UNSAFE != null;
+ }
+
2019-03-20 01:28:15 +01:00
+ @Nullable
2016-03-03 21:26:03 +01:00
+ public static Unsafe getUnsafe() {
+ return UNSAFE;
+ }
+}
diff --git a/src/main/java/org/bukkit/plugin/EventExecutor.java b/src/main/java/org/bukkit/plugin/EventExecutor.java
2019-03-20 01:28:15 +01:00
index a850f0780..9026e108c 100644
2016-03-03 21:26:03 +01:00
--- a/src/main/java/org/bukkit/plugin/EventExecutor.java
+++ b/src/main/java/org/bukkit/plugin/EventExecutor.java
2019-03-20 01:28:15 +01:00
@@ -5,9 +5,75 @@ import org.bukkit.event.EventException;
2016-03-03 21:26:03 +01:00
import org.bukkit.event.Listener;
2019-03-20 01:28:15 +01:00
import org.jetbrains.annotations.NotNull;
2016-03-12 07:40:16 +01:00
2016-03-03 21:26:03 +01:00
+// Paper start
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
2017-09-06 22:18:36 +02:00
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.function.Function;
2016-03-03 21:26:03 +01:00
+
+import com.destroystokyo.paper.event.executor.MethodHandleEventExecutor;
2016-03-12 17:43:39 +01:00
+import com.destroystokyo.paper.event.executor.StaticMethodHandleEventExecutor;
2016-03-03 21:26:03 +01:00
+import com.destroystokyo.paper.event.executor.asm.ASMEventExecutorGenerator;
+import com.destroystokyo.paper.event.executor.asm.ClassDefiner;
+import com.google.common.base.Preconditions;
+// Paper end
+
/**
* Interface which defines the class for event call backs to plugins
*/
public interface EventExecutor {
2019-03-20 01:28:15 +01:00
public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException;
2016-03-03 21:26:03 +01:00
+
+ // Paper start
2017-09-06 22:18:36 +02:00
+ ConcurrentMap<Method, Class<? extends EventExecutor>> eventExecutorMap = new ConcurrentHashMap<Method, Class<? extends EventExecutor>>() {
2019-03-20 01:28:15 +01:00
+ @NotNull
2017-09-06 22:18:36 +02:00
+ @Override
2019-03-20 01:28:15 +01:00
+ public Class<? extends EventExecutor> computeIfAbsent(@NotNull Method key, @NotNull Function<? super Method, ? extends Class<? extends EventExecutor>> mappingFunction) {
2017-09-06 22:18:36 +02:00
+ Class<? extends EventExecutor> executorClass = get(key);
+ if (executorClass != null)
+ return executorClass;
+
+ //noinspection SynchronizationOnLocalVariableOrMethodParameter
+ synchronized (key) {
+ executorClass = get(key);
+ if (executorClass != null)
+ return executorClass;
+
+ return super.computeIfAbsent(key, mappingFunction);
+ }
+ }
+ };
+
2019-03-20 01:28:15 +01:00
+ @NotNull
+ public static EventExecutor create(@NotNull Method m, @NotNull Class<? extends Event> eventClass) {
2016-03-03 21:26:03 +01:00
+ Preconditions.checkNotNull(m, "Null method");
+ Preconditions.checkArgument(m.getParameterCount() != 0, "Incorrect number of arguments %s", m.getParameterCount());
+ Preconditions.checkArgument(m.getParameterTypes()[0] == eventClass, "First parameter %s doesn't match event class %s", m.getParameterTypes()[0], eventClass);
+ ClassDefiner definer = ClassDefiner.getInstance();
2016-03-12 17:43:39 +01:00
+ if (Modifier.isStatic(m.getModifiers())) {
+ return new StaticMethodHandleEventExecutor(eventClass, m);
2017-09-06 22:18:36 +02:00
+ } else if (definer.isBypassAccessChecks() || Modifier.isPublic(m.getDeclaringClass().getModifiers()) && Modifier.isPublic(m.getModifiers())) {
+ // get the existing generated EventExecutor class for the Method or generate one
+ Class<? extends EventExecutor> executorClass = eventExecutorMap.computeIfAbsent(m, (__) -> {
+ String name = ASMEventExecutorGenerator.generateName();
+ byte[] classData = ASMEventExecutorGenerator.generateEventExecutor(m, name);
+ return definer.defineClass(m.getDeclaringClass().getClassLoader(), name, classData).asSubclass(EventExecutor.class);
+ });
+
2016-03-03 21:26:03 +01:00
+ try {
2017-09-06 22:18:36 +02:00
+ EventExecutor asmExecutor = executorClass.newInstance();
2016-03-03 21:26:03 +01:00
+ // Define a wrapper to conform to bukkit stupidity (passing in events that don't match and wrapper exception)
2019-02-23 18:17:41 +01:00
+ return (listener, event) -> {
+ if (!eventClass.isInstance(event)) return;
+ asmExecutor.execute(listener, event);
2016-03-03 21:26:03 +01:00
+ };
+ } catch (InstantiationException | IllegalAccessException e) {
+ throw new AssertionError("Unable to initialize generated event executor", e);
+ }
+ } else {
+ return new MethodHandleEventExecutor(eventClass, m);
+ }
+ }
+ // Paper end
}
diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
2019-03-20 01:28:15 +01:00
index ac0702dfb..b9bf2124b 100644
2016-03-03 21:26:03 +01:00
--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
+++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java
2019-03-20 01:28:15 +01:00
@@ -298,20 +298,7 @@ public final class JavaPluginLoader implements PluginLoader {
2016-03-03 21:26:03 +01:00
}
}
2016-03-12 07:40:16 +01:00
2019-03-20 01:28:15 +01:00
- EventExecutor executor = new co.aikar.timings.TimedEventExecutor(new EventExecutor() { // Paper
- public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { // Paper
2016-03-03 21:26:03 +01:00
- try {
- if (!eventClass.isAssignableFrom(event.getClass())) {
- return;
- }
- method.invoke(listener, event);
- } catch (InvocationTargetException ex) {
- throw new EventException(ex.getCause());
- } catch (Throwable t) {
- throw new EventException(t);
- }
- }
2019-03-20 01:28:15 +01:00
- }, plugin, method, eventClass); // Paper
+ EventExecutor executor = new co.aikar.timings.TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass); // Paper // Paper (Yes.) - Use factory method `EventExecutor.create()`
2016-03-03 21:26:03 +01:00
if (false) { // Spigot - RL handles useTimings check now
eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled()));
} else {
2016-03-12 07:40:16 +01:00
--
2019-03-20 01:28:15 +01:00
2.21.0
2016-03-12 07:40:16 +01:00