From 287eb52fa459d9b348b016f167a72d8fffa34574 Mon Sep 17 00:00:00 2001 From: Hannes Greule Date: Sun, 29 Dec 2024 00:11:09 +0100 Subject: [PATCH] Use hidden classes for event executors (#11848) Static final MethodHandles perform similar to direct calls. Additionally, hidden classes simplify logic around ClassLoaders as they can be defined weakly coupled to their defining class loader. All variants of methods (static, private, non-void) can be covered by this mechanism. --- paper-api/build.gradle.kts | 3 - .../executor/MethodHandleEventExecutor.java | 54 -------------- .../StaticMethodHandleEventExecutor.java | 51 ------------- .../asm/ASMEventExecutorGenerator.java | 59 --------------- .../event/executor/asm/ClassDefiner.java | 35 --------- .../event/executor/asm/SafeClassDefiner.java | 66 ----------------- .../event/executor/EventExecutorFactory.java | 74 +++++++++++++++++++ .../MethodHandleEventExecutorTemplate.java | 56 ++++++++++++++ .../java/org/bukkit/plugin/EventExecutor.java | 66 ++--------------- .../craftbukkit/entity/CraftHumanEntity.java | 2 +- 10 files changed, 139 insertions(+), 327 deletions(-) delete mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java delete mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java delete mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java delete mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java delete mode 100644 paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java create mode 100644 paper-api/src/main/java/io/papermc/paper/event/executor/EventExecutorFactory.java create mode 100644 paper-api/src/main/java/io/papermc/paper/event/executor/MethodHandleEventExecutorTemplate.java diff --git a/paper-api/build.gradle.kts b/paper-api/build.gradle.kts index 3ea140477c..d3bdc0b51b 100644 --- a/paper-api/build.gradle.kts +++ b/paper-api/build.gradle.kts @@ -68,9 +68,6 @@ dependencies { apiAndDocs("net.kyori:adventure-text-serializer-plain") apiAndDocs("net.kyori:adventure-text-logger-slf4j") - implementation("org.ow2.asm:asm:9.7.1") - implementation("org.ow2.asm:asm-commons:9.7.1") - api("org.apache.maven:maven-resolver-provider:3.9.6") // make API dependency for Paper Plugins compileOnly("org.apache.maven.resolver:maven-resolver-connector-basic:1.9.18") compileOnly("org.apache.maven.resolver:maven-resolver-transport-http:1.9.18") diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java deleted file mode 100644 index 2a169d2f6f..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/MethodHandleEventExecutor.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.destroystokyo.paper.event.executor; - -import com.destroystokyo.paper.util.SneakyThrow; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.reflect.Method; -import org.bukkit.event.Event; -import org.bukkit.event.EventException; -import org.bukkit.event.Listener; -import org.bukkit.plugin.EventExecutor; -import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -@ApiStatus.Internal -@NullMarked -public class MethodHandleEventExecutor implements EventExecutor { - - private final Class eventClass; - private final MethodHandle handle; - private final @Nullable Method method; - - public MethodHandleEventExecutor(final Class eventClass, final MethodHandle handle) { - this.eventClass = eventClass; - this.handle = handle; - this.method = null; - } - - public MethodHandleEventExecutor(final Class eventClass, final Method m) { - this.eventClass = eventClass; - try { - m.setAccessible(true); - this.handle = MethodHandles.lookup().unreflect(m); - } catch (final IllegalAccessException e) { - throw new AssertionError("Unable to set accessible", e); - } - this.method = m; - } - - @Override - public void execute(final Listener listener, final Event event) throws EventException { - if (!this.eventClass.isInstance(event)) return; - try { - this.handle.invoke(listener, event); - } catch (final Throwable t) { - SneakyThrow.sneaky(t); - } - } - - @Override - public String toString() { - return "MethodHandleEventExecutor['" + this.method + "']"; - } -} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java deleted file mode 100644 index e98962b6c6..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/StaticMethodHandleEventExecutor.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.destroystokyo.paper.event.executor; - -import com.destroystokyo.paper.util.SneakyThrow; -import com.google.common.base.Preconditions; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import org.bukkit.event.Event; -import org.bukkit.event.EventException; -import org.bukkit.event.Listener; -import org.bukkit.plugin.EventExecutor; -import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.NullMarked; - -@ApiStatus.Internal -@NullMarked -public class StaticMethodHandleEventExecutor implements EventExecutor { - - private final Class eventClass; - private final MethodHandle handle; - private final Method method; - - public StaticMethodHandleEventExecutor(final Class eventClass, final Method m) { - Preconditions.checkArgument(Modifier.isStatic(m.getModifiers()), "Not a static method: %s", m); - Preconditions.checkArgument(eventClass != null, "eventClass is null"); - this.eventClass = eventClass; - try { - m.setAccessible(true); - this.handle = MethodHandles.lookup().unreflect(m); - } catch (final IllegalAccessException e) { - throw new AssertionError("Unable to set accessible", e); - } - this.method = m; - } - - @Override - public void execute(final Listener listener, final Event event) throws EventException { - if (!this.eventClass.isInstance(event)) return; - try { - this.handle.invoke(event); - } catch (final Throwable throwable) { - SneakyThrow.sneaky(throwable); - } - } - - @Override - public String toString() { - return "StaticMethodHandleEventExecutor['" + this.method + "']"; - } -} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java deleted file mode 100644 index abfcb6e838..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ASMEventExecutorGenerator.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.destroystokyo.paper.event.executor.asm; - -import java.lang.reflect.Method; -import java.util.concurrent.atomic.AtomicInteger; -import org.bukkit.plugin.EventExecutor; -import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.NullMarked; -import org.objectweb.asm.ClassWriter; -import org.objectweb.asm.Type; -import org.objectweb.asm.commons.GeneratorAdapter; - -import static org.objectweb.asm.Opcodes.ACC_PUBLIC; -import static org.objectweb.asm.Opcodes.INVOKEINTERFACE; -import static org.objectweb.asm.Opcodes.INVOKESPECIAL; -import static org.objectweb.asm.Opcodes.INVOKEVIRTUAL; -import static org.objectweb.asm.Opcodes.V1_8; - -@ApiStatus.Internal -@NullMarked -public final class ASMEventExecutorGenerator { - - private static final String EXECUTE_DESCRIPTOR = "(Lorg/bukkit/event/Listener;Lorg/bukkit/event/Event;)V"; - - public static byte[] generateEventExecutor(final Method m, final String name) { - final 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, "", "()V", null, null), ACC_PUBLIC, "", "()V"); - methodGenerator.loadThis(); - methodGenerator.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(Object.class), "", "()V", false); // Invoke the super class (Object) constructor - methodGenerator.returnValue(); - methodGenerator.endMethod(); - // Generate the execute method - methodGenerator = new GeneratorAdapter(writer.visitMethod(ACC_PUBLIC, "execute", EXECUTE_DESCRIPTOR, null, null), ACC_PUBLIC, "execute", EXECUTE_DESCRIPTOR); - methodGenerator.loadArg(0); - methodGenerator.checkCast(Type.getType(m.getDeclaringClass())); - methodGenerator.loadArg(1); - methodGenerator.checkCast(Type.getType(m.getParameterTypes()[0])); - methodGenerator.visitMethodInsn(m.getDeclaringClass().isInterface() ? INVOKEINTERFACE : INVOKEVIRTUAL, Type.getInternalName(m.getDeclaringClass()), m.getName(), Type.getMethodDescriptor(m), m.getDeclaringClass().isInterface()); - // The only purpose of this switch statement is to generate the correct pop instruction, should the event handler method return something other than void. - // Non-void event handlers will be unsupported in a future release. - switch (Type.getType(m.getReturnType()).getSize()) { - // case 0 is omitted because the only type that has size 0 is void - no pop instruction needed. - case 1 -> methodGenerator.pop(); // handles reference types and most primitives - case 2 -> methodGenerator.pop2(); // handles long and double - } - methodGenerator.returnValue(); - methodGenerator.endMethod(); - writer.visitEnd(); - return writer.toByteArray(); - } - - public static AtomicInteger NEXT_ID = new AtomicInteger(1); - - public static String generateName() { - final int id = NEXT_ID.getAndIncrement(); - return "com.destroystokyo.paper.event.executor.asm.generated.GeneratedEventExecutor" + id; - } -} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java deleted file mode 100644 index 581561fbd3..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/ClassDefiner.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.destroystokyo.paper.event.executor.asm; - -import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.NullMarked; - -@ApiStatus.Internal -@NullMarked -public interface ClassDefiner { - - /** - * Returns if the defined classes can bypass access checks - * - * @return if classes bypass access checks - */ - 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 - */ - Class defineClass(ClassLoader parentLoader, String name, byte[] data); - - static ClassDefiner getInstance() { - return SafeClassDefiner.INSTANCE; - } - -} diff --git a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java b/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java deleted file mode 100644 index 48bcc72293..0000000000 --- a/paper-api/src/main/java/com/destroystokyo/paper/event/executor/asm/SafeClassDefiner.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.destroystokyo.paper.event.executor.asm; - -import com.google.common.base.Preconditions; -import com.google.common.collect.MapMaker; -import java.util.concurrent.ConcurrentMap; -import org.jetbrains.annotations.ApiStatus; -import org.jspecify.annotations.NullMarked; - -@ApiStatus.Internal -@NullMarked -public class SafeClassDefiner implements ClassDefiner { - - /* default */ static final SafeClassDefiner INSTANCE = new SafeClassDefiner(); - - private SafeClassDefiner() { - } - - private final ConcurrentMap loaders = new MapMaker().weakKeys().makeMap(); - - @Override - public Class defineClass(final ClassLoader parentLoader, final String name, final byte[] data) { - final GeneratedClassLoader loader = this.loaders.computeIfAbsent(parentLoader, GeneratedClassLoader::new); - synchronized (loader.getClassLoadingLock(name)) { - Preconditions.checkState(!loader.hasClass(name), "%s already defined", name); - final Class c = loader.define(name, data); - assert c.getName().equals(name); - return c; - } - } - - private static class GeneratedClassLoader extends ClassLoader { - - static { - ClassLoader.registerAsParallelCapable(); - } - - protected GeneratedClassLoader(final ClassLoader parent) { - super(parent); - } - - private Class define(final String name, final byte[] data) { - synchronized (this.getClassLoadingLock(name)) { - assert !this.hasClass(name); - final Class c = this.defineClass(name, data, 0, data.length); - this.resolveClass(c); - return c; - } - } - - @Override - public Object getClassLoadingLock(final String name) { - return super.getClassLoadingLock(name); - } - - public boolean hasClass(final String name) { - synchronized (this.getClassLoadingLock(name)) { - try { - Class.forName(name); - return true; - } catch (final ClassNotFoundException e) { - return false; - } - } - } - } -} diff --git a/paper-api/src/main/java/io/papermc/paper/event/executor/EventExecutorFactory.java b/paper-api/src/main/java/io/papermc/paper/event/executor/EventExecutorFactory.java new file mode 100644 index 0000000000..2e3e21c3e9 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/executor/EventExecutorFactory.java @@ -0,0 +1,74 @@ +package io.papermc.paper.event.executor; + +import org.bukkit.event.Event; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; +import java.io.IOException; +import java.io.InputStream; +import java.lang.constant.ConstantDescs; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Objects; + +@ApiStatus.Internal +@NullMarked +public final class EventExecutorFactory { + private static final byte[] TEMPLATE_CLASS_BYTES; + + static { + try (final InputStream is = EventExecutorFactory.class.getResourceAsStream("MethodHandleEventExecutorTemplate.class")) { + TEMPLATE_CLASS_BYTES = Objects.requireNonNull(is, "template class is missing").readAllBytes(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + private EventExecutorFactory() { + + } + + /** + * {@return an {@link EventExecutor} implemented by a hidden class calling a method handle} + * + * @param method the method to be invoked by the created event executor + * @param eventClass the class of the event to handle + */ + public static EventExecutor create(final Method method, final Class eventClass) { + final List classData = List.of(method, eventClass); + try { + final MethodHandles.Lookup newClass = MethodHandles.lookup().defineHiddenClassWithClassData(TEMPLATE_CLASS_BYTES, classData, true); + return newClass.lookupClass().asSubclass(EventExecutor.class).getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + + record ClassData(Method method, MethodHandle methodHandle, Class eventClass) { + + } + + /** + * Extracts the class data and creates an adjusted MethodHandle directly usable by the lookup class. + * The logic is kept here to minimize memory usage per created class. + */ + static ClassData classData(final MethodHandles.Lookup lookup) { + try { + final Method method = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Method.class, 0); + MethodHandle mh = lookup.unreflect(method); + if (Modifier.isStatic(method.getModifiers())) { + mh = MethodHandles.dropArguments(mh, 0, Listener.class); + } + mh = mh.asType(MethodType.methodType(void.class, Listener.class, Event.class)); + final Class eventClass = MethodHandles.classDataAt(lookup, ConstantDescs.DEFAULT_NAME, Class.class, 1); + return new ClassData(method, mh, eventClass.asSubclass(Event.class)); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } +} diff --git a/paper-api/src/main/java/io/papermc/paper/event/executor/MethodHandleEventExecutorTemplate.java b/paper-api/src/main/java/io/papermc/paper/event/executor/MethodHandleEventExecutorTemplate.java new file mode 100644 index 0000000000..b28fcfed96 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/event/executor/MethodHandleEventExecutorTemplate.java @@ -0,0 +1,56 @@ +package io.papermc.paper.event.executor; + +import com.destroystokyo.paper.util.SneakyThrow; +import org.bukkit.event.Event; +import org.bukkit.event.EventException; +import org.bukkit.event.Listener; +import org.bukkit.plugin.EventExecutor; +import org.jetbrains.annotations.ApiStatus; +import org.jspecify.annotations.NullMarked; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Method; + +/** + * This class is designed to be used as hidden class template. + * Initializing the class directly will fail due to missing {@code classData}. + * Instead, {@link java.lang.invoke.MethodHandles.Lookup#defineHiddenClassWithClassData(byte[], Object, boolean, MethodHandles.Lookup.ClassOption...)} + * must be used, with the {@code classData} object being a list consisting of two elements: + *
    + *
  1. A {@link Method} representing the event handler method
  2. + *
  3. A {@link Class} representing the event type
  4. + *
+ * The method must take {@link Event} or a subtype of it as its single parameter. + * If the method is non-static, it also needs to reside in a class implementing {@link Listener}. + */ +@SuppressWarnings("unused") +@ApiStatus.Internal +@NullMarked +class MethodHandleEventExecutorTemplate implements EventExecutor { + private static final Method METHOD; + private static final MethodHandle HANDLE; + private static final Class EVENT_CLASS; + + static { + final MethodHandles.Lookup lookup = MethodHandles.lookup(); + final EventExecutorFactory.ClassData classData = EventExecutorFactory.classData(lookup); + METHOD = classData.method(); + HANDLE = classData.methodHandle(); + EVENT_CLASS = classData.eventClass(); + } + + @Override + public void execute(final Listener listener, final Event event) throws EventException { + if (!EVENT_CLASS.isInstance(event)) return; + try { + HANDLE.invokeExact(listener, event); + } catch (Throwable t) { + SneakyThrow.sneaky(t); + } + } + + @Override + public String toString() { + return "MethodHandleEventExecutorTemplate['" + METHOD + "']"; + } +} diff --git a/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java b/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java index 60e086be70..5aa1c55460 100644 --- a/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java +++ b/paper-api/src/main/java/org/bukkit/plugin/EventExecutor.java @@ -6,16 +6,10 @@ import org.bukkit.event.Listener; import org.jetbrains.annotations.NotNull; // Paper start +import io.papermc.paper.event.executor.EventExecutorFactory; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import java.util.function.Function; -import com.destroystokyo.paper.event.executor.MethodHandleEventExecutor; -import com.destroystokyo.paper.event.executor.StaticMethodHandleEventExecutor; -import com.destroystokyo.paper.event.executor.asm.ASMEventExecutorGenerator; -import com.destroystokyo.paper.event.executor.asm.ClassDefiner; import com.google.common.base.Preconditions; // Paper end @@ -26,69 +20,25 @@ public interface EventExecutor { public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException; // Paper start - ConcurrentMap> eventExecutorMap = new ConcurrentHashMap>() { - @NotNull - @Override - public Class computeIfAbsent(@NotNull Method key, @NotNull Function> mappingFunction) { - Class 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); - } - } - }; - @NotNull public static EventExecutor create(@NotNull Method m, @NotNull Class eventClass) { 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(); if (m.getReturnType() != Void.TYPE) { final org.bukkit.plugin.java.JavaPlugin plugin = org.bukkit.plugin.java.JavaPlugin.getProvidingPlugin(m.getDeclaringClass()); org.bukkit.Bukkit.getLogger().warning("@EventHandler method " + m.getDeclaringClass().getName() + (Modifier.isStatic(m.getModifiers()) ? '.' : '#') + m.getName() + " returns non-void type " + m.getReturnType().getName() + ". This is unsupported behavior and will no longer work in a future version of Paper." + " This should be reported to the developers of " + plugin.getPluginMeta().getDisplayName() + " (" + String.join(",", plugin.getPluginMeta().getAuthors()) + ')'); } - if (Modifier.isStatic(m.getModifiers())) { - return new StaticMethodHandleEventExecutor(eventClass, m); - } 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 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); - }); - - try { - EventExecutor asmExecutor = executorClass.newInstance(); - // Define a wrapper to conform to bukkit stupidity (passing in events that don't match and wrapper exception) - return new EventExecutor() { - @Override - public void execute(@NotNull Listener listener, @NotNull Event event) throws EventException { - if (!eventClass.isInstance(event)) return; - asmExecutor.execute(listener, event); - } - - @Override - @NotNull - public String toString() { - return "ASMEventExecutor['" + m + "']"; - } - }; - } catch (InstantiationException | IllegalAccessException e) { - throw new AssertionError("Unable to initialize generated event executor", e); - } - } else { - return new MethodHandleEventExecutor(eventClass, m); + if (!m.trySetAccessible()) { + final org.bukkit.plugin.java.JavaPlugin plugin = org.bukkit.plugin.java.JavaPlugin.getProvidingPlugin(m.getDeclaringClass()); + throw new AssertionError( + "@EventHandler method " + m.getDeclaringClass().getName() + (Modifier.isStatic(m.getModifiers()) ? '.' : '#') + m.getName() + " is not accessible." + + " This should be reported to the developers of " + plugin.getDescription().getName() + " (" + String.join(",", plugin.getDescription().getAuthors()) + ')' + ); } + return EventExecutorFactory.create(m, eventClass); } // Paper end } diff --git a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java index d142009c06..a1f42f860f 100644 --- a/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java +++ b/paper-server/src/main/java/org/bukkit/craftbukkit/entity/CraftHumanEntity.java @@ -834,7 +834,7 @@ public class CraftHumanEntity extends CraftLivingEntity implements HumanEntity { @Override @Nullable - public Item dropItem(final @Nullable ItemStack itemStack, final boolean throwRandomly, final @Nullable Consumer entityOperation) { + public Item dropItem(final ItemStack itemStack, final boolean throwRandomly, final @Nullable Consumer entityOperation) { // This method implementation differs from the previous dropItem implementations, as it does not source // its itemstack from the players inventory. As such, we cannot reuse #internalDropItemFromInventory. Preconditions.checkArgument(itemStack != null, "Cannot drop a null itemstack");