diff --git a/Spigot-Server-Patches/0001-POM-Changes.patch b/Spigot-Server-Patches/0001-POM-Changes.patch
index 4ee8b894b0..f157e49106 100644
--- a/Spigot-Server-Patches/0001-POM-Changes.patch
+++ b/Spigot-Server-Patches/0001-POM-Changes.patch
@@ -5,7 +5,7 @@ Subject: [PATCH] POM Changes
diff --git a/pom.xml b/pom.xml
-index 9fc92e347f24a0210a9190513e93cba3b6772557..6cc18aa360c20448fca59cf5490d69267c8e2521 100644
+index 9fc92e347f24a0210a9190513e93cba3b6772557..adf32845001fae7a870f588184c2efaf0ab41504 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,15 +1,14 @@
@@ -55,7 +55,20 @@ index 9fc92e347f24a0210a9190513e93cba3b6772557..6cc18aa360c20448fca59cf5490d6926
${project.version}
compile
-@@ -106,34 +111,22 @@
+@@ -50,6 +55,12 @@
+ 7.3.1
+ compile
+
++
++
++ co.aikar
++ cleaner
++ 1.0-SNAPSHOT
++
+
+
+ com.googlecode.json-simple
+@@ -106,34 +117,22 @@
@@ -101,7 +114,7 @@ index 9fc92e347f24a0210a9190513e93cba3b6772557..6cc18aa360c20448fca59cf5490d6926
-@@ -143,6 +136,7 @@
+@@ -143,6 +142,7 @@
maven-jar-plugin
3.2.0
@@ -109,7 +122,7 @@ index 9fc92e347f24a0210a9190513e93cba3b6772557..6cc18aa360c20448fca59cf5490d6926
false
-@@ -150,8 +144,9 @@
+@@ -150,8 +150,9 @@
org.bukkit.craftbukkit.Main
CraftBukkit
@@ -121,7 +134,7 @@ index 9fc92e347f24a0210a9190513e93cba3b6772557..6cc18aa360c20448fca59cf5490d6926
Bukkit
${api.version}
Bukkit Team
-@@ -190,6 +185,7 @@
+@@ -190,6 +191,7 @@
shade
@@ -129,7 +142,7 @@ index 9fc92e347f24a0210a9190513e93cba3b6772557..6cc18aa360c20448fca59cf5490d6926
${shadeSourcesJar}
-@@ -213,10 +209,11 @@
+@@ -213,10 +215,11 @@
jline
org.bukkit.craftbukkit.libs.jline
@@ -145,7 +158,7 @@ index 9fc92e347f24a0210a9190513e93cba3b6772557..6cc18aa360c20448fca59cf5490d6926
org.apache.commons.codec
org.bukkit.craftbukkit.libs.org.apache.commons.codec
-@@ -258,10 +255,6 @@
+@@ -258,10 +261,6 @@
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
diff --git a/Spigot-Server-Patches/0004-MC-Utils.patch b/Spigot-Server-Patches/0004-MC-Utils.patch
index 3242652166..6a4038d40d 100644
--- a/Spigot-Server-Patches/0004-MC-Utils.patch
+++ b/Spigot-Server-Patches/0004-MC-Utils.patch
@@ -2077,90 +2077,173 @@ index 0000000000000000000000000000000000000000..e51104e65a07b6ea7bbbcbb6afb066ef
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java
new file mode 100644
-index 0000000000000000000000000000000000000000..e272b512520486cf7d46fe4e1021ca148d4cf74f
+index 0000000000000000000000000000000000000000..9841212a60346870535e81b22851261e12380650
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java
@@ -0,0 +1,174 @@
+package com.destroystokyo.paper.util.pooled;
+
++import net.minecraft.server.MCUtil;
+import org.apache.commons.lang3.mutable.MutableInt;
++
+import java.util.ArrayDeque;
+import java.util.concurrent.ThreadLocalRandom;
++import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.locks.ReentrantLock;
++import java.util.function.Consumer;
+
++/**
++ * Object pooling with thread safe, low contention design. Pooled objects have no additional object overhead
++ * due to usage of ArrayDeque per insertion/removal unless a resizing is needed in the buckets.
++ * Supports up to bucket size (default 8) threads concurrently accessing if all buckets have a value.
++ * Releasing may conditionally have contention if multiple buckets have same current size, but randomization will be used.
++ *
++ * Original interface API by Spottedleaf
++ * Implementation by Aikar
++ * @license MIT
++ */
+public final class PooledObjects {
+
-+ public static final PooledObjects POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 200, -1);
++ /**
++ * Wrapper for an object that will be have a cleaner registered for it, and may be automatically returned to pool.
++ */
++ public class AutoReleased {
++ private final E object;
++ private final Runnable cleaner;
++
++ public AutoReleased(E object, Runnable cleaner) {
++ this.object = object;
++ this.cleaner = cleaner;
++ }
++
++ public final E getObject() {
++ return object;
++ }
++
++ public final Runnable getCleaner() {
++ return cleaner;
++ }
++ }
++
++ public static final PooledObjects POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 1024, 16);
+
+ private final PooledObjectHandler handler;
-+ private final int maxPoolSize;
-+ private final int expectingThreads;
++ private final int bucketCount;
++ private final int bucketSize;
++ private final ArrayDeque[] buckets;
++ private final ReentrantLock[] locks;
++ private final AtomicLong bucketIdCounter = new AtomicLong(0);
+
-+ private final IsolatedPool mainPool;
-+ // use these under contention
-+ private final IsolatedPool[] contendedPools;
-+
-+ public PooledObjects(final PooledObjectHandler handler, final int maxPoolSize, int expectingThreads) {
++ public PooledObjects(final PooledObjectHandler handler, int maxPoolSize) {
++ this(handler, maxPoolSize, 8);
++ }
++ public PooledObjects(final PooledObjectHandler handler, int maxPoolSize, int bucketCount) {
+ if (handler == null) {
+ throw new NullPointerException("Handler must not be null");
+ }
+ if (maxPoolSize <= 0) {
+ throw new IllegalArgumentException("Max pool size must be greater-than 0");
+ }
-+ if (expectingThreads <= 0) {
-+ expectingThreads = Runtime.getRuntime().availableProcessors();
++ if (bucketCount < 1) {
++ throw new IllegalArgumentException("Bucket count must be greater-than 0");
+ }
-+
++ int remainder = maxPoolSize % bucketCount;
++ if (remainder > 0) {
++ // Auto adjust up to the next bucket divisible size
++ maxPoolSize = maxPoolSize - remainder + bucketCount;
++ }
++ //noinspection unchecked
++ this.buckets = new ArrayDeque[bucketCount];
++ this.locks = new ReentrantLock[bucketCount];
++ this.bucketCount = bucketCount;
+ this.handler = handler;
-+ this.maxPoolSize = maxPoolSize;
-+ this.expectingThreads = expectingThreads;
-+ this.mainPool = new IsolatedPool<>(handler, maxPoolSize);
-+ final IsolatedPool[] contendedPools = new IsolatedPool[2 * expectingThreads];
++ this.bucketSize = maxPoolSize / bucketCount;
++ for (int i = 0; i < bucketCount; i++) {
++ this.buckets[i] = new ArrayDeque<>(bucketSize / 4);
++ this.locks[i] = new ReentrantLock();
++ }
++ }
+
-+ for (int i = 0; i < contendedPools.length; ++i) {
-+ contendedPools[i] = new IsolatedPool<>(handler, Math.max(1, maxPoolSize / 2));
++ public AutoReleased acquireCleaner(Object holder) {
++ return acquireCleaner(holder, this::release);
++ }
++
++ public AutoReleased acquireCleaner(Object holder, Consumer releaser) {
++ E resource = acquire();
++ Runnable cleaner = MCUtil.registerCleaner(holder, resource, releaser);
++ return new AutoReleased(resource, cleaner);
++ }
++
++
++ public long size() {
++ long size = 0;
++ for (int i = 0; i < bucketCount; i++) {
++ size += this.buckets[i].size();
+ }
+
-+ this.contendedPools = contendedPools;
++ return size;
+ }
-+
-+ // Taken from
-+ // https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/
-+ // https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/blob/master/2016/06/25/fastrange.c
-+ // Original license is public domain
-+ public static int fastRandomBounded(final long randomInteger, final long limit) {
-+ // randomInteger must be [0, pow(2, 32))
-+ // limit must be [0, pow(2, 32))
-+ return (int)((randomInteger * limit) >>> 32);
-+ }
-+
+ public E acquire() {
-+ E ret;
-+ PooledObjects.IsolatedPool pooled = this.mainPool;
-+ int lastIndex = -1;
-+ while ((ret = pooled.tryAcquireUncontended()) == null) {
-+ int index;
-+ while (lastIndex == (index = fastRandomBounded(ThreadLocalRandom.current().nextInt() & 0xFFFFFFFFL, this.contendedPools.length)));
-+ lastIndex = index;
-+ pooled = this.contendedPools[index];
++ for (int base = (int) (this.bucketIdCounter.getAndIncrement() % bucketCount), i = 0; i < bucketCount; i++ ) {
++ int bucketId = (base + i) % bucketCount;
++ if (this.buckets[bucketId].isEmpty()) continue;
++ // lock will alloc an object if blocked, so spinwait instead since lock duration is super fast
++ lockBucket(bucketId);
++ E value = this.buckets[bucketId].poll();
++ this.locks[bucketId].unlock();
++ if (value != null) {
++ this.handler.onAcquire(value);
++ return value;
++ }
+ }
++ return this.handler.createNew();
++ }
+
-+ return ret;
++ private void lockBucket(int bucketId) {
++ // lock will alloc an object if blocked, try to avoid unless 2 failures
++ ReentrantLock lock = this.locks[bucketId];
++ if (!lock.tryLock()) {
++ Thread.yield();
++ } else {
++ return;
++ }
++ if (!lock.tryLock()) {
++ Thread.yield();
++ lock.lock();
++ }
+ }
+
+ public void release(final E value) {
-+ PooledObjects.IsolatedPool pooled = this.mainPool;
-+ int lastIndex = -1;
-+ while (!pooled.tryReleaseUncontended(value)) {
-+ int index;
-+ while (lastIndex == (index = fastRandomBounded(ThreadLocalRandom.current().nextInt() & 0xFFFFFFFFL, this.contendedPools.length)));
-+ lastIndex = index;
-+ pooled = this.contendedPools[index];
-+ }
++ int attempts = 3; // cap on contention
++ do {
++ // find least filled bucket before locking
++ int smallestIdx = -1;
++ int smallest = Integer.MAX_VALUE;
++ for (int i = 0; i < bucketCount; i++ ) {
++ ArrayDeque bucket = this.buckets[i];
++ int size = bucket.size();
++ if (size < this.bucketSize && (smallestIdx == -1 || size < smallest || (size == smallest && ThreadLocalRandom.current().nextBoolean()))) {
++ smallestIdx = i;
++ smallest = size;
++ }
++ }
++ if (smallestIdx == -1) return; // Can not find a bucket to fill
++
++ lockBucket(smallestIdx);
++ ArrayDeque bucket = this.buckets[smallestIdx];
++ if (bucket.size() < this.bucketSize) {
++ this.handler.onRelease(value);
++ bucket.push(value);
++ this.locks[smallestIdx].unlock();
++ return;
++ } else {
++ this.locks[smallestIdx].unlock();
++ }
++ } while (attempts-- > 0);
+ }
+
+ /** This object is restricted from interacting with any pool */
-+ public static interface PooledObjectHandler {
++ public interface PooledObjectHandler {
+
+ /**
+ * Must return a non-null object
@@ -2171,89 +2254,6 @@ index 0000000000000000000000000000000000000000..e272b512520486cf7d46fe4e1021ca14
+
+ default void onRelease(final E value) {}
+ }
-+
-+ protected static class IsolatedPool {
-+
-+ protected final PooledObjectHandler handler;
-+
-+ // We use arraydeque as it doesn't create garbage per element...
-+ protected final ArrayDeque pool;
-+ protected final int maxPoolSize;
-+
-+ protected final ReentrantLock lock = new ReentrantLock();
-+
-+ public IsolatedPool(final PooledObjectHandler handler, final int maxPoolSize) {
-+ this.handler = handler;
-+ this.pool = new ArrayDeque<>();
-+ this.maxPoolSize = maxPoolSize;
-+ }
-+
-+ protected E acquireOrCreateNoLock() {
-+ E ret;
-+
-+ ret = this.pool.poll();
-+
-+ if (ret == null) {
-+ ret = this.handler.createNew();
-+ }
-+ this.handler.onAcquire(ret);
-+
-+ return ret;
-+ }
-+
-+ public E tryAcquireUncontended() {
-+ if (!this.lock.tryLock()) {
-+ return null;
-+ }
-+ try {
-+ return this.acquireOrCreateNoLock();
-+ } finally {
-+ this.lock.unlock();
-+ }
-+ }
-+
-+ public E acquire() {
-+ this.lock.lock();
-+ try {
-+ return this.acquireOrCreateNoLock();
-+ } finally {
-+ this.lock.unlock();
-+ }
-+ }
-+
-+ protected void releaseNoLock(final E value) {
-+ if (this.pool.size() >= this.maxPoolSize) {
-+ this.handler.onRelease(value);
-+ return; // can't accept, we're at capacity
-+ }
-+
-+ this.pool.add(value);
-+ this.handler.onRelease(value);
-+ }
-+
-+ public boolean tryReleaseUncontended(final E value) {
-+ if (!this.lock.tryLock()) {
-+ return false;
-+ }
-+
-+ try {
-+ this.releaseNoLock(value);
-+ } finally {
-+ this.lock.unlock();
-+ }
-+
-+ return true;
-+ }
-+
-+ public void release(final E value) {
-+ this.lock.lock();
-+ try {
-+ this.releaseNoLock(value);
-+ } finally {
-+ this.lock.unlock();
-+ }
-+ }
-+ }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
new file mode 100644
@@ -3335,10 +3335,10 @@ index 75308712d0642d5ab168de653023349df8aee5ed..aa7501d366b15e7f7f64b7d98a1dccff
// CraftBukkit end
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
new file mode 100644
-index 0000000000000000000000000000000000000000..9fb9a96ccb37f5c7f39403e24e7b3bdb9279fe81
+index 0000000000000000000000000000000000000000..7164f46516bdf49ed52062f2d72f33418506bae0
--- /dev/null
+++ b/src/main/java/net/minecraft/server/MCUtil.java
-@@ -0,0 +1,414 @@
+@@ -0,0 +1,473 @@
+package net.minecraft.server;
+
+import com.destroystokyo.paper.block.TargetBlockInfo;
@@ -3355,21 +3355,80 @@ index 0000000000000000000000000000000000000000..9fb9a96ccb37f5c7f39403e24e7b3bdb
+import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
-+import java.util.concurrent.Executor;
-+import java.util.concurrent.Executors;
++import java.util.concurrent.LinkedBlockingQueue;
++import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
++import java.util.concurrent.atomic.AtomicBoolean;
++import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public final class MCUtil {
-+ private static final Executor asyncExecutor = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("Paper Async Task Handler Thread - %1$d").build());
++ public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor(
++ 0, 2, 60L, TimeUnit.SECONDS,
++ new LinkedBlockingQueue(),
++ new ThreadFactoryBuilder().setNameFormat("Paper Async Task Handler Thread - %1$d").build()
++ );
++ public static final ThreadPoolExecutor cleanerExecutor = new ThreadPoolExecutor(
++ 1, 1, 0L, TimeUnit.SECONDS,
++ new LinkedBlockingQueue(),
++ new ThreadFactoryBuilder().setNameFormat("Paper Object Cleaner").build()
++ );
+
+ public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE);
+
-+ public static void ensureTickThread(final String reason) {
-+ if (MinecraftServer.getServer().serverThread != Thread.currentThread()) {
-+ throw new IllegalStateException(reason);
-+ }
++
++ public static Runnable once(Runnable run) {
++ AtomicBoolean ran = new AtomicBoolean(false);
++ return () -> {
++ if (ran.compareAndSet(false, true)) {
++ run.run();
++ }
++ };
++ }
++
++ private static Runnable makeCleanerCallback(Runnable run) {
++ return once(() -> cleanerExecutor.execute(run));
++ }
++
++ /**
++ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky!
++ * @param obj
++ * @param run
++ * @return
++ */
++ public static Runnable registerCleaner(Object obj, Runnable run) {
++ // Wrap callback in its own method above or the lambda will leak object
++ Runnable cleaner = makeCleanerCallback(run);
++ co.aikar.cleaner.Cleaner.register(obj, cleaner);
++ return cleaner;
++ }
++
++ /**
++ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky!
++ * @param obj
++ * @param list
++ * @param cleaner
++ * @param
++ * @return
++ */
++ public static Runnable registerListCleaner(Object obj, List list, Consumer cleaner) {
++ return registerCleaner(obj, () -> {
++ list.forEach(cleaner);
++ list.clear();
++ });
++ }
++
++ /**
++ * DANGER WILL ROBINSON: Be sure you do not use a lambda that lives in the object being monitored, or leaky leaky!
++ * @param obj
++ * @param resource
++ * @param cleaner
++ * @param
++ * @return
++ */
++ public static Runnable registerCleaner(Object obj, T resource, java.util.function.Consumer cleaner) {
++ return registerCleaner(obj, () -> cleaner.accept(resource));
+ }
+
+ public static List getSpiralOutChunks(BlockPosition blockposition, int radius) {
@@ -3753,6 +3812,20 @@ index 0000000000000000000000000000000000000000..9fb9a96ccb37f5c7f39403e24e7b3bdb
+ }
+ }
+}
+diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
+index b4a0bd79511a3b1185a165991c937375aeecf3d1..786d38438cc1bd5a736b2dfa80aca9b9c6253e65 100644
+--- a/src/main/java/net/minecraft/server/MinecraftServer.java
++++ b/src/main/java/net/minecraft/server/MinecraftServer.java
+@@ -741,6 +741,9 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant= 5000000000L) {
-@@ -997,14 +1018,12 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant 0 && this.ticks % autosavePeriod == 0) { // CraftBukkit
@@ -840,7 +840,7 @@ index b4a0bd79511a3b1185a165991c937375aeecf3d1..67bdd577477730f1775f87189c9fcee6
}
this.methodProfiler.enter("snooper");
-@@ -1017,6 +1036,13 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant(ResourcePackLoader::new);
this.craftingManager = new CraftingManager();
this.tagRegistry = new TagRegistry();
-@@ -2186,7 +2188,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant
org.ow2.asm
-@@ -246,10 +263,18 @@
+@@ -252,10 +269,18 @@
META-INF/services/java.sql.Driver
@@ -185,7 +185,7 @@ index 4b1f8c53737f998fa57859146d5ddb999cdc8d41..d34f772fae3543cec6a130831b1f3eaa
System.setOut(new PrintStream(new LoggerOutputStream(logger, Level.INFO), true));
System.setErr(new PrintStream(new LoggerOutputStream(logger, Level.WARN), true));
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 8d6a0890073adbbb39db202f80d4b83cef2ceca9..284793e4bf04cddae3e070a1fa0afdd18001fd2e 100644
+index 80d8b0b0eac47b8d8e62db60da9daf0da8671fb3..87595425a358a13c8f2393619d51a981140556cf 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -57,7 +57,7 @@ import org.apache.commons.lang3.Validate;
@@ -225,7 +225,7 @@ index 8d6a0890073adbbb39db202f80d4b83cef2ceca9..284793e4bf04cddae3e070a1fa0afdd1
Runtime.getRuntime().addShutdownHook(new org.bukkit.craftbukkit.util.ServerShutdownThread(this));
}
// CraftBukkit end
-@@ -951,7 +955,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant= 5000000000L) {
this.Z = i;
this.serverPing.setPlayerSample(new ServerPing.ServerPingPlayerSample(this.getMaxPlayers(), this.getPlayerCount()));
diff --git a/Spigot-Server-Patches/0269-Add-Early-Warning-Feature-to-WatchDog.patch b/Spigot-Server-Patches/0269-Add-Early-Warning-Feature-to-WatchDog.patch
index 501d3869f3..736e84fac2 100644
--- a/Spigot-Server-Patches/0269-Add-Early-Warning-Feature-to-WatchDog.patch
+++ b/Spigot-Server-Patches/0269-Add-Early-Warning-Feature-to-WatchDog.patch
@@ -36,10 +36,10 @@ index adef07d4d521b4aaa6f3389b04aa27e29bec0229..214b577b326bc794fa3721deb6171228
public static int tabSpamLimit = 500;
private static void tabSpamLimiters() {
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index c502aedb8dc4e7a5d7ba9d16a200c20ca3d24cd4..056cbdeec8a1c17de44d59f16b77a995c82a3abb 100644
+index d1667eba3398efecc8913c2778931030a90d6195..b7c83cd82ca1c9b6bdaaf566e800b8d15ad7d966 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -870,6 +870,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant8.0.1
compile
-
+
diff --git a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java b/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
index 9b4a0f0678a7e8e347ef062ad15562484a74452b..4ae41fd2557dcc2a8e31d39ed978b2b26093dd06 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/Commodore.java
diff --git a/Spigot-Server-Patches/0321-Optimize-World-Time-Updates.patch b/Spigot-Server-Patches/0321-Optimize-World-Time-Updates.patch
index b01022a40a..d56e204895 100644
--- a/Spigot-Server-Patches/0321-Optimize-World-Time-Updates.patch
+++ b/Spigot-Server-Patches/0321-Optimize-World-Time-Updates.patch
@@ -8,10 +8,10 @@ the updates per world, so that we can re-use the same packet
object for every player unless they have per-player time enabled.
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index c97bbe933dd05829d9da7bc71d03d2c0a26a4ad1..3d9cc2ce67b5dc033df397e8d1c31f718792dcc4 100644
+index 66097d8cbc916d459ac0ab3b69fbac91a5b57ed3..207dd30539fa3961ba96aafe1e3cfef5020a885c 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -1166,12 +1166,24 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant> {
@@ -46,11 +45,18 @@ index b1dededc15cce686ead74a99bee64c89ac1de22c..35085ca3992e7c21139540d0c404e156
this.channel.attr(NetworkManager.c).set(enumprotocol);
this.channel.config().setAutoRead(true);
NetworkManager.LOGGER.debug("Enabled auto read");
-@@ -158,19 +163,75 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
+@@ -158,19 +163,82 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
NetworkManager.LOGGER.debug("Set listener of {} to {}", this, packetlistener);
this.packetListener = packetlistener;
}
+ // Paper start
++ private EntityPlayer getPlayer() {
++ if (packetListener instanceof PlayerConnection) {
++ return ((PlayerConnection) packetListener).player;
++ } else {
++ return null;
++ }
++ }
+ private static class InnerUtil { // Attempt to hide these methods from ProtocolLib so it doesn't accidently pick them up.
+ private static java.util.List buildExtraPackets(Packet packet) {
+ java.util.List extra = packet.getExtraPackets();
@@ -95,9 +101,9 @@ index b1dededc15cce686ead74a99bee64c89ac1de22c..35085ca3992e7c21139540d0c404e156
+ // Paper start - handle oversized packets better
+ boolean connected = this.isConnected();
+ if (!connected && !preparing) {
-+ packet.onPacketDone();
+ return; // Do nothing
+ }
++ packet.onPacketDispatch(getPlayer());
+ if (connected && (InnerUtil.canSendImmediate(this, packet) || (
+ MCUtil.isMainThread() && packet.isReady() && this.packetQueue.isEmpty() &&
+ (packet.getExtraPackets() == null || packet.getExtraPackets().isEmpty())
@@ -113,13 +119,13 @@ index b1dededc15cce686ead74a99bee64c89ac1de22c..35085ca3992e7c21139540d0c404e156
+ } else {
+ java.util.List packets = new java.util.ArrayList<>(1 + extraPackets.size());
+ packets.add(new NetworkManager.QueuedPacket(packet, null)); // delay the future listener until the end of the extra packets
-+
+
+ for (int i = 0, len = extraPackets.size(); i < len;) {
+ Packet extra = extraPackets.get(i);
+ boolean end = ++i == len;
+ packets.add(new NetworkManager.QueuedPacket(extra, end ? genericfuturelistener : null)); // append listener to the end
+ }
-
++
+ this.packetQueue.addAll(packets); // atomic
+ }
+ this.sendPacketQueue();
@@ -127,7 +133,31 @@ index b1dededc15cce686ead74a99bee64c89ac1de22c..35085ca3992e7c21139540d0c404e156
}
private void dispatchPacket(Packet> packet, @Nullable GenericFutureListener extends Future super Void>> genericFutureListener) { this.b(packet, genericFutureListener); } // Paper - OBFHELPER
-@@ -214,21 +275,46 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
+@@ -194,6 +262,11 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
+ if (genericfuturelistener != null) {
+ channelfuture.addListener(genericfuturelistener);
+ }
++ // Paper start
++ if (packet.hasFinishListener()) {
++ channelfuture.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(getPlayer(), channelFuture));
++ }
++ // Paper end
+
+ channelfuture.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+ } else {
+@@ -207,6 +280,11 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
+ if (genericfuturelistener != null) {
+ channelfuture1.addListener(genericfuturelistener);
+ }
++ // Paper start
++ if (packet.hasFinishListener()) {
++ channelfuture1.addListener((ChannelFutureListener) channelFuture -> packet.onPacketDispatchFinish(getPlayer(), channelFuture));
++ }
++ // Paper end
+
+ channelfuture1.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
+ });
+@@ -214,21 +292,46 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
}
@@ -184,11 +214,21 @@ index b1dededc15cce686ead74a99bee64c89ac1de22c..35085ca3992e7c21139540d0c404e156
public void a() {
this.o();
-@@ -257,9 +343,11 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
+@@ -257,9 +360,21 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
return this.socketAddress;
}
-+ public void clearPacketQueue() { QueuedPacket packet; while ((packet = packetQueue.poll()) != null) packet.getPacket().onPacketDone(); } // Paper
++ // Paper start
++ public void clearPacketQueue() {
++ EntityPlayer player = getPlayer();
++ packetQueue.forEach(queuedPacket -> {
++ Packet> packet = queuedPacket.getPacket();
++ if (packet.hasFinishListener()) {
++ packet.onPacketDispatchFinish(player, null);
++ }
++ });
++ packetQueue.clear();
++ } // Paper end
public void close(IChatBaseComponent ichatbasecomponent) {
// Spigot Start
this.preparing = false;
@@ -196,7 +236,7 @@ index b1dededc15cce686ead74a99bee64c89ac1de22c..35085ca3992e7c21139540d0c404e156
// Spigot End
if (this.channel.isOpen()) {
this.channel.close(); // We can't wait as this may be called from an event loop.
-@@ -335,7 +423,7 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
+@@ -335,7 +450,7 @@ public class NetworkManager extends SimpleChannelInboundHandler> {
} else if (this.i() != null) {
this.i().a(new ChatMessage("multiplayer.disconnect.generic", new Object[0]));
}
@@ -206,34 +246,32 @@ index b1dededc15cce686ead74a99bee64c89ac1de22c..35085ca3992e7c21139540d0c404e156
final PacketListener packetListener = this.i();
if (packetListener instanceof PlayerConnection) {
diff --git a/src/main/java/net/minecraft/server/Packet.java b/src/main/java/net/minecraft/server/Packet.java
-index 2d8e6a2f4a0c3c5d74a647d7164b0028781d3bf5..df1b4877b1560f982a1fcaf98404c8fe73e29973 100644
+index 2d8e6a2f4a0c3c5d74a647d7164b0028781d3bf5..ffc9a1f7d58d67611c4ab46462ac13a921042313 100644
--- a/src/main/java/net/minecraft/server/Packet.java
+++ b/src/main/java/net/minecraft/server/Packet.java
-@@ -11,6 +11,9 @@ public interface Packet {
+@@ -11,6 +11,20 @@ public interface Packet {
void a(T t0);
// Paper start
-+ default void onPacketDone() {}
++
++ /**
++ * @param player Null if not at PLAY stage yet
++ */
++ default void onPacketDispatch(@javax.annotation.Nullable EntityPlayer player) {}
++
++ /**
++ * @param player Null if not at PLAY stage yet
++ * @param future Can be null if packet was cancelled
++ */
++ default void onPacketDispatchFinish(@javax.annotation.Nullable EntityPlayer player, @javax.annotation.Nullable io.netty.channel.ChannelFuture future) {}
++ default boolean hasFinishListener() { return false; }
+ default boolean isReady() { return true; }
+ default java.util.List getExtraPackets() { return null; }
default boolean packetTooLarge(NetworkManager manager) {
return false;
}
-diff --git a/src/main/java/net/minecraft/server/PacketEncoder.java b/src/main/java/net/minecraft/server/PacketEncoder.java
-index b0cfef52cbb5e23beae528668e4e98cedecf603c..f46d028016a425a29674e768ae9310c825c088f2 100644
---- a/src/main/java/net/minecraft/server/PacketEncoder.java
-+++ b/src/main/java/net/minecraft/server/PacketEncoder.java
-@@ -48,7 +48,7 @@ public class PacketEncoder extends MessageToByteEncoder> {
- } else {
- throw throwable;
- }
-- }
-+ } finally { try { packet.onPacketDone(); } catch (Exception e) { e.printStackTrace(); } ; } // Paper
-
- // Paper start
- int packetLength = bytebuf.readableBytes();
diff --git a/src/main/java/net/minecraft/server/PlayerList.java b/src/main/java/net/minecraft/server/PlayerList.java
-index e148940ab3721cff27cf791c159c11b9b94191e4..e917d37382dab70ed9e6b62decf1557c33b26065 100644
+index 5136905b71085445eb6bac00e9200af8cc7fbe27..14c82861158eed8c91336590fb71c69d185d22f8 100644
--- a/src/main/java/net/minecraft/server/PlayerList.java
+++ b/src/main/java/net/minecraft/server/PlayerList.java
@@ -143,6 +143,7 @@ public abstract class PlayerList {
diff --git a/Spigot-Server-Patches/0360-Server-Tick-Events.patch b/Spigot-Server-Patches/0360-Server-Tick-Events.patch
index db2d5cb2bb..981bed288c 100644
--- a/Spigot-Server-Patches/0360-Server-Tick-Events.patch
+++ b/Spigot-Server-Patches/0360-Server-Tick-Events.patch
@@ -6,10 +6,10 @@ Subject: [PATCH] Server Tick Events
Fires event at start and end of a server tick
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 3d9cc2ce67b5dc033df397e8d1c31f718792dcc4..249eaf56bc0ec9eb99fdf8958d3ebe2b18999819 100644
+index 207dd30539fa3961ba96aafe1e3cfef5020a885c..ae4d62d52c0763849d06709fc405018711db6f90 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -1089,6 +1089,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant chunkGenerator;
private final WorldServer world;
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
-index 3342278bcd42a6d5a1793e33bc7fe4356be02451..2dcecc1bbd00e46b0a9b5e48bc580475fc8b4cb3 100644
+index be20d770df41a656cf2aabfec87e0bdc639053f4..de8b8f54cd906c1154a6790b9220d3e0976c74bd 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -4,7 +4,13 @@ import com.destroystokyo.paper.block.TargetBlockInfo;
@@ -226,8 +226,8 @@ index 3342278bcd42a6d5a1793e33bc7fe4356be02451..2dcecc1bbd00e46b0a9b5e48bc580475
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
- import java.util.concurrent.Executor;
-@@ -452,4 +461,170 @@ public final class MCUtil {
+ import java.util.concurrent.LinkedBlockingQueue;
+@@ -511,4 +520,170 @@ public final class MCUtil {
return null;
}
diff --git a/Spigot-Server-Patches/0378-incremental-chunk-saving.patch b/Spigot-Server-Patches/0378-incremental-chunk-saving.patch
index 6dc53a9e7c..5c1a01b75e 100644
--- a/Spigot-Server-Patches/0378-incremental-chunk-saving.patch
+++ b/Spigot-Server-Patches/0378-incremental-chunk-saving.patch
@@ -62,7 +62,7 @@ index e6d08756f76360b29b29f18305e5ec84d09f2d54..6713b7667ae4fe3f1f555a71321832b4
public void close() throws IOException {
// CraftBukkit start
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 0ee1d8e4869bfd0ccba21a227e115a36ff027984..7ecf781263179d87c943b08e192d8f010cf20d3e 100644
+index 8b499c815c77bf5b356d4216ba6cbf2a329c9aca..cfed5f51431ec5aecb538a321327bfb6e8a0bd88 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -168,6 +168,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant 0; // Paper
diff --git a/Spigot-Server-Patches/0456-Optimize-PlayerChunkMap-memory-use-for-visibleChunks.patch b/Spigot-Server-Patches/0456-Optimize-PlayerChunkMap-memory-use-for-visibleChunks.patch
index 71ba807c1b..3b16c1f78f 100644
--- a/Spigot-Server-Patches/0456-Optimize-PlayerChunkMap-memory-use-for-visibleChunks.patch
+++ b/Spigot-Server-Patches/0456-Optimize-PlayerChunkMap-memory-use-for-visibleChunks.patch
@@ -63,10 +63,10 @@ index fd998e4fb1534690a2ef8c1bca55e0ae9fe855f9..8f849d83d08b39f1cd9184f484a2089a
if (optional.isPresent()) {
diff --git a/src/main/java/net/minecraft/server/MCUtil.java b/src/main/java/net/minecraft/server/MCUtil.java
-index d9941b38ca037a31f520784b3706080f1d322fb4..71ab65e00fe31ea4047cf8a5921c6deba13de6b9 100644
+index f8a1f0b96f2eb8535e3080db979bb383d5a18a11..88b41b1d0c2045d01449256a5875ae73765c5595 100644
--- a/src/main/java/net/minecraft/server/MCUtil.java
+++ b/src/main/java/net/minecraft/server/MCUtil.java
-@@ -529,7 +529,7 @@ public final class MCUtil {
+@@ -588,7 +588,7 @@ public final class MCUtil {
WorldServer world = ((org.bukkit.craftbukkit.CraftWorld)bukkitWorld).getHandle();
PlayerChunkMap chunkMap = world.getChunkProvider().playerChunkMap;
diff --git a/Spigot-Server-Patches/0459-Mid-Tick-Chunk-Tasks-Speed-up-processing-of-chunk-lo.patch b/Spigot-Server-Patches/0459-Mid-Tick-Chunk-Tasks-Speed-up-processing-of-chunk-lo.patch
index 6aa92c7107..df6324af80 100644
--- a/Spigot-Server-Patches/0459-Mid-Tick-Chunk-Tasks-Speed-up-processing-of-chunk-lo.patch
+++ b/Spigot-Server-Patches/0459-Mid-Tick-Chunk-Tasks-Speed-up-processing-of-chunk-lo.patch
@@ -135,10 +135,10 @@ index 8f849d83d08b39f1cd9184f484a2089a7a3124ef..5806ca545191e609bab04e522e358948
protected boolean executeNext() {
// CraftBukkit start - process pending Chunk loadCallback() and unloadCallback() after each run task
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
-index 77adc64e30cbc1d4542eb8f4a446788c1fdc61be..3c25436f158316d2e09cbf4673365eddb03ecef4 100644
+index f9faa30ef914b1dd2dada9b7d89e80b34d2f1d0d..97cca4495a8dab4434e917a5d94192a28581925c 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
-@@ -907,6 +907,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant {
@@ -187,7 +187,7 @@ index 77adc64e30cbc1d4542eb8f4a446788c1fdc61be..3c25436f158316d2e09cbf4673365edd
return !this.canOversleep();
});
isOversleep = false;MinecraftTimings.serverOversleep.stopTiming();
-@@ -1175,13 +1194,16 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant1.3
test