From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Mon, 28 Mar 2016 20:55:47 -0400
Subject: [PATCH] MC Utils


diff --git a/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java
new file mode 100644
index 0000000000000000000000000000000000000000..4029dc68cf35d63aa70c4a76c35bf65a7fc6358f
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/concurrent/WeakSeqLock.java
@@ -0,0 +1,68 @@
+package com.destroystokyo.paper.util.concurrent;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+/**
+ * copied from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/lock/WeakSeqLock.java
+ * @author Spottedleaf
+ */
+public final class WeakSeqLock {
+    // TODO when the switch to J11 is made, nuke this class from orbit
+
+    protected final AtomicLong lock = new AtomicLong();
+
+    public WeakSeqLock() {
+        //VarHandle.storeStoreFence(); // warn: usages must be checked to ensure this behaviour isn't needed
+    }
+
+    public void acquireWrite() {
+        // must be release-type write
+        this.lock.lazySet(this.lock.get() + 1);
+    }
+
+    public boolean canRead(final long read) {
+        return (read & 1) == 0;
+    }
+
+    public boolean tryAcquireWrite() {
+        this.acquireWrite();
+        return true;
+    }
+
+    public void releaseWrite() {
+        // must be acquire-type write
+        final long lock = this.lock.get(); // volatile here acts as store-store
+        this.lock.lazySet(lock + 1);
+    }
+
+    public void abortWrite() {
+        // must be acquire-type write
+        final long lock = this.lock.get(); // volatile here acts as store-store
+        this.lock.lazySet(lock ^ 1);
+    }
+
+    public long acquireRead() {
+        int failures = 0;
+        long curr;
+
+        for (curr = this.lock.get(); !this.canRead(curr); curr = this.lock.get()) {
+            // without j11, our only backoff is the yield() call...
+
+            if (++failures > 5_000) { /* TODO determine a threshold */
+                Thread.yield();
+            }
+            /* Better waiting is beyond the scope of this lock; if it is needed the lock is being misused */
+        }
+
+        //VarHandle.loadLoadFence(); // volatile acts as the load-load barrier
+        return curr;
+    }
+
+    public boolean tryReleaseRead(final long read) {
+        return this.lock.get() == read; // volatile acts as the load-load barrier
+    }
+
+    public long getSequentialCounter() {
+        return this.lock.get();
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java
new file mode 100644
index 0000000000000000000000000000000000000000..59868f37d14bbc0ece0836095cdad148778995e6
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Int.java
@@ -0,0 +1,162 @@
+package com.destroystokyo.paper.util.map;
+
+import com.destroystokyo.paper.util.concurrent.WeakSeqLock;
+import it.unimi.dsi.fastutil.longs.Long2IntMap;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
+import it.unimi.dsi.fastutil.objects.ObjectIterator;
+
+/**
+ * @author Spottedleaf
+ */
+public class QueuedChangesMapLong2Int {
+
+    protected final Long2IntOpenHashMap updatingMap;
+    protected final Long2IntOpenHashMap visibleMap;
+    protected final Long2IntOpenHashMap queuedPuts;
+    protected final LongOpenHashSet queuedRemove;
+
+    protected int queuedDefaultReturnValue;
+
+    // we use a seqlock as writes are not common.
+    protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock();
+
+    public QueuedChangesMapLong2Int() {
+        this(16, 0.75f);
+    }
+
+    public QueuedChangesMapLong2Int(final int capacity, final float loadFactor) {
+        this.updatingMap = new Long2IntOpenHashMap(capacity, loadFactor);
+        this.visibleMap = new Long2IntOpenHashMap(capacity, loadFactor);
+        this.queuedPuts = new Long2IntOpenHashMap();
+        this.queuedRemove = new LongOpenHashSet();
+    }
+
+    public void queueDefaultReturnValue(final int dfl) {
+        this.queuedDefaultReturnValue = dfl;
+        this.updatingMap.defaultReturnValue(dfl);
+    }
+
+    public int queueUpdate(final long k, final int v) {
+        this.queuedRemove.remove(k);
+        this.queuedPuts.put(k, v);
+
+        return this.updatingMap.put(k, v);
+    }
+
+    public int queueRemove(final long k) {
+        this.queuedPuts.remove(k);
+        this.queuedRemove.add(k);
+
+        return this.updatingMap.remove(k);
+    }
+
+    public int getUpdating(final long k) {
+        return this.updatingMap.get(k);
+    }
+
+    public int getVisible(final long k) {
+        return this.visibleMap.get(k);
+    }
+
+    public int getVisibleAsync(final long k) {
+        long readlock;
+        int ret = 0;
+
+        do {
+            readlock = this.updatingMapSeqLock.acquireRead();
+            try {
+                ret = this.visibleMap.get(k);
+            } catch (final Throwable thr) {
+                if (thr instanceof ThreadDeath) {
+                    throw (ThreadDeath)thr;
+                }
+                // ignore...
+                continue;
+            }
+
+        } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
+
+        return ret;
+    }
+
+    public boolean performUpdates() {
+        this.updatingMapSeqLock.acquireWrite();
+        this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue);
+        this.updatingMapSeqLock.releaseWrite();
+
+        if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) {
+            return false;
+        }
+
+        // update puts
+        final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator();
+        while (iterator0.hasNext()) {
+            final Long2IntMap.Entry entry = iterator0.next();
+            final long key = entry.getLongKey();
+            final int val = entry.getIntValue();
+
+            this.updatingMapSeqLock.acquireWrite();
+            try {
+                this.visibleMap.put(key, val);
+            } finally {
+                this.updatingMapSeqLock.releaseWrite();
+            }
+        }
+
+        this.queuedPuts.clear();
+
+        final LongIterator iterator1 = this.queuedRemove.iterator();
+        while (iterator1.hasNext()) {
+            final long key = iterator1.nextLong();
+
+            this.updatingMapSeqLock.acquireWrite();
+            try {
+                this.visibleMap.remove(key);
+            } finally {
+                this.updatingMapSeqLock.releaseWrite();
+            }
+        }
+
+        this.queuedRemove.clear();
+
+        return true;
+    }
+
+    public boolean performUpdatesLockMap() {
+        this.updatingMapSeqLock.acquireWrite();
+        try {
+            this.visibleMap.defaultReturnValue(this.queuedDefaultReturnValue);
+
+            if (this.queuedPuts.isEmpty() && this.queuedRemove.isEmpty()) {
+                return false;
+            }
+
+            // update puts
+            final ObjectIterator<Long2IntMap.Entry> iterator0 = this.queuedPuts.long2IntEntrySet().fastIterator();
+            while (iterator0.hasNext()) {
+                final Long2IntMap.Entry entry = iterator0.next();
+                final long key = entry.getLongKey();
+                final int val = entry.getIntValue();
+
+                this.visibleMap.put(key, val);
+            }
+
+            this.queuedPuts.clear();
+
+            final LongIterator iterator1 = this.queuedRemove.iterator();
+            while (iterator1.hasNext()) {
+                final long key = iterator1.nextLong();
+
+                this.visibleMap.remove(key);
+            }
+
+            this.queuedRemove.clear();
+
+            return true;
+        } finally {
+            this.updatingMapSeqLock.releaseWrite();
+        }
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java
new file mode 100644
index 0000000000000000000000000000000000000000..7bab31a312463cc963d9621cdc543a281459bd32
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/map/QueuedChangesMapLong2Object.java
@@ -0,0 +1,202 @@
+package com.destroystokyo.paper.util.map;
+
+import com.destroystokyo.paper.util.concurrent.WeakSeqLock;
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import it.unimi.dsi.fastutil.objects.ObjectBidirectionalIterator;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @author Spottedleaf
+ */
+public class QueuedChangesMapLong2Object<V> {
+
+    protected static final Object REMOVED = new Object();
+
+    protected final Long2ObjectLinkedOpenHashMap<V> updatingMap;
+    protected final Long2ObjectLinkedOpenHashMap<V> visibleMap;
+    protected final Long2ObjectLinkedOpenHashMap<Object> queuedChanges;
+
+    // we use a seqlock as writes are not common.
+    protected final WeakSeqLock updatingMapSeqLock = new WeakSeqLock();
+
+    public QueuedChangesMapLong2Object() {
+        this(16, 0.75f); // dfl for fastutil
+    }
+
+    public QueuedChangesMapLong2Object(final int capacity, final float loadFactor) {
+        this.updatingMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor);
+        this.visibleMap = new Long2ObjectLinkedOpenHashMap<>(capacity, loadFactor);
+        this.queuedChanges = new Long2ObjectLinkedOpenHashMap<>();
+    }
+
+    public V queueUpdate(final long k, final V value) {
+        this.queuedChanges.put(k, value);
+        return this.updatingMap.put(k, value);
+    }
+
+    public V queueRemove(final long k) {
+        this.queuedChanges.put(k, REMOVED);
+        return this.updatingMap.remove(k);
+    }
+
+    public V getUpdating(final long k) {
+        return this.updatingMap.get(k);
+    }
+
+    public boolean updatingContainsKey(final long k) {
+        return this.updatingMap.containsKey(k);
+    }
+
+    public V getVisible(final long k) {
+        return this.visibleMap.get(k);
+    }
+
+    public boolean visibleContainsKey(final long k) {
+        return this.visibleMap.containsKey(k);
+    }
+
+    public V getVisibleAsync(final long k) {
+        long readlock;
+        V ret = null;
+
+        do {
+            readlock = this.updatingMapSeqLock.acquireRead();
+
+            try {
+                ret = this.visibleMap.get(k);
+            } catch (final Throwable thr) {
+                if (thr instanceof ThreadDeath) {
+                    throw (ThreadDeath)thr;
+                }
+                // ignore...
+                continue;
+            }
+
+        } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
+
+        return ret;
+    }
+
+    public boolean visibleContainsKeyAsync(final long k) {
+        long readlock;
+        boolean ret = false;
+
+        do {
+            readlock = this.updatingMapSeqLock.acquireRead();
+
+            try {
+                ret = this.visibleMap.containsKey(k);
+            } catch (final Throwable thr) {
+                if (thr instanceof ThreadDeath) {
+                    throw (ThreadDeath)thr;
+                }
+                // ignore...
+                continue;
+            }
+
+        } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
+
+        return ret;
+    }
+
+    public Long2ObjectLinkedOpenHashMap<V> getVisibleMap() {
+        return this.visibleMap;
+    }
+
+    public Long2ObjectLinkedOpenHashMap<V> getUpdatingMap() {
+        return this.updatingMap;
+    }
+
+    public int getVisibleSize() {
+        return this.visibleMap.size();
+    }
+
+    public int getVisibleSizeAsync() {
+        long readlock;
+        int ret;
+
+        do {
+            readlock = this.updatingMapSeqLock.acquireRead();
+            ret = this.visibleMap.size();
+        } while (!this.updatingMapSeqLock.tryReleaseRead(readlock));
+
+        return ret;
+    }
+
+    // unlike mojang's impl this cannot be used async since it's not a view of an immutable map
+    public Collection<V> getUpdatingValues() {
+        return this.updatingMap.values();
+    }
+
+    public List<V> getUpdatingValuesCopy() {
+        return new ArrayList<>(this.updatingMap.values());
+    }
+
+    // unlike mojang's impl this cannot be used async since it's not a view of an immutable map
+    public Collection<V> getVisibleValues() {
+        return this.visibleMap.values();
+    }
+
+    public List<V> getVisibleValuesCopy() {
+        return new ArrayList<>(this.visibleMap.values());
+    }
+
+    public boolean performUpdates() {
+        if (this.queuedChanges.isEmpty()) {
+            return false;
+        }
+
+        final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator();
+        while (iterator.hasNext()) {
+            final Long2ObjectMap.Entry<Object> entry = iterator.next();
+            final long key = entry.getLongKey();
+            final Object val = entry.getValue();
+
+            this.updatingMapSeqLock.acquireWrite();
+            try {
+                if (val == REMOVED) {
+                    this.visibleMap.remove(key);
+                } else {
+                    this.visibleMap.put(key, (V)val);
+                }
+            } finally {
+                this.updatingMapSeqLock.releaseWrite();
+            }
+        }
+
+        this.queuedChanges.clear();
+        return true;
+    }
+
+    public boolean performUpdatesLockMap() {
+        if (this.queuedChanges.isEmpty()) {
+            return false;
+        }
+
+        final ObjectBidirectionalIterator<Long2ObjectMap.Entry<Object>> iterator = this.queuedChanges.long2ObjectEntrySet().fastIterator();
+
+        try {
+            this.updatingMapSeqLock.acquireWrite();
+
+            while (iterator.hasNext()) {
+                final Long2ObjectMap.Entry<Object> entry = iterator.next();
+                final long key = entry.getLongKey();
+                final Object val = entry.getValue();
+
+                if (val == REMOVED) {
+                    this.visibleMap.remove(key);
+                } else {
+                    this.visibleMap.put(key, (V)val);
+                }
+            }
+        } finally {
+            this.updatingMapSeqLock.releaseWrite();
+        }
+
+        this.queuedChanges.clear();
+        return true;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java
new file mode 100644
index 0000000000000000000000000000000000000000..4eac0577862450e0e3299f5579f9ff6759b0256d
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/maplist/ChunkList.java
@@ -0,0 +1,129 @@
+package com.destroystokyo.paper.util.maplist;
+
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import net.minecraft.server.Chunk;
+import net.minecraft.server.MCUtil;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+// list with O(1) remove & contains
+/**
+ * @author Spottedleaf
+ */
+public final class ChunkList implements Iterable<Chunk> {
+
+    protected final Long2IntOpenHashMap chunkToIndex = new Long2IntOpenHashMap(2, 0.8f);
+    {
+        this.chunkToIndex.defaultReturnValue(Integer.MIN_VALUE);
+    }
+
+    protected static final Chunk[] EMPTY_LIST = new Chunk[0];
+
+    protected Chunk[] chunks = EMPTY_LIST;
+    protected int count;
+
+    public int size() {
+        return this.count;
+    }
+
+    public boolean contains(final Chunk chunk) {
+        return this.chunkToIndex.containsKey(chunk.coordinateKey);
+    }
+
+    public boolean remove(final Chunk chunk) {
+        final int index = this.chunkToIndex.remove(chunk.coordinateKey);
+        if (index == Integer.MIN_VALUE) {
+            return false;
+        }
+
+        // move the entity at the end to this index
+        final int endIndex = --this.count;
+        final Chunk end = this.chunks[endIndex];
+        if (index != endIndex) {
+            // not empty after this call
+            this.chunkToIndex.put(end.coordinateKey, index); // update index
+        }
+        this.chunks[index] = end;
+        this.chunks[endIndex] = null;
+
+        return true;
+    }
+
+    public boolean add(final Chunk chunk) {
+        final int count = this.count;
+        final int currIndex = this.chunkToIndex.putIfAbsent(chunk.coordinateKey, count);
+
+        if (currIndex != Integer.MIN_VALUE) {
+            return false; // already in this list
+        }
+
+        Chunk[] list = this.chunks;
+
+        if (list.length == count) {
+            // resize required
+            list = this.chunks = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+        }
+
+        list[count] = chunk;
+        this.count = count + 1;
+
+        return true;
+    }
+
+    public Chunk getChecked(final int index) {
+        if (index < 0 || index >= this.count) {
+            throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
+        }
+        return this.chunks[index];
+    }
+
+    public Chunk getUnchecked(final int index) {
+        return this.chunks[index];
+    }
+
+    public Chunk[] getRawData() {
+        return this.chunks;
+    }
+
+    public void clear() {
+        this.chunkToIndex.clear();
+        Arrays.fill(this.chunks, 0, this.count, null);
+        this.count = 0;
+    }
+
+    @Override
+    public Iterator<Chunk> iterator() {
+        return new Iterator<Chunk>() {
+
+            Chunk lastRet;
+            int current;
+
+            @Override
+            public boolean hasNext() {
+                return this.current < ChunkList.this.count;
+            }
+
+            @Override
+            public Chunk next() {
+                if (this.current >= ChunkList.this.count) {
+                    throw new NoSuchElementException();
+                }
+                return this.lastRet = ChunkList.this.chunks[this.current++];
+            }
+
+            @Override
+            public void remove() {
+                final Chunk lastRet = this.lastRet;
+
+                if (lastRet == null) {
+                    throw new IllegalStateException();
+                }
+                this.lastRet = null;
+
+                ChunkList.this.remove(lastRet);
+                --this.current;
+            }
+        };
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java
new file mode 100644
index 0000000000000000000000000000000000000000..cdda74564ced196ae577a64782236c2bfe36e433
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/maplist/EntityList.java
@@ -0,0 +1,128 @@
+package com.destroystokyo.paper.util.maplist;
+
+import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
+import net.minecraft.server.Entity;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+// list with O(1) remove & contains
+/**
+ * @author Spottedleaf
+ */
+public final class EntityList implements Iterable<Entity> {
+
+    protected final Int2IntOpenHashMap entityToIndex = new Int2IntOpenHashMap(2, 0.8f);
+    {
+        this.entityToIndex.defaultReturnValue(Integer.MIN_VALUE);
+    }
+
+    protected static final Entity[] EMPTY_LIST = new Entity[0];
+
+    protected Entity[] entities = EMPTY_LIST;
+    protected int count;
+
+    public int size() {
+        return this.count;
+    }
+
+    public boolean contains(final Entity entity) {
+        return this.entityToIndex.containsKey(entity.getId());
+    }
+
+    public boolean remove(final Entity entity) {
+        final int index = this.entityToIndex.remove(entity.getId());
+        if (index == Integer.MIN_VALUE) {
+            return false;
+        }
+
+        // move the entity at the end to this index
+        final int endIndex = --this.count;
+        final Entity end = this.entities[endIndex];
+        if (index != endIndex) {
+            // not empty after this call
+            this.entityToIndex.put(end.getId(), index); // update index
+        }
+        this.entities[index] = end;
+        this.entities[endIndex] = null;
+
+        return true;
+    }
+
+    public boolean add(final Entity entity) {
+        final int count = this.count;
+        final int currIndex = this.entityToIndex.putIfAbsent(entity.getId(), count);
+
+        if (currIndex != Integer.MIN_VALUE) {
+            return false; // already in this list
+        }
+
+        Entity[] list = this.entities;
+
+        if (list.length == count) {
+            // resize required
+            list = this.entities = Arrays.copyOf(list, (int)Math.max(4L, count * 2L)); // overflow results in negative
+        }
+
+        list[count] = entity;
+        this.count = count + 1;
+
+        return true;
+    }
+
+    public Entity getChecked(final int index) {
+        if (index < 0 || index >= this.count) {
+            throw new IndexOutOfBoundsException("Index: " + index + " is out of bounds, size: " + this.count);
+        }
+        return this.entities[index];
+    }
+
+    public Entity getUnchecked(final int index) {
+        return this.entities[index];
+    }
+
+    public Entity[] getRawData() {
+        return this.entities;
+    }
+
+    public void clear() {
+        this.entityToIndex.clear();
+        Arrays.fill(this.entities, 0, this.count, null);
+        this.count = 0;
+    }
+
+    @Override
+    public Iterator<Entity> iterator() {
+        return new Iterator<Entity>() {
+
+            Entity lastRet;
+            int current;
+
+            @Override
+            public boolean hasNext() {
+                return this.current < EntityList.this.count;
+            }
+
+            @Override
+            public Entity next() {
+                if (this.current >= EntityList.this.count) {
+                    throw new NoSuchElementException();
+                }
+                return this.lastRet = EntityList.this.entities[this.current++];
+            }
+
+            @Override
+            public void remove() {
+                final Entity lastRet = this.lastRet;
+
+                if (lastRet == null) {
+                    throw new IllegalStateException();
+                }
+                this.lastRet = null;
+
+                EntityList.this.remove(lastRet);
+                --this.current;
+            }
+        };
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java
new file mode 100644
index 0000000000000000000000000000000000000000..84ef8d9ecab4745a90504718f803110b9e2dbf65
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/maplist/IBlockDataList.java
@@ -0,0 +1,128 @@
+package com.destroystokyo.paper.util.maplist;
+
+import it.unimi.dsi.fastutil.longs.LongIterator;
+import it.unimi.dsi.fastutil.shorts.Short2LongOpenHashMap;
+import net.minecraft.server.ChunkSection;
+import net.minecraft.server.DataPaletteGlobal;
+import net.minecraft.server.IBlockData;
+import java.util.Arrays;
+
+/**
+ * @author Spottedleaf
+ */
+public final class IBlockDataList {
+
+    static final DataPaletteGlobal<IBlockData> GLOBAL_PALETTE = (DataPaletteGlobal)ChunkSection.GLOBAL_PALETTE;
+
+    // map of location -> (index | (location << 16) | (palette id << 32))
+    private final Short2LongOpenHashMap map = new Short2LongOpenHashMap(2, 0.8f);
+    {
+        this.map.defaultReturnValue(Long.MAX_VALUE);
+    }
+
+    private static final long[] EMPTY_LIST = new long[0];
+
+    private long[] byIndex = EMPTY_LIST;
+    private int size;
+
+    public static int getLocationKey(final int x, final int y, final int z) {
+        return (x & 15) | (((z & 15) << 4)) | ((y & 255) << (4 + 4));
+    }
+
+    public static IBlockData getBlockDataFromRaw(final long raw) {
+        return GLOBAL_PALETTE.getObject((int)(raw >>> 32));
+    }
+
+    public static int getIndexFromRaw(final long raw) {
+        return (int)(raw & 0xFFFF);
+    }
+
+    public static int getLocationFromRaw(final long raw) {
+        return (int)((raw >>> 16) & 0xFFFF);
+    }
+
+    public static long getRawFromValues(final int index, final int location, final IBlockData data) {
+        return (long)index | ((long)location << 16) | (((long)GLOBAL_PALETTE.getOrCreateIdFor(data)) << 32);
+    }
+
+    public static long setIndexRawValues(final long value, final int index) {
+        return value & ~(0xFFFF) | (index);
+    }
+
+    public long add(final int x, final int y, final int z, final IBlockData data) {
+        return this.add(getLocationKey(x, y, z), data);
+    }
+
+    public long add(final int location, final IBlockData data) {
+        final long curr = this.map.get((short)location);
+
+        if (curr == Long.MAX_VALUE) {
+            final int index = this.size++;
+            final long raw = getRawFromValues(index, location, data);
+            this.map.put((short)location, raw);
+
+            if (index >= this.byIndex.length) {
+                this.byIndex = Arrays.copyOf(this.byIndex, (int)Math.max(4L, this.byIndex.length * 2L));
+            }
+
+            this.byIndex[index] = raw;
+            return raw;
+        } else {
+            final int index = getIndexFromRaw(curr);
+            final long raw = this.byIndex[index] = getRawFromValues(index, location, data);
+
+            this.map.put((short)location, raw);
+
+            return raw;
+        }
+    }
+
+    public long remove(final int x, final int y, final int z) {
+        return this.remove(getLocationKey(x, y, z));
+    }
+
+    public long remove(final int location) {
+        final long ret = this.map.remove((short)location);
+        final int index = getIndexFromRaw(ret);
+        if (ret == Long.MAX_VALUE) {
+            return ret;
+        }
+
+        // move the entry at the end to this index
+        final int endIndex = --this.size;
+        final long end = this.byIndex[endIndex];
+        if (index != endIndex) {
+            // not empty after this call
+            this.map.put((short)getLocationFromRaw(end), setIndexRawValues(end, index));
+        }
+        this.byIndex[index] = end;
+        this.byIndex[endIndex] = 0L;
+
+        return ret;
+    }
+
+    public int size() {
+        return this.size;
+    }
+
+    public long getRaw(final int index) {
+        return this.byIndex[index];
+    }
+
+    public int getLocation(final int index) {
+        return getLocationFromRaw(this.getRaw(index));
+    }
+
+    public IBlockData getData(final int index) {
+        return getBlockDataFromRaw(this.getRaw(index));
+    }
+
+    public void clear() {
+        this.size = 0;
+        this.map.clear();
+    }
+
+    public LongIterator getRawIterator() {
+        return this.map.values().iterator();
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/math/IntegerUtil.java b/src/main/java/com/destroystokyo/paper/util/math/IntegerUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..c3b936f54b3fff418c265639ef223292ccc89356
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/math/IntegerUtil.java
@@ -0,0 +1,230 @@
+package com.destroystokyo.paper.util.math;
+
+/**
+ * @author Spottedleaf <Spottedleaf@users.noreply.github.com>
+ */
+public final class IntegerUtil {
+
+    public static final int HIGH_BIT_U32 = Integer.MIN_VALUE;
+    public static final long HIGH_BIT_U64 = Long.MIN_VALUE;
+
+    public static int ceilLog2(final int value) {
+        return Integer.SIZE - Integer.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
+    }
+
+    public static long ceilLog2(final long value) {
+        return Long.SIZE - Long.numberOfLeadingZeros(value - 1); // see doc of numberOfLeadingZeros
+    }
+
+    public static int floorLog2(final int value) {
+        // xor is optimized subtract for 2^n -1
+        // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
+        return (Integer.SIZE - 1) ^ Integer.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
+    }
+
+    public static int floorLog2(final long value) {
+        // xor is optimized subtract for 2^n -1
+        // note that (2^n -1) - k = (2^n -1) ^ k for k <= (2^n - 1)
+        return (Long.SIZE - 1) ^ Long.numberOfLeadingZeros(value); // see doc of numberOfLeadingZeros
+    }
+
+    public static int roundCeilLog2(final int value) {
+        // optimized variant of 1 << (32 - leading(val - 1))
+        // given
+        // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
+        // 1 << (32 - leading(val - 1)) = HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
+        // HIGH_BIT_32 >>> (31 - (32 - leading(val - 1)))
+        // HIGH_BIT_32 >>> (31 - 32 + leading(val - 1))
+        // HIGH_BIT_32 >>> (-1 + leading(val - 1))
+        return HIGH_BIT_U32 >>> (Integer.numberOfLeadingZeros(value - 1) - 1);
+    }
+
+    public static long roundCeilLog2(final long value) {
+        // see logic documented above
+        return HIGH_BIT_U64 >>> (Long.numberOfLeadingZeros(value - 1) - 1);
+    }
+
+    public static int roundFloorLog2(final int value) {
+        // optimized variant of 1 << (31 - leading(val))
+        // given
+        // 1 << n = HIGH_BIT_32 >>> (31 - n) for n [0, 32)
+        // 1 << (31 - leading(val)) = HIGH_BIT_32 >> (31 - (31 - leading(val)))
+        // HIGH_BIT_32 >> (31 - (31 - leading(val)))
+        // HIGH_BIT_32 >> (31 - 31 + leading(val))
+        return HIGH_BIT_U32 >>> Integer.numberOfLeadingZeros(value);
+    }
+
+    public static long roundFloorLog2(final long value) {
+        // see logic documented above
+        return HIGH_BIT_U64 >>> Long.numberOfLeadingZeros(value);
+    }
+
+    public static boolean isPowerOfTwo(final int n) {
+        // 2^n has one bit
+        // note: this rets true for 0 still
+        return IntegerUtil.getTrailingBit(n) == n;
+    }
+
+    public static boolean isPowerOfTwo(final long n) {
+        // 2^n has one bit
+        // note: this rets true for 0 still
+        return IntegerUtil.getTrailingBit(n) == n;
+    }
+
+
+    public static int getTrailingBit(final int n) {
+        return -n & n;
+    }
+
+    public static long getTrailingBit(final long n) {
+        return -n & n;
+    }
+
+    public static int trailingZeros(final int n) {
+        return Integer.numberOfTrailingZeros(n);
+    }
+
+    public static long trailingZeros(final long n) {
+        return Long.numberOfTrailingZeros(n);
+    }
+
+    // from hacker's delight (signed division magic value)
+    public static int getDivisorMultiple(final long numbers) {
+        return (int)(numbers >>> 32);
+    }
+
+    // from hacker's delight (signed division magic value)
+    public static int getDivisorShift(final long numbers) {
+        return (int)numbers;
+    }
+
+    // copied from hacker's delight (signed division magic value)
+    // http://www.hackersdelight.org/hdcodetxt/magic.c.txt
+    public static long getDivisorNumbers(final int d) {
+        final int ad = IntegerUtil.branchlessAbs(d);
+
+        if (ad < 2) {
+            throw new IllegalArgumentException("|number| must be in [2, 2^31 -1], not: " + d);
+        }
+
+        final int two31 = 0x80000000;
+        final long mask = 0xFFFFFFFFL; // mask for enforcing unsigned behaviour
+
+        int p = 31;
+
+        // all these variables are UNSIGNED!
+        int t = two31 + (d >>> 31);
+        int anc = t - 1 - t%ad;
+        int q1 = (int)((two31 & mask)/(anc & mask));
+        int r1 = two31 - q1*anc;
+        int q2 = (int)((two31 & mask)/(ad & mask));
+        int r2 = two31 - q2*ad;
+        int delta;
+
+        do {
+            p = p + 1;
+            q1 = 2*q1;                        // Update q1 = 2**p/|nc|.
+            r1 = 2*r1;                        // Update r1 = rem(2**p, |nc|).
+            if ((r1 & mask) >= (anc & mask)) {// (Must be an unsigned comparison here)
+                q1 = q1 + 1;
+                r1 = r1 - anc;
+            }
+            q2 = 2*q2;                       // Update q2 = 2**p/|d|.
+            r2 = 2*r2;                       // Update r2 = rem(2**p, |d|).
+            if ((r2 & mask) >= (ad & mask)) {// (Must be an unsigned comparison here)
+                q2 = q2 + 1;
+                r2 = r2 - ad;
+            }
+            delta = ad - r2;
+        } while ((q1 & mask) < (delta & mask) || (q1 == delta && r1 == 0));
+
+        int magicNum = q2 + 1;
+        if (d < 0) {
+            magicNum = -magicNum;
+        }
+        int shift = p - 32;
+        return ((long)magicNum << 32) | shift;
+    }
+
+    public static int branchlessAbs(final int val) {
+        // -n = -1 ^ n + 1
+        final int mask = val >> (Integer.SIZE - 1); // -1 if < 0, 0 if >= 0
+        return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
+    }
+
+    public static long branchlessAbs(final long val) {
+        // -n = -1 ^ n + 1
+        final long mask = val >> (Long.SIZE - 1); // -1 if < 0, 0 if >= 0
+        return (mask ^ val) - mask; // if val < 0, then (0 ^ val) - 0 else (-1 ^ val) + 1
+    }
+
+    //https://github.com/skeeto/hash-prospector for hash functions
+
+    //score = ~590.47984224483832
+    public static int hash0(int x) {
+        x *= 0x36935555;
+        x ^= x >>> 16;
+        return x;
+    }
+
+    //score = ~310.01596637036749
+    public static int hash1(int x) {
+        x ^= x >>> 15;
+        x *= 0x356aaaad;
+        x ^= x >>> 17;
+        return x;
+    }
+
+    public static int hash2(int x) {
+        x ^= x >>> 16;
+        x *= 0x7feb352d;
+        x ^= x >>> 15;
+        x *= 0x846ca68b;
+        x ^= x >>> 16;
+        return x;
+    }
+
+    public static int hash3(int x) {
+        x ^= x >>> 17;
+        x *= 0xed5ad4bb;
+        x ^= x >>> 11;
+        x *= 0xac4c1b51;
+        x ^= x >>> 15;
+        x *= 0x31848bab;
+        x ^= x >>> 14;
+        return x;
+    }
+
+    //score = ~365.79959673201887
+    public static long hash1(long x) {
+        x ^= x >>> 27;
+        x *= 0xb24924b71d2d354bL;
+        x ^= x >>> 28;
+        return x;
+    }
+
+    //h2 hash
+    public static long hash2(long x) {
+        x ^= x >>> 32;
+        x *= 0xd6e8feb86659fd93L;
+        x ^= x >>> 32;
+        x *= 0xd6e8feb86659fd93L;
+        x ^= x >>> 32;
+        return x;
+    }
+
+    public static long hash3(long x) {
+        x ^= x >>> 45;
+        x *= 0xc161abe5704b6c79L;
+        x ^= x >>> 41;
+        x *= 0xe3e5389aedbc90f7L;
+        x ^= x >>> 56;
+        x *= 0x1f9aba75a52db073L;
+        x ^= x >>> 53;
+        return x;
+    }
+
+    private IntegerUtil() {
+        throw new RuntimeException();
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..1330df2c1d3c4f52dad0adeb169409eb412814ab
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/misc/AreaMap.java
@@ -0,0 +1,453 @@
+package com.destroystokyo.paper.util.misc;
+
+import com.destroystokyo.paper.util.math.IntegerUtil;
+import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2LongOpenHashMap;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.MCUtil;
+import net.minecraft.server.MinecraftServer;
+import javax.annotation.Nullable;
+import java.util.Iterator;
+
+/** @author Spottedleaf */
+public abstract class AreaMap<E> {
+
+    /* Tested via https://gist.github.com/Spottedleaf/520419c6f41ef348fe9926ce674b7217 */
+
+    protected final Object2LongOpenHashMap<E> objectToLastCoordinate = new Object2LongOpenHashMap<>();
+    protected final Object2IntOpenHashMap<E> objectToViewDistance = new Object2IntOpenHashMap<>();
+
+    {
+        this.objectToViewDistance.defaultReturnValue(-1);
+        this.objectToLastCoordinate.defaultReturnValue(Long.MIN_VALUE);
+    }
+
+    // we use linked for better iteration.
+    // map of: coordinate to set of objects in coordinate
+    protected final Long2ObjectOpenHashMap<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> areaMap = new Long2ObjectOpenHashMap<>(1024, 0.7f);
+    protected final PooledLinkedHashSets<E> pooledHashSets;
+
+    protected final ChangeCallback<E> addCallback;
+    protected final ChangeCallback<E> removeCallback;
+    protected final ChangeSourceCallback<E> changeSourceCallback;
+
+    public AreaMap() {
+        this(new PooledLinkedHashSets<>());
+    }
+
+    // let users define a "global" or "shared" pooled sets if they wish
+    public AreaMap(final PooledLinkedHashSets<E> pooledHashSets) {
+        this(pooledHashSets, null, null);
+    }
+
+    public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback) {
+        this(pooledHashSets, addCallback, removeCallback, null);
+    }
+    public AreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback, final ChangeSourceCallback<E> changeSourceCallback) {
+        this.pooledHashSets = pooledHashSets;
+        this.addCallback = addCallback;
+        this.removeCallback = removeCallback;
+        this.changeSourceCallback = changeSourceCallback;
+    }
+
+    @Nullable
+    public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final long key) {
+        return this.areaMap.get(key);
+    }
+
+    @Nullable
+    public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final ChunkCoordIntPair chunkPos) {
+        return this.areaMap.get(MCUtil.getCoordinateKey(chunkPos));
+    }
+
+    @Nullable
+    public final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getObjectsInRange(final int chunkX, final int chunkZ) {
+        return this.areaMap.get(MCUtil.getCoordinateKey(chunkX, chunkZ));
+    }
+
+    // Long.MIN_VALUE indicates the object is not mapped
+    public final long getLastCoordinate(final E object) {
+        return this.objectToLastCoordinate.getOrDefault(object, Long.MIN_VALUE);
+    }
+
+    // -1 indicates the object is not mapped
+    public final int getLastViewDistance(final E object) {
+        return this.objectToViewDistance.getOrDefault(object, -1);
+    }
+
+    // returns the total number of mapped chunks
+    public final int size() {
+        return this.areaMap.size();
+    }
+
+    public final void addOrUpdate(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
+        final int oldViewDistance = this.objectToViewDistance.put(object, viewDistance);
+        final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
+        final long oldPos = this.objectToLastCoordinate.put(object, newPos);
+
+        if (oldViewDistance == -1) {
+            this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance);
+            this.addObjectCallback(object, chunkX, chunkZ, viewDistance);
+        } else {
+            this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance);
+            this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance);
+        }
+        //this.validate(object, viewDistance);
+    }
+
+    public final boolean update(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
+        final int oldViewDistance = this.objectToViewDistance.replace(object, viewDistance);
+        if (oldViewDistance == -1) {
+            return false;
+        } else {
+            final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
+            final long oldPos = this.objectToLastCoordinate.put(object, newPos);
+            this.updateObject(object, oldPos, newPos, oldViewDistance, viewDistance);
+            this.updateObjectCallback(object, oldPos, newPos, oldViewDistance, viewDistance);
+        }
+        //this.validate(object, viewDistance);
+        return true;
+    }
+
+    // called after the distance map updates
+    protected void updateObjectCallback(final E Object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
+        if (newPosition != oldPosition && this.changeSourceCallback != null) {
+            this.changeSourceCallback.accept(Object, oldPosition, newPosition);
+        }
+    }
+
+    public final boolean add(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
+        final int oldViewDistance = this.objectToViewDistance.putIfAbsent(object, viewDistance);
+        if (oldViewDistance != -1) {
+            return false;
+        }
+
+        final long newPos = MCUtil.getCoordinateKey(chunkX, chunkZ);
+        this.objectToLastCoordinate.put(object, newPos);
+        this.addObject(object, chunkX, chunkZ, Integer.MIN_VALUE, Integer.MIN_VALUE, viewDistance);
+        this.addObjectCallback(object, chunkX, chunkZ, viewDistance);
+
+        //this.validate(object, viewDistance);
+
+        return true;
+    }
+
+    // called after the distance map updates
+    protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {}
+
+    public final boolean remove(final E object) {
+        final long position = this.objectToLastCoordinate.removeLong(object);
+        final int viewDistance = this.objectToViewDistance.removeInt(object);
+
+        if (viewDistance == -1) {
+            return false;
+        }
+
+        final int currentX = MCUtil.getCoordinateX(position);
+        final int currentZ = MCUtil.getCoordinateZ(position);
+
+        this.removeObject(object, currentX, currentZ, currentX, currentZ, viewDistance);
+        this.removeObjectCallback(object, currentX, currentZ, viewDistance);
+        //this.validate(object, -1);
+        return true;
+    }
+
+    // called after the distance map updates
+    protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {}
+
+    protected abstract PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> getEmptySetFor(final E object);
+
+    // expensive op, only for debug
+    protected void validate(final E object, final int viewDistance) {
+        int entiesGot = 0;
+        int expectedEntries = (2 * viewDistance + 1);
+        expectedEntries *= expectedEntries;
+        if (viewDistance < 0) {
+            expectedEntries = 0;
+        }
+
+        final long currPosition = this.objectToLastCoordinate.getLong(object);
+
+        final int centerX = MCUtil.getCoordinateX(currPosition);
+        final int centerZ = MCUtil.getCoordinateZ(currPosition);
+
+        for (Iterator<Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>>> iterator = this.areaMap.long2ObjectEntrySet().fastIterator();
+             iterator.hasNext();) {
+
+            final Long2ObjectLinkedOpenHashMap.Entry<PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E>> entry = iterator.next();
+            final long key = entry.getLongKey();
+            final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> map = entry.getValue();
+
+            if (map.referenceCount == 0) {
+                throw new IllegalStateException("Invalid map");
+            }
+
+            if (map.contains(object)) {
+                ++entiesGot;
+
+                final int chunkX = MCUtil.getCoordinateX(key);
+                final int chunkZ = MCUtil.getCoordinateZ(key);
+
+                final int dist = Math.max(IntegerUtil.branchlessAbs(chunkX - centerX), IntegerUtil.branchlessAbs(chunkZ - centerZ));
+
+                if (dist > viewDistance) {
+                    throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist);
+                }
+            }
+        }
+
+        if (entiesGot != expectedEntries) {
+            throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot);
+        }
+    }
+
+    private void addObjectTo(final E object, final int chunkX, final int chunkZ, final int currChunkX,
+                             final int currChunkZ, final int prevChunkX, final int prevChunkZ) {
+        final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
+
+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> empty = this.getEmptySetFor(object);
+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.putIfAbsent(key, empty);
+
+        if (current != null) {
+            PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWith(current, object);
+            if (next == current) {
+                throw new IllegalStateException("Expected different map: got " + next.toString());
+            }
+            this.areaMap.put(key, next);
+
+            current = next;
+            // fall through to callback
+        } else {
+            current = empty;
+        }
+
+        if (this.addCallback != null) {
+            try {
+                this.addCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, current);
+            } catch (final Throwable ex) {
+                if (ex instanceof ThreadDeath) {
+                    throw (ThreadDeath)ex;
+                }
+                MinecraftServer.LOGGER.error("Add callback for map threw exception ", ex);
+            }
+        }
+    }
+
+    private void removeObjectFrom(final E object, final int chunkX, final int chunkZ, final int currChunkX,
+                                  final int currChunkZ, final int prevChunkX, final int prevChunkZ) {
+        final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
+
+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> current = this.areaMap.get(key);
+
+        if (current == null) {
+            throw new IllegalStateException("Current map may not be null for " + object + ", (" + chunkX + "," + chunkZ + ")");
+        }
+
+        PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> next = this.pooledHashSets.findMapWithout(current, object);
+
+        if (next == current) {
+            throw new IllegalStateException("Current map [" + next.toString() + "] should have contained " + object + ", (" + chunkX + "," + chunkZ + ")");
+        }
+
+        if (next != null) {
+            this.areaMap.put(key, next);
+        } else {
+            this.areaMap.remove(key);
+        }
+
+        if (this.removeCallback != null) {
+            try {
+                this.removeCallback.accept(object, chunkX, chunkZ, currChunkX, currChunkZ, prevChunkX, prevChunkZ, next);
+            } catch (final Throwable ex) {
+                if (ex instanceof ThreadDeath) {
+                    throw (ThreadDeath)ex;
+                }
+                MinecraftServer.LOGGER.error("Remove callback for map threw exception ", ex);
+            }
+        }
+    }
+
+    private void addObject(final E object, final int chunkX, final int chunkZ, final int prevChunkX, final int prevChunkZ, final int viewDistance) {
+        final int maxX = chunkX + viewDistance;
+        final int maxZ = chunkZ + viewDistance;
+        final int minX = chunkX - viewDistance;
+        final int minZ = chunkZ - viewDistance;
+        for (int x = minX; x <= maxX; ++x) {
+            for (int z = minZ; z <= maxZ; ++z) {
+                this.addObjectTo(object, x, z, chunkX, chunkZ, prevChunkX, prevChunkZ);
+            }
+        }
+    }
+
+    private void removeObject(final E object, final int chunkX, final int chunkZ, final int currentChunkX, final int currentChunkZ, final int viewDistance) {
+        final int maxX = chunkX + viewDistance;
+        final int maxZ = chunkZ + viewDistance;
+        final int minX = chunkX - viewDistance;
+        final int minZ = chunkZ - viewDistance;
+        for (int x = minX; x <= maxX; ++x) {
+            for (int z = minZ; z <= maxZ; ++z) {
+                this.removeObjectFrom(object, x, z, currentChunkX, currentChunkZ, chunkX, chunkZ);
+            }
+        }
+    }
+
+    /* math sign function except 0 returns 1 */
+    protected static int sign(int val) {
+        return 1 | (val >> (Integer.SIZE - 1));
+    }
+
+    private void updateObject(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
+        final int toX = MCUtil.getCoordinateX(newPosition);
+        final int toZ = MCUtil.getCoordinateZ(newPosition);
+        final int fromX = MCUtil.getCoordinateX(oldPosition);
+        final int fromZ = MCUtil.getCoordinateZ(oldPosition);
+
+        final int dx = toX - fromX;
+        final int dz = toZ - fromZ;
+
+        final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
+        final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
+
+        if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
+            // teleported?
+            this.removeObject(object, fromX, fromZ, fromX, fromZ, oldViewDistance);
+            this.addObject(object, toX, toZ, fromX, fromZ, newViewDistance);
+            return;
+        }
+
+        if (oldViewDistance != newViewDistance) {
+            // remove loop
+
+            final int oldMinX = fromX - oldViewDistance;
+            final int oldMinZ = fromZ - oldViewDistance;
+            final int oldMaxX = fromX + oldViewDistance;
+            final int oldMaxZ = fromZ + oldViewDistance;
+            for (int currX = oldMinX; currX <= oldMaxX; ++currX) {
+                for (int currZ = oldMinZ; currZ <= oldMaxZ; ++currZ) {
+
+                    // only remove if we're outside the new view distance...
+                    if (Math.max(IntegerUtil.branchlessAbs(currX - toX), IntegerUtil.branchlessAbs(currZ - toZ)) > newViewDistance) {
+                        this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
+                    }
+                }
+            }
+
+            // add loop
+
+            final int newMinX = toX - newViewDistance;
+            final int newMinZ = toZ - newViewDistance;
+            final int newMaxX = toX + newViewDistance;
+            final int newMaxZ = toZ + newViewDistance;
+            for (int currX = newMinX; currX <= newMaxX; ++currX) {
+                for (int currZ = newMinZ; currZ <= newMaxZ; ++currZ) {
+
+                    // only add if we're outside the old view distance...
+                    if (Math.max(IntegerUtil.branchlessAbs(currX - fromX), IntegerUtil.branchlessAbs(currZ - fromZ)) > oldViewDistance) {
+                        this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
+                    }
+                }
+            }
+
+            return;
+        }
+
+        // x axis is width
+        // z axis is height
+        // right refers to the x axis of where we moved
+        // top refers to the z axis of where we moved
+
+        // same view distance
+
+        // used for relative positioning
+        final int up = sign(dz); // 1 if dz >= 0, -1 otherwise
+        final int right = sign(dx); // 1 if dx >= 0, -1 otherwise
+
+        // The area excluded by overlapping the two view distance squares creates four rectangles:
+        // Two on the left, and two on the right. The ones on the left we consider the "removed" section
+        // and on the right the "added" section.
+        // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
+        // exclusive to the regions they surround.
+
+        // 4 points of the rectangle
+        int maxX; // exclusive
+        int minX; // inclusive
+        int maxZ; // exclusive
+        int minZ; // inclusive
+
+        if (dx != 0) {
+            // handle right addition
+
+            maxX = toX + (oldViewDistance * right) + right; // exclusive
+            minX = fromX + (oldViewDistance * right) + right; // inclusive
+            maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+            minZ = toZ - (oldViewDistance * up); // inclusive
+
+            for (int currX = minX; currX != maxX; currX += right) {
+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
+                    this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
+                }
+            }
+        }
+
+        if (dz != 0) {
+            // handle up addition
+
+            maxX = toX + (oldViewDistance * right) + right; // exclusive
+            minX = toX - (oldViewDistance * right); // inclusive
+            maxZ = toZ + (oldViewDistance * up) + up; // exclusive
+            minZ = fromZ + (oldViewDistance * up) + up; // inclusive
+
+            for (int currX = minX; currX != maxX; currX += right) {
+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
+                    this.addObjectTo(object, currX, currZ, toX, toZ, fromX, fromZ);
+                }
+            }
+        }
+
+        if (dx != 0) {
+            // handle left removal
+
+            maxX = toX - (oldViewDistance * right); // exclusive
+            minX = fromX - (oldViewDistance * right); // inclusive
+            maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
+            minZ = toZ - (oldViewDistance * up); // inclusive
+
+            for (int currX = minX; currX != maxX; currX += right) {
+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
+                    this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
+                }
+            }
+        }
+
+        if (dz != 0) {
+            // handle down removal
+
+            maxX = fromX + (oldViewDistance * right) + right; // exclusive
+            minX = fromX - (oldViewDistance * right); // inclusive
+            maxZ = toZ - (oldViewDistance * up); // exclusive
+            minZ = fromZ - (oldViewDistance * up); // inclusive
+
+            for (int currX = minX; currX != maxX; currX += right) {
+                for (int currZ = minZ; currZ != maxZ; currZ += up) {
+                    this.removeObjectFrom(object, currX, currZ, toX, toZ, fromX, fromZ);
+                }
+            }
+        }
+    }
+
+    @FunctionalInterface
+    public static interface ChangeCallback<E> {
+
+        // if there is no previous position, then prevPos = Integer.MIN_VALUE
+        void accept(final E object, final int rangeX, final int rangeZ, final int currPosX, final int currPosZ, final int prevPosX, final int prevPosZ,
+                    final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> newState);
+
+    }
+
+    @FunctionalInterface
+    public static interface ChangeSourceCallback<E> {
+        void accept(final E object, final long prevPos, final long newPos);
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..3f86c1ad43782bdc56be6c0eca053311e51228ca
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/misc/DistanceTrackingAreaMap.java
@@ -0,0 +1,175 @@
+package com.destroystokyo.paper.util.misc;
+
+import com.destroystokyo.paper.util.math.IntegerUtil;
+import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
+import net.minecraft.server.ChunkCoordIntPair;
+import net.minecraft.server.MCUtil;
+
+/** @author Spottedleaf */
+public abstract class DistanceTrackingAreaMap<E> extends AreaMap<E> {
+
+    // use this map only if you need distance tracking, the tracking here is obviously going to hit harder.
+
+    protected final Long2IntOpenHashMap chunkToNearestDistance = new Long2IntOpenHashMap(1024, 0.7f);
+    {
+        this.chunkToNearestDistance.defaultReturnValue(-1);
+    }
+
+    protected final DistanceChangeCallback<E> distanceChangeCallback;
+
+    public DistanceTrackingAreaMap() {
+        this(new PooledLinkedHashSets<>());
+    }
+
+    // let users define a "global" or "shared" pooled sets if they wish
+    public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets) {
+        this(pooledHashSets, null, null, null);
+    }
+
+    public DistanceTrackingAreaMap(final PooledLinkedHashSets<E> pooledHashSets, final ChangeCallback<E> addCallback, final ChangeCallback<E> removeCallback,
+                                   final DistanceChangeCallback<E> distanceChangeCallback) {
+        super(pooledHashSets, addCallback, removeCallback);
+        this.distanceChangeCallback = distanceChangeCallback;
+    }
+
+    // ret -1 if there is nothing mapped
+    public final int getNearestObjectDistance(final long key) {
+        return this.chunkToNearestDistance.get(key);
+    }
+
+    // ret -1 if there is nothing mapped
+    public final int getNearestObjectDistance(final ChunkCoordIntPair chunkPos) {
+        return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkPos));
+    }
+
+    // ret -1 if there is nothing mapped
+    public final int getNearestObjectDistance(final int chunkX, final int chunkZ) {
+        return this.chunkToNearestDistance.get(MCUtil.getCoordinateKey(chunkX, chunkZ));
+    }
+
+    protected final void recalculateDistance(final int chunkX, final int chunkZ) {
+        final long key = MCUtil.getCoordinateKey(chunkX, chunkZ);
+        final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state = this.areaMap.get(key);
+        if (state == null) {
+            final int oldDistance = this.chunkToNearestDistance.remove(key);
+            // nothing here.
+            if (oldDistance == -1) {
+                // nothing was here previously
+                return;
+            }
+            if (this.distanceChangeCallback != null) {
+                this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, -1, null);
+            }
+            return;
+        }
+
+        int newDistance = Integer.MAX_VALUE;
+
+        final Object[] rawData = state.getBackingSet();
+        for (int i = 0, len = rawData.length; i < len; ++i) {
+            final Object raw = rawData[i];
+
+            if (raw == null) {
+                continue;
+            }
+
+            final E object = (E)raw;
+            final long location = this.objectToLastCoordinate.getLong(object);
+
+            final int distance = Math.max(IntegerUtil.branchlessAbs(chunkX - MCUtil.getCoordinateX(location)), IntegerUtil.branchlessAbs(chunkZ - MCUtil.getCoordinateZ(location)));
+
+            if (distance < newDistance) {
+                newDistance = distance;
+            }
+        }
+
+        final int oldDistance = this.chunkToNearestDistance.put(key, newDistance);
+
+        if (oldDistance != newDistance) {
+            if (this.distanceChangeCallback != null) {
+                this.distanceChangeCallback.accept(chunkX, chunkZ, oldDistance, newDistance, state);
+            }
+        }
+    }
+
+    @Override
+    protected void addObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
+        final int maxX = chunkX + viewDistance;
+        final int maxZ = chunkZ + viewDistance;
+        final int minX = chunkX - viewDistance;
+        final int minZ = chunkZ - viewDistance;
+        for (int x = minX; x <= maxX; ++x) {
+            for (int z = minZ; z <= maxZ; ++z) {
+                this.recalculateDistance(x, z);
+            }
+        }
+    }
+
+    @Override
+    protected void removeObjectCallback(final E object, final int chunkX, final int chunkZ, final int viewDistance) {
+        final int maxX = chunkX + viewDistance;
+        final int maxZ = chunkZ + viewDistance;
+        final int minX = chunkX - viewDistance;
+        final int minZ = chunkZ - viewDistance;
+        for (int x = minX; x <= maxX; ++x) {
+            for (int z = minZ; z <= maxZ; ++z) {
+                this.recalculateDistance(x, z);
+            }
+        }
+    }
+
+    @Override
+    protected void updateObjectCallback(final E object, final long oldPosition, final long newPosition, final int oldViewDistance, final int newViewDistance) {
+        if (oldPosition == newPosition && newViewDistance == oldViewDistance) {
+            return;
+        }
+
+        final int toX = MCUtil.getCoordinateX(newPosition);
+        final int toZ = MCUtil.getCoordinateZ(newPosition);
+        final int fromX = MCUtil.getCoordinateX(oldPosition);
+        final int fromZ = MCUtil.getCoordinateZ(oldPosition);
+
+        final int totalX = IntegerUtil.branchlessAbs(fromX - toX);
+        final int totalZ = IntegerUtil.branchlessAbs(fromZ - toZ);
+
+        if (Math.max(totalX, totalZ) > (2 * Math.max(newViewDistance, oldViewDistance))) {
+            // teleported?
+            this.removeObjectCallback(object, fromX, fromZ, oldViewDistance);
+            this.addObjectCallback(object, toX, toZ, newViewDistance);
+            return;
+        }
+
+        final int minX = Math.min(fromX - oldViewDistance, toX - newViewDistance);
+        final int maxX = Math.max(fromX + oldViewDistance, toX + newViewDistance);
+        final int minZ = Math.min(fromZ - oldViewDistance, toZ - newViewDistance);
+        final int maxZ = Math.max(fromZ + oldViewDistance, toZ + newViewDistance);
+
+        for (int x = minX; x <= maxX; ++x) {
+            for (int z = minZ; z <= maxZ; ++z) {
+                final int distXOld = IntegerUtil.branchlessAbs(x - fromX);
+                final int distZOld = IntegerUtil.branchlessAbs(z - fromZ);
+
+                if (Math.max(distXOld, distZOld) <= oldViewDistance) {
+                    this.recalculateDistance(x, z);
+                    continue;
+                }
+
+                final int distXNew = IntegerUtil.branchlessAbs(x - toX);
+                final int distZNew = IntegerUtil.branchlessAbs(z - toZ);
+
+                if (Math.max(distXNew, distZNew) <= newViewDistance) {
+                    this.recalculateDistance(x, z);
+                    continue;
+                }
+            }
+        }
+    }
+
+    @FunctionalInterface
+    public static interface DistanceChangeCallback<E> {
+
+        void accept(final int posX, final int posZ, final int oldNearestDistance, final int newNearestDistance,
+                    final PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<E> state);
+
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..b1396f405d041fc3ca1f7ce1e0f884a3cfb8b96e
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerAreaMap.java
@@ -0,0 +1,32 @@
+package com.destroystokyo.paper.util.misc;
+
+import net.minecraft.server.EntityPlayer;
+
+/**
+ * @author Spottedleaf
+ */
+public final class PlayerAreaMap extends AreaMap<EntityPlayer> {
+
+    public PlayerAreaMap() {
+        super();
+    }
+
+    public PlayerAreaMap(final PooledLinkedHashSets<EntityPlayer> pooledHashSets) {
+        super(pooledHashSets);
+    }
+
+    public PlayerAreaMap(final PooledLinkedHashSets<EntityPlayer> pooledHashSets, final ChangeCallback<EntityPlayer> addCallback,
+                         final ChangeCallback<EntityPlayer> removeCallback) {
+        this(pooledHashSets, addCallback, removeCallback, null);
+    }
+
+    public PlayerAreaMap(final PooledLinkedHashSets<EntityPlayer> pooledHashSets, final ChangeCallback<EntityPlayer> addCallback,
+                         final ChangeCallback<EntityPlayer> removeCallback, final ChangeSourceCallback<EntityPlayer> changeSourceCallback) {
+        super(pooledHashSets, addCallback, removeCallback, changeSourceCallback);
+    }
+
+    @Override
+    protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> getEmptySetFor(final EntityPlayer player) {
+        return player.cachedSingleHashSet;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java
new file mode 100644
index 0000000000000000000000000000000000000000..0292afc5224326b767bd56d0718c215c184f2e0f
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/misc/PlayerDistanceTrackingAreaMap.java
@@ -0,0 +1,24 @@
+package com.destroystokyo.paper.util.misc;
+
+import net.minecraft.server.EntityPlayer;
+
+public class PlayerDistanceTrackingAreaMap extends DistanceTrackingAreaMap<EntityPlayer> {
+
+    public PlayerDistanceTrackingAreaMap() {
+        super();
+    }
+
+    public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<EntityPlayer> pooledHashSets) {
+        super(pooledHashSets);
+    }
+
+    public PlayerDistanceTrackingAreaMap(final PooledLinkedHashSets<EntityPlayer> pooledHashSets, final ChangeCallback<EntityPlayer> addCallback,
+                                         final ChangeCallback<EntityPlayer> removeCallback, final DistanceChangeCallback<EntityPlayer> distanceChangeCallback) {
+        super(pooledHashSets, addCallback, removeCallback, distanceChangeCallback);
+    }
+
+    @Override
+    protected PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> getEmptySetFor(final EntityPlayer player) {
+        return player.cachedSingleHashSet;
+    }
+}
diff --git a/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java
new file mode 100644
index 0000000000000000000000000000000000000000..e51104e65a07b6ea7bbbcbb6afb066ef6401cc5b
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/misc/PooledLinkedHashSets.java
@@ -0,0 +1,287 @@
+package com.destroystokyo.paper.util.misc;
+
+import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
+import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
+import java.lang.ref.WeakReference;
+
+/** @author Spottedleaf */
+public class PooledLinkedHashSets<E> {
+
+    /* Tested via https://gist.github.com/Spottedleaf/a93bb7a8993d6ce142d3efc5932bf573 */
+
+    // we really want to avoid that equals() check as much as possible...
+    protected final Object2ObjectOpenHashMap<PooledObjectLinkedOpenHashSet<E>, PooledObjectLinkedOpenHashSet<E>> mapPool = new Object2ObjectOpenHashMap<>(128, 0.25f);
+
+    protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet<E> current) {
+        if (current.referenceCount == 0) {
+            throw new IllegalStateException("Cannot decrement reference count for " + current);
+        }
+        if (current.referenceCount == -1 || --current.referenceCount > 0) {
+            return;
+        }
+
+        this.mapPool.remove(current);
+        return;
+    }
+
+    public PooledObjectLinkedOpenHashSet<E> findMapWith(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
+        final PooledObjectLinkedOpenHashSet<E> cached = current.getAddCache(object);
+
+        if (cached != null) {
+            decrementReferenceCount(current);
+
+            if (cached.referenceCount == 0) {
+                // bring the map back from the dead
+                PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached);
+                if (contending != null) {
+                    // a map already exists with the elements we want
+                    if (contending.referenceCount != -1) {
+                        ++contending.referenceCount;
+                    }
+                    current.updateAddCache(object, contending);
+                    return contending;
+                }
+
+                cached.referenceCount = 1;
+            } else if (cached.referenceCount != -1) {
+                ++cached.referenceCount;
+            }
+
+            return cached;
+        }
+
+        if (!current.add(object)) {
+            return current;
+        }
+
+        // we use get/put since we use a different key on put
+        PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
+
+        if (ret == null) {
+            ret = new PooledObjectLinkedOpenHashSet<>(current);
+            current.remove(object);
+            this.mapPool.put(ret, ret);
+            ret.referenceCount = 1;
+        } else {
+            if (ret.referenceCount != -1) {
+                ++ret.referenceCount;
+            }
+            current.remove(object);
+        }
+
+        current.updateAddCache(object, ret);
+
+        decrementReferenceCount(current);
+        return ret;
+    }
+
+    // rets null if current.size() == 1
+    public PooledObjectLinkedOpenHashSet<E> findMapWithout(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
+        if (current.set.size() == 1) {
+            decrementReferenceCount(current);
+            return null;
+        }
+
+        final PooledObjectLinkedOpenHashSet<E> cached = current.getRemoveCache(object);
+
+        if (cached != null) {
+            decrementReferenceCount(current);
+
+            if (cached.referenceCount == 0) {
+                // bring the map back from the dead
+                PooledObjectLinkedOpenHashSet<E> contending = this.mapPool.putIfAbsent(cached, cached);
+                if (contending != null) {
+                    // a map already exists with the elements we want
+                    if (contending.referenceCount != -1) {
+                        ++contending.referenceCount;
+                    }
+                    current.updateRemoveCache(object, contending);
+                    return contending;
+                }
+
+                cached.referenceCount = 1;
+            } else if (cached.referenceCount != -1) {
+                ++cached.referenceCount;
+            }
+
+            return cached;
+        }
+
+        if (!current.remove(object)) {
+            return current;
+        }
+
+        // we use get/put since we use a different key on put
+        PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
+
+        if (ret == null) {
+            ret = new PooledObjectLinkedOpenHashSet<>(current);
+            current.add(object);
+            this.mapPool.put(ret, ret);
+            ret.referenceCount = 1;
+        } else {
+            if (ret.referenceCount != -1) {
+                ++ret.referenceCount;
+            }
+            current.add(object);
+        }
+
+        current.updateRemoveCache(object, ret);
+
+        decrementReferenceCount(current);
+        return ret;
+    }
+
+    static final class RawSetObjectLinkedOpenHashSet<E> extends ObjectOpenHashSet<E> {
+
+        public RawSetObjectLinkedOpenHashSet() {
+            super();
+        }
+
+        public RawSetObjectLinkedOpenHashSet(final int capacity) {
+            super(capacity);
+        }
+
+        public RawSetObjectLinkedOpenHashSet(final int capacity, final float loadFactor) {
+            super(capacity, loadFactor);
+        }
+
+        @Override
+        public RawSetObjectLinkedOpenHashSet<E> clone() {
+            return (RawSetObjectLinkedOpenHashSet<E>)super.clone();
+        }
+
+        public E[] getRawSet() {
+            return this.key;
+        }
+    }
+
+    public static final class PooledObjectLinkedOpenHashSet<E> {
+
+        private static final WeakReference NULL_REFERENCE = new WeakReference<>(null);
+
+        final RawSetObjectLinkedOpenHashSet<E> set;
+        int referenceCount; // -1 if special
+        int hash; // optimize hashcode
+
+        // add cache
+        WeakReference<E> lastAddObject = NULL_REFERENCE;
+        WeakReference<PooledObjectLinkedOpenHashSet<E>> lastAddMap = NULL_REFERENCE;
+
+        // remove cache
+        WeakReference<E> lastRemoveObject = NULL_REFERENCE;
+        WeakReference<PooledObjectLinkedOpenHashSet<E>> lastRemoveMap = NULL_REFERENCE;
+
+        public PooledObjectLinkedOpenHashSet(final PooledLinkedHashSets<E> pooledSets) {
+            this.set = new RawSetObjectLinkedOpenHashSet<>(2, 0.8f);
+        }
+
+        public PooledObjectLinkedOpenHashSet(final E single) {
+            this((PooledLinkedHashSets<E>)null);
+            this.referenceCount = -1;
+            this.add(single);
+        }
+
+        public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet<E> other) {
+            this.set = other.set.clone();
+            this.hash = other.hash;
+        }
+
+        // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
+        // generated by https://github.com/skeeto/hash-prospector
+        private static int hash0(int x) {
+            x *= 0x36935555;
+            x ^= x >>> 16;
+            return x;
+        }
+
+        PooledObjectLinkedOpenHashSet<E> getAddCache(final E element) {
+            final E currentAdd = this.lastAddObject.get();
+
+            if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) {
+                return null;
+            }
+
+            return this.lastAddMap.get();
+        }
+
+        PooledObjectLinkedOpenHashSet<E> getRemoveCache(final E element) {
+            final E currentRemove = this.lastRemoveObject.get();
+
+            if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) {
+                return null;
+            }
+
+            return this.lastRemoveMap.get();
+        }
+
+        void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
+            this.lastAddObject = new WeakReference<>(element);
+            this.lastAddMap = new WeakReference<>(map);
+        }
+
+        void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
+            this.lastRemoveObject = new WeakReference<>(element);
+            this.lastRemoveMap = new WeakReference<>(map);
+        }
+
+        boolean add(final E element) {
+            boolean added =  this.set.add(element);
+
+            if (added) {
+                this.hash += hash0(element.hashCode());
+            }
+
+            return added;
+        }
+
+        boolean remove(Object element) {
+            boolean removed = this.set.remove(element);
+
+            if (removed) {
+                this.hash -= hash0(element.hashCode());
+            }
+
+            return removed;
+        }
+
+        public boolean contains(final Object element) {
+            return this.set.contains(element);
+        }
+
+        public E[] getBackingSet() {
+            return this.set.getRawSet();
+        }
+
+        public int size() {
+            return this.set.size();
+        }
+
+        @Override
+        public int hashCode() {
+            return this.hash;
+        }
+
+        @Override
+        public boolean equals(final Object other) {
+            if (!(other instanceof PooledObjectLinkedOpenHashSet)) {
+                return false;
+            }
+            if (this.referenceCount == 0) {
+                return other == this;
+            } else {
+                if (other == this) {
+                    // Unfortunately we are never equal to our own instance while in use!
+                    return false;
+                }
+                return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set);
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " +
+                this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString();
+        }
+    }
+}
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..d0c77068e9a53d1b8bbad0f3f6b420d6bc85f8c8
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/pooled/PooledObjects.java
@@ -0,0 +1,85 @@
+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.function.Consumer;
+import java.util.function.Supplier;
+
+public final class PooledObjects<E> {
+
+    /**
+     * 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<MutableInt> POOLED_MUTABLE_INTEGERS = new PooledObjects<>(MutableInt::new, 1024);
+
+    private final Supplier<E> creator;
+    private final Consumer<E> releaser;
+    private final int maxPoolSize;
+    private final ArrayDeque<E> queue;
+
+    public PooledObjects(final Supplier<E> creator, int maxPoolSize) {
+        this(creator, maxPoolSize, null);
+    }
+    public PooledObjects(final Supplier<E> creator, int maxPoolSize, Consumer<E> releaser) {
+        if (creator == null) {
+            throw new NullPointerException("Creator must not be null");
+        }
+        if (maxPoolSize <= 0) {
+            throw new IllegalArgumentException("Max pool size must be greater-than 0");
+        }
+
+        this.queue = new ArrayDeque<>(maxPoolSize);
+        this.maxPoolSize = maxPoolSize;
+        this.creator = creator;
+        this.releaser = releaser;
+    }
+
+    public AutoReleased acquireCleaner(Object holder) {
+        return acquireCleaner(holder, this::release);
+    }
+
+    public AutoReleased acquireCleaner(Object holder, Consumer<E> releaser) {
+        E resource = acquire();
+        Runnable cleaner = MCUtil.registerCleaner(holder, resource, releaser);
+        return new AutoReleased(resource, cleaner);
+    }
+
+    public final E acquire() {
+        E value;
+        synchronized (queue) {
+            value = this.queue.pollLast();
+        }
+        return value != null ? value : this.creator.get();
+    }
+
+    public final void release(final E value) {
+        if (this.releaser != null) {
+            this.releaser.accept(value);
+        }
+        synchronized (this.queue) {
+            if (queue.size() < this.maxPoolSize) {
+                this.queue.addLast(value);
+            }
+        }
+    }
+}
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
index 0000000000000000000000000000000000000000..9df0006c1a283f77c4d01d9fce9062fc1c9bbb1f
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/set/OptimizedSmallEnumSet.java
@@ -0,0 +1,67 @@
+package com.destroystokyo.paper.util.set;
+
+import java.util.Collection;
+
+/**
+ * @author Spottedleaf <Spottedleaf@users.noreply.github.com>
+ */
+public final class OptimizedSmallEnumSet<E extends Enum<E>> {
+
+    private final Class<E> enumClass;
+    private long backingSet;
+
+    public OptimizedSmallEnumSet(final Class<E> clazz) {
+        if (clazz == null) {
+            throw new IllegalArgumentException("Null class");
+        }
+        if (!clazz.isEnum()) {
+            throw new IllegalArgumentException("Class must be enum, not " + clazz.getCanonicalName());
+        }
+        this.enumClass = clazz;
+    }
+
+    public boolean addUnchecked(final E element) {
+        final int ordinal = element.ordinal();
+        final long key = 1L << ordinal;
+
+        final long prev = this.backingSet;
+        this.backingSet = prev | key;
+
+        return (prev & key) == 0;
+    }
+
+    public boolean removeUnchecked(final E element) {
+        final int ordinal = element.ordinal();
+        final long key = 1L << ordinal;
+
+        final long prev = this.backingSet;
+        this.backingSet = prev & ~key;
+
+        return (prev & key) != 0;
+    }
+
+    public void clear() {
+        this.backingSet = 0L;
+    }
+
+    public int size() {
+        return Long.bitCount(this.backingSet);
+    }
+
+    public void addAllUnchecked(final Collection<E> enums) {
+        for (final E element : enums) {
+            if (element == null) {
+                throw new NullPointerException("Null element");
+            }
+            this.backingSet |= (1L << element.ordinal());
+        }
+    }
+
+    public long getBackingSet() {
+        return this.backingSet;
+    }
+
+    public boolean hasCommonElements(final OptimizedSmallEnumSet<E> other) {
+        return (other.backingSet & this.backingSet) != 0;
+    }
+}
diff --git a/src/main/java/net/minecraft/server/AxisAlignedBB.java b/src/main/java/net/minecraft/server/AxisAlignedBB.java
index 277d7a124e9a21803fe4d5a66fc0b311df2cfba7..02c09f39848399a86d46bd17569b4f01a7b5ab1f 100644
--- a/src/main/java/net/minecraft/server/AxisAlignedBB.java
+++ b/src/main/java/net/minecraft/server/AxisAlignedBB.java
@@ -190,10 +190,12 @@ public class AxisAlignedBB {
         return this.d(vec3d.x, vec3d.y, vec3d.z);
     }
 
+    public final boolean intersects(AxisAlignedBB axisalignedbb) { return this.c(axisalignedbb); } // Paper - OBFHELPER
     public boolean c(AxisAlignedBB axisalignedbb) {
         return this.a(axisalignedbb.minX, axisalignedbb.minY, axisalignedbb.minZ, axisalignedbb.maxX, axisalignedbb.maxY, axisalignedbb.maxZ);
     }
 
+    public final boolean intersects(double d0, double d1, double d2, double d3, double d4, double d5) { return a(d0, d1, d2, d3, d4, d5); } // Paper - OBFHELPER
     public boolean a(double d0, double d1, double d2, double d3, double d4, double d5) {
         return this.minX < d3 && this.maxX > d0 && this.minY < d4 && this.maxY > d1 && this.minZ < d5 && this.maxZ > d2;
     }
@@ -206,6 +208,7 @@ public class AxisAlignedBB {
         return d0 >= this.minX && d0 < this.maxX && d1 >= this.minY && d1 < this.maxY && d2 >= this.minZ && d2 < this.maxZ;
     }
 
+    public final double getAverageSideLength(){return a();} // Paper - OBFHELPER
     public double a() {
         double d0 = this.b();
         double d1 = this.c();
diff --git a/src/main/java/net/minecraft/server/BaseBlockPosition.java b/src/main/java/net/minecraft/server/BaseBlockPosition.java
index ff4b23927bb0e0ac8221d71fe2543cbee54f913a..ee28d0335418a0053f8448ab5e12ebba5a9a3b2d 100644
--- a/src/main/java/net/minecraft/server/BaseBlockPosition.java
+++ b/src/main/java/net/minecraft/server/BaseBlockPosition.java
@@ -98,6 +98,7 @@ public class BaseBlockPosition implements Comparable<BaseBlockPosition> {
         return this.distanceSquared(iposition.getX(), iposition.getY(), iposition.getZ(), true) < d0 * d0;
     }
 
+    public final double distanceSquared(BaseBlockPosition baseblockposition) { return j(baseblockposition); } // Paper - OBFHELPER
     public double j(BaseBlockPosition baseblockposition) {
         return this.distanceSquared((double) baseblockposition.getX(), (double) baseblockposition.getY(), (double) baseblockposition.getZ(), true);
     }
diff --git a/src/main/java/net/minecraft/server/BlockAccessAir.java b/src/main/java/net/minecraft/server/BlockAccessAir.java
index eff6ebcd30b538cbaedaa031a46a59ea956253ba..30cbfc8eac20910aa55951e3dce63862f5a43c37 100644
--- a/src/main/java/net/minecraft/server/BlockAccessAir.java
+++ b/src/main/java/net/minecraft/server/BlockAccessAir.java
@@ -14,6 +14,18 @@ public enum BlockAccessAir implements IBlockAccess {
         return null;
     }
 
+    // Paper start - If loaded util
+    @Override
+    public Fluid getFluidIfLoaded(BlockPosition blockposition) {
+        return this.getFluid(blockposition);
+    }
+
+    @Override
+    public IBlockData getTypeIfLoaded(BlockPosition blockposition) {
+        return this.getType(blockposition);
+    }
+    // Paper end
+
     @Override
     public IBlockData getType(BlockPosition blockposition) {
         return Blocks.AIR.getBlockData();
diff --git a/src/main/java/net/minecraft/server/BlockBase.java b/src/main/java/net/minecraft/server/BlockBase.java
index 47324feca49786b49563d3d0e854e74ad27c190b..26d446077bb563ca3c5bb0339695b3364a3e41bf 100644
--- a/src/main/java/net/minecraft/server/BlockBase.java
+++ b/src/main/java/net/minecraft/server/BlockBase.java
@@ -632,6 +632,7 @@ public abstract class BlockBase {
             return this.a != null ? this.a.e : Block.a(this.getCollisionShape(iblockaccess, blockposition));
         }
 
+        public IBlockData getBlockData() { return p(); } // Paper - OBFHELPER
         protected abstract IBlockData p();
 
         public boolean isAlwaysDestroyable() {
diff --git a/src/main/java/net/minecraft/server/BlockPosition.java b/src/main/java/net/minecraft/server/BlockPosition.java
index 886b43e2b8f21c358b4d6785c677f14c91d191f3..f1ad6ad22a59b28d2e8aeb2c0f4c21bce6070bc5 100644
--- a/src/main/java/net/minecraft/server/BlockPosition.java
+++ b/src/main/java/net/minecraft/server/BlockPosition.java
@@ -98,6 +98,7 @@ public class BlockPosition extends BaseBlockPosition {
         return d0 == 0.0D && d1 == 0.0D && d2 == 0.0D ? this : new BlockPosition((double) this.getX() + d0, (double) this.getY() + d1, (double) this.getZ() + d2);
     }
 
+    public BlockPosition add(int i, int j, int k) {return b(i, j, k);} // Paper - OBFHELPER
     public BlockPosition b(int i, int j, int k) {
         return i == 0 && j == 0 && k == 0 ? this : new BlockPosition(this.getX() + i, this.getY() + j, this.getZ() + k);
     }
@@ -188,6 +189,8 @@ public class BlockPosition extends BaseBlockPosition {
         return new BlockPosition(this.getY() * baseblockposition.getZ() - this.getZ() * baseblockposition.getY(), this.getZ() * baseblockposition.getX() - this.getX() * baseblockposition.getZ(), this.getX() * baseblockposition.getY() - this.getY() * baseblockposition.getX());
     }
 
+    @Deprecated // We'll replace this...
+    public BlockPosition asImmutable() { return immutableCopy(); } // Paper - OBFHELPER
     public BlockPosition immutableCopy() {
         return this;
     }
@@ -368,6 +371,7 @@ public class BlockPosition extends BaseBlockPosition {
             return super.a(enumblockrotation).immutableCopy();
         }
 
+        public BlockPosition.MutableBlockPosition setValues(int i, int j, int k) { return d(i, j, k);} // Paper - OBFHELPER
         public BlockPosition.MutableBlockPosition d(int i, int j, int k) {
             this.o(i);
             this.p(j);
@@ -375,6 +379,7 @@ public class BlockPosition extends BaseBlockPosition {
             return this;
         }
 
+        public BlockPosition.MutableBlockPosition setValues(double d0, double d1, double d2) { return c(d0, d1, d2);} // Paper - OBFHELPER
         public BlockPosition.MutableBlockPosition c(double d0, double d1, double d2) {
             return this.d(MathHelper.floor(d0), MathHelper.floor(d1), MathHelper.floor(d2));
         }
@@ -424,16 +429,19 @@ public class BlockPosition extends BaseBlockPosition {
             }
         }
 
+        public final void setX(final int x) { super.o(x); } // Paper - OBFHELPER
         @Override
         public void o(int i) {
             super.o(i);
         }
 
+        public final void setY(final int y) { super.p(y); } // Paper - OBFHELPER
         @Override
         public void p(int i) {
             super.p(i);
         }
 
+        public final void setZ(final int z) { super.q(z); } // Paper - OBFHELPER
         @Override
         public void q(int i) {
             super.q(i);
@@ -444,4 +452,13 @@ public class BlockPosition extends BaseBlockPosition {
             return new BlockPosition(this);
         }
     }
+
+    // Paper start
+    public static class PooledBlockPosition extends BlockPosition.MutableBlockPosition implements AutoCloseable {
+        @Deprecated
+        public void close() {}
+        @Deprecated
+        public static BlockPosition.PooledBlockPosition acquire() { return new PooledBlockPosition(); }
+    }
+    // Paper end
 }
diff --git a/src/main/java/net/minecraft/server/Chunk.java b/src/main/java/net/minecraft/server/Chunk.java
index 3cdcdc60df4f28197cf19c59ea42a92ae4af3819..7c7826cf3adb19814984ab627e4c4726d8933244 100644
--- a/src/main/java/net/minecraft/server/Chunk.java
+++ b/src/main/java/net/minecraft/server/Chunk.java
@@ -26,7 +26,7 @@ public class Chunk implements IChunkAccess {
 
     private static final Logger LOGGER = LogManager.getLogger();
     @Nullable
-    public static final ChunkSection a = null;
+    public static final ChunkSection a = null; public static final ChunkSection EMPTY_CHUNK_SECTION = a; // Paper - OBFHELPER
     private final ChunkSection[] sections;
     private BiomeStorage d;
     private final Map<BlockPosition, NBTTagCompound> e;
@@ -49,7 +49,7 @@ public class Chunk implements IChunkAccess {
     private Supplier<PlayerChunk.State> u;
     @Nullable
     private Consumer<Chunk> v;
-    private final ChunkCoordIntPair loc;
+    private final ChunkCoordIntPair loc; public final long coordinateKey; // Paper - cache coordinate key
     private volatile boolean x;
 
     public Chunk(World world, ChunkCoordIntPair chunkcoordintpair, BiomeStorage biomestorage) {
@@ -66,7 +66,7 @@ public class Chunk implements IChunkAccess {
         this.n = new ShortList[16];
         this.entitySlices = (List[]) (new List[16]); // Spigot
         this.world = (WorldServer) world; // CraftBukkit - type
-        this.loc = chunkcoordintpair;
+        this.loc = chunkcoordintpair; this.coordinateKey = MCUtil.getCoordinateKey(chunkcoordintpair); // Paper - cache coordinate key
         this.i = chunkconverter;
         HeightMap.Type[] aheightmap_type = HeightMap.Type.values();
         int j = aheightmap_type.length;
@@ -109,6 +109,110 @@ public class Chunk implements IChunkAccess {
     public boolean needsDecoration;
     // CraftBukkit end
 
+    // Paper start
+    public final com.destroystokyo.paper.util.maplist.EntityList entities = new com.destroystokyo.paper.util.maplist.EntityList();
+    public PlayerChunk playerChunk;
+
+    static final int NEIGHBOUR_CACHE_RADIUS = 3;
+    public static int getNeighbourCacheRadius() {
+        return NEIGHBOUR_CACHE_RADIUS;
+    }
+
+    boolean loadedTicketLevel;
+    private long neighbourChunksLoadedBitset;
+    private final Chunk[] loadedNeighbourChunks = new Chunk[(NEIGHBOUR_CACHE_RADIUS * 2 + 1) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)];
+
+    private static int getNeighbourIndex(final int relativeX, final int relativeZ) {
+        // index = (relativeX + NEIGHBOUR_CACHE_RADIUS) + (relativeZ + NEIGHBOUR_CACHE_RADIUS) * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)
+        // optimised variant of the above by moving some of the ops to compile time
+        return relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1)));
+    }
+
+    public final Chunk getRelativeNeighbourIfLoaded(final int relativeX, final int relativeZ) {
+        return this.loadedNeighbourChunks[getNeighbourIndex(relativeX, relativeZ)];
+    }
+
+    public final boolean isNeighbourLoaded(final int relativeX, final int relativeZ) {
+        return (this.neighbourChunksLoadedBitset & (1L << getNeighbourIndex(relativeX, relativeZ))) != 0;
+    }
+
+    public final void setNeighbourLoaded(final int relativeX, final int relativeZ, final Chunk chunk) {
+        if (chunk == null) {
+            throw new IllegalArgumentException("Chunk must be non-null, neighbour: (" + relativeX + "," + relativeZ + "), chunk: " + this.loc);
+        }
+        final long before = this.neighbourChunksLoadedBitset;
+        final int index = getNeighbourIndex(relativeX, relativeZ);
+        this.loadedNeighbourChunks[index] = chunk;
+        this.neighbourChunksLoadedBitset |= (1L << index);
+        this.onNeighbourChange(before, this.neighbourChunksLoadedBitset);
+    }
+
+    public final void setNeighbourUnloaded(final int relativeX, final int relativeZ) {
+        final long before = this.neighbourChunksLoadedBitset;
+        final int index = getNeighbourIndex(relativeX, relativeZ);
+        this.loadedNeighbourChunks[index] = null;
+        this.neighbourChunksLoadedBitset &= ~(1L << index);
+        this.onNeighbourChange(before, this.neighbourChunksLoadedBitset);
+    }
+
+    public final void resetNeighbours() {
+        final long before = this.neighbourChunksLoadedBitset;
+        this.neighbourChunksLoadedBitset = 0L;
+        java.util.Arrays.fill(this.loadedNeighbourChunks, null);
+        this.onNeighbourChange(before, 0L);
+    }
+
+    protected void onNeighbourChange(final long bitsetBefore, final long bitsetAfter) {
+
+    }
+
+    public final boolean isAnyNeighborsLoaded() {
+        return neighbourChunksLoadedBitset != 0;
+    }
+    public final boolean areNeighboursLoaded(final int radius) {
+        return Chunk.areNeighboursLoaded(this.neighbourChunksLoadedBitset, radius);
+    }
+
+    public static boolean areNeighboursLoaded(final long bitset, final int radius) {
+        // index = relativeX + (relativeZ * (NEIGHBOUR_CACHE_RADIUS * 2 + 1)) + (NEIGHBOUR_CACHE_RADIUS + NEIGHBOUR_CACHE_RADIUS * ((NEIGHBOUR_CACHE_RADIUS * 2 + 1)))
+        switch (radius) {
+            case 0: {
+                return (bitset & (1L << getNeighbourIndex(0, 0))) != 0;
+            }
+            case 1: {
+                long mask = 0L;
+                for (int dx = -1; dx <= 1; ++dx) {
+                    for (int dz = -1; dz <= 1; ++dz) {
+                        mask |= (1L << getNeighbourIndex(dx, dz));
+                    }
+                }
+                return (bitset & mask) == mask;
+            }
+            case 2: {
+                long mask = 0L;
+                for (int dx = -2; dx <= 2; ++dx) {
+                    for (int dz = -2; dz <= 2; ++dz) {
+                        mask |= (1L << getNeighbourIndex(dx, dz));
+                    }
+                }
+                return (bitset & mask) == mask;
+            }
+            case 3: {
+                long mask = 0L;
+                for (int dx = -3; dx <= 3; ++dx) {
+                    for (int dz = -3; dz <= 3; ++dz) {
+                        mask |= (1L << getNeighbourIndex(dx, dz));
+                    }
+                }
+                return (bitset & mask) == mask;
+            }
+
+            default:
+                throw new IllegalArgumentException("Radius not recognized: " + radius);
+        }
+    }
+    // Paper end
+
     public Chunk(World world, ProtoChunk protochunk) {
         this(world, protochunk.getPos(), protochunk.getBiomeIndex(), protochunk.p(), protochunk.n(), protochunk.o(), protochunk.getInhabitedTime(), protochunk.getSections(), (Consumer) null);
         Iterator iterator = protochunk.y().iterator();
@@ -214,6 +318,18 @@ public class Chunk implements IChunkAccess {
         }
     }
 
+    // Paper start - If loaded util
+    @Override
+    public Fluid getFluidIfLoaded(BlockPosition blockposition) {
+        return this.getFluid(blockposition);
+    }
+
+    @Override
+    public IBlockData getTypeIfLoaded(BlockPosition blockposition) {
+        return this.getType(blockposition);
+    }
+    // Paper end
+
     @Override
     public Fluid getFluid(BlockPosition blockposition) {
         return this.a(blockposition.getX(), blockposition.getY(), blockposition.getZ());
@@ -353,6 +469,7 @@ public class Chunk implements IChunkAccess {
         entity.chunkX = this.loc.x;
         entity.chunkY = k;
         entity.chunkZ = this.loc.z;
+        this.entities.add(entity); // Paper - per chunk entity list
         this.entitySlices[k].add(entity);
     }
 
@@ -375,6 +492,7 @@ public class Chunk implements IChunkAccess {
         }
 
         this.entitySlices[i].remove(entity);
+        this.entities.remove(entity); // Paper
     }
 
     @Override
@@ -396,6 +514,7 @@ public class Chunk implements IChunkAccess {
         return this.a(blockposition, Chunk.EnumTileEntityState.CHECK);
     }
 
+    @Nullable public final TileEntity getTileEntityImmediately(BlockPosition pos) { return this.a(pos, EnumTileEntityState.IMMEDIATE); } // Paper - OBFHELPER
     @Nullable
     public TileEntity a(BlockPosition blockposition, Chunk.EnumTileEntityState chunk_enumtileentitystate) {
         // CraftBukkit start
@@ -507,7 +626,25 @@ public class Chunk implements IChunkAccess {
 
     // CraftBukkit start
     public void loadCallback() {
+        // Paper start - neighbour cache
+        int chunkX = this.loc.x;
+        int chunkZ = this.loc.z;
+        ChunkProviderServer chunkProvider = ((WorldServer)this.world).getChunkProvider();
+        for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) {
+            for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) {
+                Chunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz);
+                if (neighbour != null) {
+                    neighbour.setNeighbourLoaded(-dx, -dz, this);
+                    // should be in cached already
+                    this.setNeighbourLoaded(dx, dz, neighbour);
+                }
+            }
+        }
+        this.setNeighbourLoaded(0, 0, this);
+        this.loadedTicketLevel = true;
+        // Paper end - neighbour cache
         org.bukkit.Server server = this.world.getServer();
+        ((WorldServer)this.world).getChunkProvider().addLoadedChunk(this); // Paper
         if (server != null) {
             /*
              * If it's a new world, the first few chunks are generated inside
@@ -546,6 +683,22 @@ public class Chunk implements IChunkAccess {
         server.getPluginManager().callEvent(unloadEvent);
         // note: saving can be prevented, but not forced if no saving is actually required
         this.mustNotSave = !unloadEvent.isSaveChunk();
+        ((WorldServer)this.world).getChunkProvider().removeLoadedChunk(this); // Paper
+        // Paper start - neighbour cache
+        int chunkX = this.loc.x;
+        int chunkZ = this.loc.z;
+        ChunkProviderServer chunkProvider = ((WorldServer)this.world).getChunkProvider();
+        for (int dx = -NEIGHBOUR_CACHE_RADIUS; dx <= NEIGHBOUR_CACHE_RADIUS; ++dx) {
+            for (int dz = -NEIGHBOUR_CACHE_RADIUS; dz <= NEIGHBOUR_CACHE_RADIUS; ++dz) {
+                Chunk neighbour = chunkProvider.getChunkAtIfLoadedMainThreadNoCache(chunkX + dx, chunkZ + dz);
+                if (neighbour != null) {
+                    neighbour.setNeighbourUnloaded(-dx, -dz);
+                }
+            }
+        }
+        this.loadedTicketLevel = false;
+        this.resetNeighbours();
+        // Paper end
     }
     // CraftBukkit end
 
diff --git a/src/main/java/net/minecraft/server/ChunkCache.java b/src/main/java/net/minecraft/server/ChunkCache.java
index b703382204a3ccd57e642cff18c7c28fef157cc0..8eecdcde510661ec3a13a25a04ba394f6b6dc012 100644
--- a/src/main/java/net/minecraft/server/ChunkCache.java
+++ b/src/main/java/net/minecraft/server/ChunkCache.java
@@ -10,7 +10,7 @@ public class ChunkCache implements IBlockAccess, ICollisionAccess {
     protected final int b;
     protected final IChunkAccess[][] c;
     protected boolean d;
-    protected final World e;
+    protected final World e; protected final World getWorld() { return e; } // Paper - OBFHELPER
 
     public ChunkCache(World world, BlockPosition blockposition, BlockPosition blockposition1) {
         this.e = world;
@@ -29,7 +29,7 @@ public class ChunkCache implements IBlockAccess, ICollisionAccess {
 
         for (k = this.a; k <= i; ++k) {
             for (l = this.b; l <= j; ++l) {
-                this.c[k - this.a][l - this.b] = ichunkprovider.a(k, l);
+                this.c[k - this.a][l - this.b] = ((WorldServer)world).getChunkProvider().getChunkAtIfLoadedMainThreadNoCache(k, l); // Paper
             }
         }
 
@@ -54,7 +54,7 @@ public class ChunkCache implements IBlockAccess, ICollisionAccess {
         int k = i - this.a;
         int l = j - this.b;
 
-        if (k >= 0 && k < this.c.length && l >= 0 && l < this.c[k].length) {
+        if (k >= 0 && k < this.c.length && l >= 0 && l < this.c[k].length) { // Paper - if this changes, update getChunkIfLoaded below
             IChunkAccess ichunkaccess = this.c[k][l];
 
             return (IChunkAccess) (ichunkaccess != null ? ichunkaccess : new ChunkEmpty(this.e, new ChunkCoordIntPair(i, j)));
@@ -73,6 +73,29 @@ public class ChunkCache implements IBlockAccess, ICollisionAccess {
         return this.a(i, j);
     }
 
+    // Paper start - if loaded util
+    private IChunkAccess getChunkIfLoaded(int x, int z) {
+        int k = x - this.a;
+        int l = z - this.b;
+
+        if (k >= 0 && k < this.c.length && l >= 0 && l < this.c[k].length) {
+            return this.c[k][l];
+        }
+        return null;
+    }
+    @Override
+    public Fluid getFluidIfLoaded(BlockPosition blockposition) {
+        IChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+        return chunk == null ? null : chunk.getFluid(blockposition);
+    }
+
+    @Override
+    public IBlockData getTypeIfLoaded(BlockPosition blockposition) {
+        IChunkAccess chunk = getChunkIfLoaded(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+        return chunk == null ? null : chunk.getType(blockposition);
+    }
+    // Paper end
+
     @Nullable
     @Override
     public TileEntity getTileEntity(BlockPosition blockposition) {
diff --git a/src/main/java/net/minecraft/server/ChunkCoordIntPair.java b/src/main/java/net/minecraft/server/ChunkCoordIntPair.java
index 2837823547bdc9655376af3af89c43d84719d513..35b8a85d1280ba3be757b14b14388954ac1617d4 100644
--- a/src/main/java/net/minecraft/server/ChunkCoordIntPair.java
+++ b/src/main/java/net/minecraft/server/ChunkCoordIntPair.java
@@ -11,27 +11,33 @@ public class ChunkCoordIntPair {
     public static final long a = pair(1875016, 1875016);
     public final int x;
     public final int z;
+    public final long longKey; // Paper
 
     public ChunkCoordIntPair(int i, int j) {
         this.x = i;
         this.z = j;
+        this.longKey = pair(this.x, this.z); // Paper
     }
 
     public ChunkCoordIntPair(BlockPosition blockposition) {
         this.x = blockposition.getX() >> 4;
         this.z = blockposition.getZ() >> 4;
+        this.longKey = pair(this.x, this.z); // Paper
     }
 
     public ChunkCoordIntPair(long i) {
         this.x = (int) i;
         this.z = (int) (i >> 32);
+        this.longKey = pair(this.x, this.z); // Paper
     }
 
     public long pair() {
-        return pair(this.x, this.z);
+        return longKey; // Paper
     }
 
-    public static long pair(int i, int j) {
+    public static long asLong(final BlockPosition pos) { return pair(pos.getX() >> 4, pos.getZ() >> 4); } // Paper - OBFHELPER
+    public static long asLong(int x, int z) { return pair(x, z); } // Paper - OBFHELPER
+        public static long pair(int i, int j) {
         return (long) i & 4294967295L | ((long) j & 4294967295L) << 32;
     }
 
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
index 5585b5646b5f3650aa3b795be06f920699a85403..359441cd993a95f933f23aebcec8180f314a5f09 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
@@ -24,7 +24,7 @@ public class ChunkProviderServer extends IChunkProvider {
     private final ChunkMapDistance chunkMapDistance;
     public final ChunkGenerator chunkGenerator;
     private final WorldServer world;
-    private final Thread serverThread;
+    public final Thread serverThread; // Paper - private -> public
     private final LightEngineThreaded lightEngine;
     private final ChunkProviderServer.a serverThreadQueue;
     public final PlayerChunkMap playerChunkMap;
@@ -37,6 +37,167 @@ public class ChunkProviderServer extends IChunkProvider {
     private final IChunkAccess[] cacheChunk = new IChunkAccess[4];
     @Nullable
     private SpawnerCreature.d p;
+    // Paper start
+    final com.destroystokyo.paper.util.concurrent.WeakSeqLock loadedChunkMapSeqLock = new com.destroystokyo.paper.util.concurrent.WeakSeqLock();
+    final it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<Chunk> loadedChunkMap = new it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap<>(8192, 0.5f);
+
+    private final Chunk[] lastLoadedChunks = new Chunk[4 * 4];
+    private final long[] lastLoadedChunkKeys = new long[4 * 4];
+
+    {
+        java.util.Arrays.fill(this.lastLoadedChunkKeys, MCUtil.INVALID_CHUNK_KEY);
+    }
+
+    private static int getCacheKey(int x, int z) {
+        return x & 3 | ((z & 3) << 2);
+    }
+
+    void addLoadedChunk(Chunk chunk) {
+        this.loadedChunkMapSeqLock.acquireWrite();
+        try {
+            this.loadedChunkMap.put(chunk.coordinateKey, chunk);
+        } finally {
+            this.loadedChunkMapSeqLock.releaseWrite();
+        }
+
+        // rewrite cache if we have to
+        // we do this since we also cache null chunks
+        int cacheKey = getCacheKey(chunk.getPos().x, chunk.getPos().z);
+
+        long cachedKey = this.lastLoadedChunkKeys[cacheKey];
+        if (cachedKey == chunk.coordinateKey) {
+            this.lastLoadedChunks[cacheKey] = chunk;
+        }
+    }
+
+    void removeLoadedChunk(Chunk chunk) {
+        this.loadedChunkMapSeqLock.acquireWrite();
+        try {
+            this.loadedChunkMap.remove(chunk.coordinateKey);
+        } finally {
+            this.loadedChunkMapSeqLock.releaseWrite();
+        }
+
+        // rewrite cache if we have to
+        // we do this since we also cache null chunks
+        int cacheKey = getCacheKey(chunk.getPos().x, chunk.getPos().z);
+
+        long cachedKey = this.lastLoadedChunkKeys[cacheKey];
+        if (cachedKey == chunk.coordinateKey) {
+            this.lastLoadedChunks[cacheKey] = null;
+        }
+    }
+
+    public Chunk getChunkAtIfLoadedMainThread(int x, int z) {
+        int cacheKey = getCacheKey(x, z);
+        long chunkKey = MCUtil.getCoordinateKey(x, z);
+
+        long cachedKey = this.lastLoadedChunkKeys[cacheKey];
+        if (cachedKey == chunkKey) {
+            return this.lastLoadedChunks[cacheKey];
+        }
+
+        Chunk ret = this.loadedChunkMap.get(chunkKey);
+
+        this.lastLoadedChunkKeys[cacheKey] = chunkKey;
+        this.lastLoadedChunks[cacheKey] = ret;
+
+        return ret;
+    }
+
+    public Chunk getChunkAtIfLoadedMainThreadNoCache(int x, int z) {
+        return this.loadedChunkMap.get(MCUtil.getCoordinateKey(x, z));
+    }
+
+    public Chunk getChunkAtMainThread(int x, int z) {
+        Chunk ret = this.getChunkAtIfLoadedMainThread(x, z);
+        if (ret != null) {
+            return ret;
+        }
+        return (Chunk)this.getChunkAt(x, z, ChunkStatus.FULL, true);
+    }
+
+    private long chunkFutureAwaitCounter;
+
+    public void getEntityTickingChunkAsync(int x, int z, java.util.function.Consumer<Chunk> onLoad) {
+        if (Thread.currentThread() != this.serverThread) {
+            this.serverThreadQueue.execute(() -> {
+                ChunkProviderServer.this.getEntityTickingChunkAsync(x, z, onLoad);
+            });
+            return;
+        }
+        this.getChunkFutureAsynchronously(x, z, 31, PlayerChunk::getEntityTickingFuture, onLoad);
+    }
+
+    public void getTickingChunkAsync(int x, int z, java.util.function.Consumer<Chunk> onLoad) {
+        if (Thread.currentThread() != this.serverThread) {
+            this.serverThreadQueue.execute(() -> {
+                ChunkProviderServer.this.getTickingChunkAsync(x, z, onLoad);
+            });
+            return;
+        }
+        this.getChunkFutureAsynchronously(x, z, 32, PlayerChunk::getTickingFuture, onLoad);
+    }
+
+    public void getFullChunkAsync(int x, int z, java.util.function.Consumer<Chunk> onLoad) {
+        if (Thread.currentThread() != this.serverThread) {
+            this.serverThreadQueue.execute(() -> {
+                ChunkProviderServer.this.getFullChunkAsync(x, z, onLoad);
+            });
+            return;
+        }
+        this.getChunkFutureAsynchronously(x, z, 33, PlayerChunk::getFullChunkFuture, onLoad);
+    }
+
+    private void getChunkFutureAsynchronously(int x, int z, int ticketLevel, Function<PlayerChunk, CompletableFuture<Either<Chunk, PlayerChunk.Failure>>> futureGet, java.util.function.Consumer<Chunk> onLoad) {
+        if (Thread.currentThread() != this.serverThread) {
+            throw new IllegalStateException();
+        }
+        ChunkCoordIntPair chunkPos = new ChunkCoordIntPair(x, z);
+        Long identifier = Long.valueOf(this.chunkFutureAwaitCounter++);
+        this.chunkMapDistance.addTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
+        this.tickDistanceManager();
+
+        PlayerChunk chunk = this.playerChunkMap.getUpdatingChunk(chunkPos.pair());
+
+        if (chunk == null) {
+            throw new IllegalStateException("Expected playerchunk " + chunkPos + " in world '" + this.world.getWorld().getName() + "'");
+        }
+
+        CompletableFuture<Either<Chunk, PlayerChunk.Failure>> future = futureGet.apply(chunk);
+
+        future.whenCompleteAsync((either, throwable) -> {
+            try {
+                if (throwable != null) {
+                    if (throwable instanceof ThreadDeath) {
+                        throw (ThreadDeath)throwable;
+                    }
+                    MinecraftServer.LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "'", throwable);
+                } else if (either.right().isPresent()) {
+                    MinecraftServer.LOGGER.fatal("Failed to complete future await for chunk " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "': " + either.right().get().toString());
+                }
+
+                try {
+                    if (onLoad != null) {
+                        playerChunkMap.callbackExecutor.execute(() -> {
+                            onLoad.accept(either == null ? null : either.left().orElse(null)); // indicate failure to the callback.
+                        });
+                    }
+                } catch (Throwable thr) {
+                    if (thr instanceof ThreadDeath) {
+                        throw (ThreadDeath)thr;
+                    }
+                    MinecraftServer.LOGGER.fatal("Load callback for future await failed " + chunkPos.toString() + " in world '" + ChunkProviderServer.this.world.getWorld().getName() + "'", thr);
+                    return;
+                }
+            } finally {
+                // due to odd behaviour with CB unload implementation we need to have these AFTER the load callback.
+                ChunkProviderServer.this.chunkMapDistance.addTicketAtLevel(TicketType.UNKNOWN, chunkPos, ticketLevel, chunkPos);
+                ChunkProviderServer.this.chunkMapDistance.removeTicketAtLevel(TicketType.FUTURE_AWAIT, chunkPos, ticketLevel, identifier);
+            }
+        }, this.serverThreadQueue);
+    }
+    // Paper end
 
     public ChunkProviderServer(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, ChunkGenerator chunkgenerator, int i, boolean flag, WorldLoadListener worldloadlistener, Supplier<WorldPersistentData> supplier) {
         this.world = worldserver;
@@ -90,6 +251,49 @@ public class ChunkProviderServer extends IChunkProvider {
         this.cacheChunk[0] = ichunkaccess;
     }
 
+    // Paper start - "real" get chunk if loaded
+    // Note: Partially copied from the getChunkAt method below
+    @Nullable
+    public Chunk getChunkAtIfCachedImmediately(int x, int z) {
+        long k = ChunkCoordIntPair.pair(x, z);
+
+        // Note: Bypass cache since we need to check ticket level, and to make this MT-Safe
+
+        PlayerChunk playerChunk = this.getChunk(k);
+        if (playerChunk == null) {
+            return null;
+        }
+
+        return playerChunk.getFullChunkIfCached();
+    }
+
+    @Nullable
+    public Chunk getChunkAtIfLoadedImmediately(int x, int z) {
+        long k = ChunkCoordIntPair.pair(x, z);
+
+        if (Thread.currentThread() == this.serverThread) {
+            return this.getChunkAtIfLoadedMainThread(x, z);
+        }
+
+        Chunk ret = null;
+        long readlock;
+        do {
+            readlock = this.loadedChunkMapSeqLock.acquireRead();
+            try {
+                ret = this.loadedChunkMap.get(k);
+            } catch (Throwable thr) {
+                if (thr instanceof ThreadDeath) {
+                    throw (ThreadDeath)thr;
+                }
+                // re-try, this means a CME occurred...
+                continue;
+            }
+        } while (!this.loadedChunkMapSeqLock.tryReleaseRead(readlock));
+
+        return ret;
+    }
+    // Paper end
+
     @Nullable
     @Override
     public IChunkAccess getChunkAt(int i, int j, ChunkStatus chunkstatus, boolean flag) {
@@ -372,10 +576,9 @@ public class ChunkProviderServer extends IChunkProvider {
 
             this.p = spawnercreature_d;
             this.world.getMethodProfiler().exit();
-            List<PlayerChunk> list = Lists.newArrayList(this.playerChunkMap.f());
-
-            Collections.shuffle(list);
-            list.forEach((playerchunk) -> {
+            //List<PlayerChunk> list = Lists.newArrayList(this.playerChunkMap.f()); // Paper
+            //Collections.shuffle(list); // Paper
+            this.playerChunkMap.f().forEach((playerchunk) -> { // Paper - no... just no...
                 Optional<Chunk> optional = ((Either) playerchunk.a().getNow(PlayerChunk.UNLOADED_CHUNK)).left();
 
                 if (optional.isPresent()) {
diff --git a/src/main/java/net/minecraft/server/ChunkSection.java b/src/main/java/net/minecraft/server/ChunkSection.java
index b0f7ea97d4795655b6c30b296fd929806dac4ef1..882c2733beaff1df68b892d44fc77cacf4364ff4 100644
--- a/src/main/java/net/minecraft/server/ChunkSection.java
+++ b/src/main/java/net/minecraft/server/ChunkSection.java
@@ -133,6 +133,7 @@ public class ChunkSection {
         return this.blockIds;
     }
 
+    public void writeChunkSection(PacketDataSerializer packetDataSerializer) { this.b(packetDataSerializer); } // Paper - OBFHELPER
     public void b(PacketDataSerializer packetdataserializer) {
         packetdataserializer.writeShort(this.nonEmptyBlockCount);
         this.blockIds.b(packetdataserializer);
diff --git a/src/main/java/net/minecraft/server/DataBits.java b/src/main/java/net/minecraft/server/DataBits.java
index cd572c76522b2ee900a03fd5cf6753f3297c1ccd..2c3580c1c7bcd6afc83a45550c0f672a592e0c38 100644
--- a/src/main/java/net/minecraft/server/DataBits.java
+++ b/src/main/java/net/minecraft/server/DataBits.java
@@ -83,6 +83,7 @@ public class DataBits {
         return (int) (k >> l & this.d);
     }
 
+    public long[] getDataBits() { return this.a(); } // Paper - OBFHELPER
     public long[] a() {
         return this.b;
     }
diff --git a/src/main/java/net/minecraft/server/DataPalette.java b/src/main/java/net/minecraft/server/DataPalette.java
index b7f4330bbe3b51e6792043cbd0c46c73aad457cb..75b721933ccbe8edc1cd7ea5cc4562214e26b66d 100644
--- a/src/main/java/net/minecraft/server/DataPalette.java
+++ b/src/main/java/net/minecraft/server/DataPalette.java
@@ -5,10 +5,12 @@ import javax.annotation.Nullable;
 
 public interface DataPalette<T> {
 
+    default int getOrCreateIdFor(T object) { return this.a(object); } // Paper - OBFHELPER
     int a(T t0);
 
     boolean a(Predicate<T> predicate);
 
+    @Nullable default T getObject(int dataBits) { return this.a(dataBits); } // Paper - OBFHELPER
     @Nullable
     T a(int i);
 
diff --git a/src/main/java/net/minecraft/server/DataPaletteBlock.java b/src/main/java/net/minecraft/server/DataPaletteBlock.java
index 8856981da86219bdb036aa6246152f382ff8a818..4c6979903d287f7f37d9029f6ce2551742f26164 100644
--- a/src/main/java/net/minecraft/server/DataPaletteBlock.java
+++ b/src/main/java/net/minecraft/server/DataPaletteBlock.java
@@ -10,7 +10,7 @@ import java.util.stream.Collectors;
 
 public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
 
-    private final DataPalette<T> b;
+    private final DataPalette<T> b; private final DataPalette<T> getDataPaletteGlobal() { return this.b; } // Paper - OBFHELPER
     private final DataPaletteExpandable<T> c = (i, object) -> {
         return 0;
     };
@@ -18,9 +18,9 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
     private final Function<NBTTagCompound, T> e;
     private final Function<T, NBTTagCompound> f;
     private final T g;
-    protected DataBits a;
-    private DataPalette<T> h;
-    private int i;
+    protected DataBits a; protected DataBits getDataBits() { return this.a; } // Paper - OBFHELPER
+    private DataPalette<T> h; private DataPalette<T> getDataPalette() { return this.h; } // Paper - OBFHELPER
+    private int i; private int getBitsPerObject() { return this.i; } // Paper - OBFHELPER
     private final ReentrantLock j = new ReentrantLock();
 
     public void a() {
@@ -55,6 +55,7 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
         return j << 8 | k << 4 | i;
     }
 
+    private void initialize(int bitsPerObject) { this.b(bitsPerObject); } // Paper - OBFHELPER
     private void b(int i) {
         if (i != this.i) {
             this.i = i;
@@ -132,6 +133,7 @@ public class DataPaletteBlock<T> implements DataPaletteExpandable<T> {
         return t0 == null ? this.g : t0;
     }
 
+    public void writeDataPaletteBlock(PacketDataSerializer packetDataSerializer) { this.b(packetDataSerializer); } // Paper - OBFHELPER
     public void b(PacketDataSerializer packetdataserializer) {
         this.a();
         packetdataserializer.writeByte(this.i);
diff --git a/src/main/java/net/minecraft/server/Entity.java b/src/main/java/net/minecraft/server/Entity.java
index 51499ef45faa2bba5f180ee333c09af73d3b708e..9da321fc0e02e4a2e47f515011df33714affd368 100644
--- a/src/main/java/net/minecraft/server/Entity.java
+++ b/src/main/java/net/minecraft/server/Entity.java
@@ -983,8 +983,9 @@ public abstract class Entity implements INamableTileEntity, ICommandListener {
 
     }
 
-    @Nullable
-    public AxisAlignedBB ay() {
+
+    @Nullable public final AxisAlignedBB getCollisionBox(){return ay();} //Paper - OBFHELPER
+    @Nullable public AxisAlignedBB ay() {
         return null;
     }
 
@@ -1774,6 +1775,7 @@ public abstract class Entity implements INamableTileEntity, ICommandListener {
         return EnumInteractionResult.PASS;
     }
 
+    public final AxisAlignedBB getHardCollisionBox(Entity entity){ return j(entity);}//Paper - OBFHELPER
     @Nullable
     public AxisAlignedBB j(Entity entity) {
         return null;
diff --git a/src/main/java/net/minecraft/server/EntityCreature.java b/src/main/java/net/minecraft/server/EntityCreature.java
index e83d587fd4feee1e36c18c49b98e669c09f5de20..c94197a50269622e8995685119bac984c45e6833 100644
--- a/src/main/java/net/minecraft/server/EntityCreature.java
+++ b/src/main/java/net/minecraft/server/EntityCreature.java
@@ -6,6 +6,8 @@ import org.bukkit.event.entity.EntityUnleashEvent;
 
 public abstract class EntityCreature extends EntityInsentient {
 
+    public org.bukkit.craftbukkit.entity.CraftCreature getBukkitCreature() { return (org.bukkit.craftbukkit.entity.CraftCreature) super.getBukkitEntity(); } // Paper
+
     protected EntityCreature(EntityTypes<? extends EntityCreature> entitytypes, World world) {
         super(entitytypes, world);
     }
diff --git a/src/main/java/net/minecraft/server/EntityInsentient.java b/src/main/java/net/minecraft/server/EntityInsentient.java
index b309d9a13b35f5c04c3a0f048e858df99d8ac617..e5455d99e3f5607a5754e5760d42853a62dddb82 100644
--- a/src/main/java/net/minecraft/server/EntityInsentient.java
+++ b/src/main/java/net/minecraft/server/EntityInsentient.java
@@ -160,6 +160,7 @@ public abstract class EntityInsentient extends EntityLiving {
         return this.goalTarget;
     }
 
+    public org.bukkit.craftbukkit.entity.CraftMob getBukkitMob() { return (org.bukkit.craftbukkit.entity.CraftMob) super.getBukkitEntity(); } // Paper
     public void setGoalTarget(@Nullable EntityLiving entityliving) {
         // CraftBukkit start - fire event
         setGoalTarget(entityliving, EntityTargetEvent.TargetReason.UNKNOWN, true);
diff --git a/src/main/java/net/minecraft/server/EntityLiving.java b/src/main/java/net/minecraft/server/EntityLiving.java
index 8ff295ed373cc316e56e4a01a268f98b7b773bd5..dccb315440f7429fe881bd0d12af8f1ae8e35c3d 100644
--- a/src/main/java/net/minecraft/server/EntityLiving.java
+++ b/src/main/java/net/minecraft/server/EntityLiving.java
@@ -137,6 +137,7 @@ public abstract class EntityLiving extends Entity {
     public boolean collides = true;
     public Set<UUID> collidableExemptions = new HashSet<>();
     public boolean canPickUpLoot;
+    public org.bukkit.craftbukkit.entity.CraftLivingEntity getBukkitLivingEntity() { return (org.bukkit.craftbukkit.entity.CraftLivingEntity) super.getBukkitEntity(); } // Paper
 
     @Override
     public float getBukkitYaw() {
diff --git a/src/main/java/net/minecraft/server/EntityMonster.java b/src/main/java/net/minecraft/server/EntityMonster.java
index fb0f281261455c9f127abd8d3b336135ad84fc27..ebdd990829edb8e423f482fa4352fe2d468efcba 100644
--- a/src/main/java/net/minecraft/server/EntityMonster.java
+++ b/src/main/java/net/minecraft/server/EntityMonster.java
@@ -5,6 +5,7 @@ import java.util.function.Predicate;
 
 public abstract class EntityMonster extends EntityCreature implements IMonster {
 
+    public org.bukkit.craftbukkit.entity.CraftMonster getBukkitMonster() { return (org.bukkit.craftbukkit.entity.CraftMonster) super.getBukkitEntity(); } // Paper
     protected EntityMonster(EntityTypes<? extends EntityMonster> entitytypes, World world) {
         super(entitytypes, world);
         this.f = 5;
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
index 6a38418d40763705639d361e47201f8b26216052..5bf3c279b86ed0fa416ee7dd9923172d6e6261a5 100644
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
@@ -91,6 +91,8 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
     public Integer clientViewDistance;
     // CraftBukkit end
 
+    public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> cachedSingleHashSet; // Paper
+
     public EntityPlayer(MinecraftServer minecraftserver, WorldServer worldserver, GameProfile gameprofile, PlayerInteractManager playerinteractmanager) {
         super(worldserver, worldserver.getSpawn(), gameprofile);
         this.spawnDimension = World.OVERWORLD;
@@ -102,6 +104,8 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
         this.G = 1.0F;
         this.b(worldserver);
 
+        this.cachedSingleHashSet = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
+
         // CraftBukkit start
         this.displayName = this.getName();
         this.canPickUpLoot = true;
diff --git a/src/main/java/net/minecraft/server/EntityTypes.java b/src/main/java/net/minecraft/server/EntityTypes.java
index 484e78746aa62bb0b12968165bf8e056b27152f3..9a772e40ad8f9858e6278b99d9d1ff5dc54513cb 100644
--- a/src/main/java/net/minecraft/server/EntityTypes.java
+++ b/src/main/java/net/minecraft/server/EntityTypes.java
@@ -3,6 +3,7 @@ package net.minecraft.server;
 import com.google.common.collect.ImmutableSet;
 import java.util.Optional;
 import java.util.Set; // Paper
+import java.util.Map; // Paper
 import java.util.UUID;
 import java.util.function.Function;
 import java.util.stream.Stream;
@@ -305,8 +306,8 @@ public class EntityTypes<T extends Entity> {
         return this.bq.height;
     }
 
-    @Nullable
-    public T a(World world) {
+    public T create(World world) { return this.a(world); } // Paper - OBFHELPER
+    @Nullable public T a(World world) { // Paper - OBFHELPER
         return this.be.create(this, world);
     }
 
diff --git a/src/main/java/net/minecraft/server/IAsyncTaskHandler.java b/src/main/java/net/minecraft/server/IAsyncTaskHandler.java
index b77a0f0c2ee30df44b113aa6c8d4fa9206d3e2ba..1ba26ee10f338edbec0f580bb55d083a3d6d2284 100644
--- a/src/main/java/net/minecraft/server/IAsyncTaskHandler.java
+++ b/src/main/java/net/minecraft/server/IAsyncTaskHandler.java
@@ -68,6 +68,15 @@ public abstract class IAsyncTaskHandler<R extends Runnable> implements Mailbox<R
 
     }
 
+    // Paper start
+    public void scheduleOnMain(Runnable r0) {
+        // postToMainThread does not work the same as older versions of mc
+        // This method is actually used to create a TickTask, which can then be posted onto main
+        this.addTask(this.postToMainThread(r0));
+    }
+    // Paper end
+
+    public void addTask(R r0) { a(r0); }; // Paper - OBFHELPER
     public void a(R r0) {
         this.d.add(r0);
         LockSupport.unpark(this.getThread());
diff --git a/src/main/java/net/minecraft/server/IBlockAccess.java b/src/main/java/net/minecraft/server/IBlockAccess.java
index 33238e2ac98e2add810ac21e40714cc2f43ac389..077fcb67f6248156aaebe7545e370ade4311b9e4 100644
--- a/src/main/java/net/minecraft/server/IBlockAccess.java
+++ b/src/main/java/net/minecraft/server/IBlockAccess.java
@@ -10,10 +10,24 @@ public interface IBlockAccess {
     @Nullable
     TileEntity getTileEntity(BlockPosition blockposition);
 
+    IBlockData getTypeIfLoaded(BlockPosition blockposition); // Paper - if loaded util
     IBlockData getType(BlockPosition blockposition);
 
+    Fluid getFluidIfLoaded(BlockPosition blockposition); // Paper - if loaded util
     Fluid getFluid(BlockPosition blockposition);
 
+    // Paper start - if loaded util
+    default Material getMaterialIfLoaded(BlockPosition blockposition) {
+        IBlockData type = this.getTypeIfLoaded(blockposition);
+        return type == null ? null : type.getMaterial();
+    }
+
+    default Block getBlockIfLoaded(BlockPosition blockposition) {
+        IBlockData type = this.getTypeIfLoaded(blockposition);
+        return type == null ? null : type.getBlock();
+    }
+    // Paper end
+
     default int h(BlockPosition blockposition) {
         return this.getType(blockposition).f();
     }
diff --git a/src/main/java/net/minecraft/server/IOWorker.java b/src/main/java/net/minecraft/server/IOWorker.java
index 38ccfd78639a85abcefb915c5c231be5881cebc1..8668b8f3941f37a7bc30a55c33baf74bd8ac49e3 100644
--- a/src/main/java/net/minecraft/server/IOWorker.java
+++ b/src/main/java/net/minecraft/server/IOWorker.java
@@ -21,7 +21,7 @@ public class IOWorker implements AutoCloseable {
     private static final Logger LOGGER = LogManager.getLogger();
     private final AtomicBoolean b = new AtomicBoolean();
     private final ThreadedMailbox<PairedQueue.b> c;
-    private final RegionFileCache d;
+    private final RegionFileCache d;public RegionFileCache getRegionFileCache() { return d; } // Paper - OBFHELPER
     private final Map<ChunkCoordIntPair, IOWorker.a> e = Maps.newLinkedHashMap();
 
     protected IOWorker(File file, boolean flag, String s) {
diff --git a/src/main/java/net/minecraft/server/IWorldReader.java b/src/main/java/net/minecraft/server/IWorldReader.java
index 284283c5f062107df88e77edaf092cf23302a109..b6d6905260cdd32873010f24ef5a3505e66159be 100644
--- a/src/main/java/net/minecraft/server/IWorldReader.java
+++ b/src/main/java/net/minecraft/server/IWorldReader.java
@@ -5,6 +5,7 @@ import javax.annotation.Nullable;
 
 public interface IWorldReader extends IBlockLightAccess, ICollisionAccess, BiomeManager.Provider {
 
+    @Nullable IChunkAccess getChunkIfLoadedImmediately(int x, int z); // Paper - ifLoaded api (we need this since current impl blocks if the chunk is loading)
     @Nullable
     IChunkAccess getChunkAt(int i, int j, ChunkStatus chunkstatus, boolean flag);
 
diff --git a/src/main/java/net/minecraft/server/ItemStack.java b/src/main/java/net/minecraft/server/ItemStack.java
index dc5625f54a54db35e5dfa630f1414887c736b704..76091ab3f149decc0d3c848b79edd24e20cf181d 100644
--- a/src/main/java/net/minecraft/server/ItemStack.java
+++ b/src/main/java/net/minecraft/server/ItemStack.java
@@ -49,7 +49,7 @@ public final class ItemStack {
         })).apply(instance, ItemStack::new);
     });
     private static final Logger LOGGER = LogManager.getLogger();
-    public static final ItemStack b = new ItemStack((Item) null);
+    public static final ItemStack b = new ItemStack((Item) null);public static final ItemStack NULL_ITEM = b; // Paper - OBFHELPER
     public static final DecimalFormat c = (DecimalFormat) SystemUtils.a((new DecimalFormat("#.##")), (decimalformat) -> { // CraftBukkit - decompile error
         decimalformat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ROOT));
     });
@@ -606,6 +606,24 @@ public final class ItemStack {
         return this.tag != null ? this.tag.getList("Enchantments", 10) : new NBTTagList();
     }
 
+    // Paper start - (this is just a good no conflict location)
+    public org.bukkit.inventory.ItemStack asBukkitMirror() {
+        return CraftItemStack.asCraftMirror(this);
+    }
+    public org.bukkit.inventory.ItemStack asBukkitCopy() {
+        return CraftItemStack.asCraftMirror(this.cloneItemStack());
+    }
+    public static ItemStack fromBukkitCopy(org.bukkit.inventory.ItemStack itemstack) {
+        return CraftItemStack.asNMSCopy(itemstack);
+    }
+    private org.bukkit.craftbukkit.inventory.CraftItemStack bukkitStack;
+    public org.bukkit.inventory.ItemStack getBukkitStack() {
+        if (bukkitStack == null || bukkitStack.getHandle() != this) {
+            bukkitStack = org.bukkit.craftbukkit.inventory.CraftItemStack.asCraftMirror(this);
+        }
+        return bukkitStack;
+    }
+    // Paper end
     public void setTag(@Nullable NBTTagCompound nbttagcompound) {
         this.tag = nbttagcompound;
         if (this.getItem().usesDurability()) {
@@ -698,6 +716,7 @@ public final class ItemStack {
         return this.tag != null && this.tag.hasKeyOfType("Enchantments", 9) ? !this.tag.getList("Enchantments", 10).isEmpty() : false;
     }
 
+    public void getOrCreateTagAndSet(String s, NBTBase nbtbase) { a(s, nbtbase);} // Paper - OBFHELPER
     public void a(String s, NBTBase nbtbase) {
         this.getOrCreateTag().set(s, nbtbase);
     }
@@ -783,6 +802,7 @@ public final class ItemStack {
     // CraftBukkit start
     @Deprecated
     public void setItem(Item item) {
+        this.bukkitStack = null; // Paper
         this.item = item;
     }
     // 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..da7a325d070e194cd1664ed20dcb3a762c9a517a
--- /dev/null
+++ b/src/main/java/net/minecraft/server/MCUtil.java
@@ -0,0 +1,502 @@
+package net.minecraft.server;
+
+import com.destroystokyo.paper.block.TargetBlockInfo;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import org.apache.commons.lang.exception.ExceptionUtils;
+import org.bukkit.Location;
+import org.bukkit.block.BlockFace;
+import org.bukkit.craftbukkit.CraftWorld;
+import org.bukkit.craftbukkit.util.Waitable;
+import org.spigotmc.AsyncCatcher;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.List;
+import java.util.Queue;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+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.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+public final class MCUtil {
+    public static final ThreadPoolExecutor asyncExecutor = new ThreadPoolExecutor(
+        0, 2, 60L, TimeUnit.SECONDS,
+        new LinkedBlockingQueue<Runnable>(),
+        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<Runnable>(),
+        new ThreadFactoryBuilder().setNameFormat("Paper Object Cleaner").build()
+    );
+
+    public static final long INVALID_CHUNK_KEY = getCoordinateKey(Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+
+    public static Runnable once(Runnable run) {
+        AtomicBoolean ran = new AtomicBoolean(false);
+        return () -> {
+            if (ran.compareAndSet(false, true)) {
+                run.run();
+            }
+        };
+    }
+
+    public static <T> Runnable once(List<T> list, Consumer<T> cb) {
+        return once(() -> {
+            list.forEach(cb);
+        });
+    }
+
+    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 <T>
+     * @return
+     */
+    public static <T> Runnable registerListCleaner(Object obj, List<T> list, Consumer<T> 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 <T>
+     * @return
+     */
+    public static <T> Runnable registerCleaner(Object obj, T resource, java.util.function.Consumer<T> cleaner) {
+        return registerCleaner(obj, () -> cleaner.accept(resource));
+    }
+
+    public static List<ChunkCoordIntPair> getSpiralOutChunks(BlockPosition blockposition, int radius) {
+        List<ChunkCoordIntPair> list = com.google.common.collect.Lists.newArrayList();
+
+        list.add(new ChunkCoordIntPair(blockposition.getX() >> 4, blockposition.getZ() >> 4));
+        for (int r = 1; r <= radius; r++) {
+            int x = -r;
+            int z = r;
+
+            // Iterates the edge of half of the box; then negates for other half.
+            while (x <= r && z > -r) {
+                list.add(new ChunkCoordIntPair((blockposition.getX() + (x << 4)) >> 4, (blockposition.getZ() + (z << 4)) >> 4));
+                list.add(new ChunkCoordIntPair((blockposition.getX() - (x << 4)) >> 4, (blockposition.getZ() - (z << 4)) >> 4));
+
+                if (x < r) {
+                    x++;
+                } else {
+                    z--;
+                }
+            }
+        }
+        return list;
+    }
+
+    public static int fastFloor(double x) {
+        int truncated = (int)x;
+        return x < (double)truncated ? truncated - 1 : truncated;
+    }
+
+    public static int fastFloor(float x) {
+        int truncated = (int)x;
+        return x < (double)truncated ? truncated - 1 : truncated;
+    }
+
+    public static float normalizeYaw(float f) {
+        float f1 = f % 360.0F;
+
+        if (f1 >= 180.0F) {
+            f1 -= 360.0F;
+        }
+
+        if (f1 < -180.0F) {
+            f1 += 360.0F;
+        }
+
+        return f1;
+    }
+
+    /**
+     * Quickly generate a stack trace for current location
+     *
+     * @return Stacktrace
+     */
+    public static String stack() {
+        return ExceptionUtils.getFullStackTrace(new Throwable());
+    }
+
+    /**
+     * Quickly generate a stack trace for current location with message
+     *
+     * @param str
+     * @return Stacktrace
+     */
+    public static String stack(String str) {
+        return ExceptionUtils.getFullStackTrace(new Throwable(str));
+    }
+
+    public static long getCoordinateKey(final BlockPosition blockPos) {
+        return ((long)(blockPos.getZ() >> 4) << 32) | ((blockPos.getX() >> 4) & 0xFFFFFFFFL);
+    }
+
+    public static long getCoordinateKey(final Entity entity) {
+        return ((long)(MCUtil.fastFloor(entity.locZ()) >> 4) << 32) | ((MCUtil.fastFloor(entity.locX()) >> 4) & 0xFFFFFFFFL);
+    }
+
+    public static long getCoordinateKey(final ChunkCoordIntPair pair) {
+        return ((long)pair.z << 32) | (pair.x & 0xFFFFFFFFL);
+    }
+
+    public static long getCoordinateKey(final int x, final int z) {
+        return ((long)z << 32) | (x & 0xFFFFFFFFL);
+    }
+
+    public static int getCoordinateX(final long key) {
+        return (int)key;
+    }
+
+    public static int getCoordinateZ(final long key) {
+        return (int)(key >>> 32);
+    }
+
+    public static int getChunkCoordinate(final double coordinate) {
+        return MCUtil.fastFloor(coordinate) >> 4;
+    }
+
+    public static int getBlockCoordinate(final double coordinate) {
+        return MCUtil.fastFloor(coordinate);
+    }
+
+    public static long getBlockKey(final int x, final int y, final int z) {
+        return ((long)x & 0x7FFFFFF) | (((long)z & 0x7FFFFFF) << 27) | ((long)y << 54);
+    }
+
+    public static long getBlockKey(final BlockPosition pos) {
+        return ((long)pos.getX() & 0x7FFFFFF) | (((long)pos.getZ() & 0x7FFFFFF) << 27) | ((long)pos.getY() << 54);
+    }
+
+    public static long getBlockKey(final Entity entity) {
+        return getBlockKey(getBlockCoordinate(entity.locX()), getBlockCoordinate(entity.locY()), getBlockCoordinate(entity.locZ()));
+    }
+
+    // assumes the sets have the same comparator, and if this comparator is null then assume T is Comparable
+    public static <T> void mergeSortedSets(final java.util.function.Consumer<T> consumer, final java.util.Comparator<? super T> comparator, final java.util.SortedSet<T>...sets) {
+        final it.unimi.dsi.fastutil.objects.ObjectRBTreeSet<T> all = new it.unimi.dsi.fastutil.objects.ObjectRBTreeSet<>(comparator);
+        // note: this is done in log(n!) ~ nlogn time. It could be improved if it were to mimic what mergesort does.
+        for (java.util.SortedSet<T> set : sets) {
+            if (set != null) {
+                all.addAll(set);
+            }
+        }
+        all.forEach(consumer);
+    }
+
+    private MCUtil() {}
+
+    public static final java.util.concurrent.Executor MAIN_EXECUTOR = (run) -> {
+        if (!isMainThread()) {
+            MinecraftServer.getServer().execute(run);
+        } else {
+            run.run();
+        }
+    };
+
+    public static <T> CompletableFuture<T> ensureMain(CompletableFuture<T> future) {
+        return future.thenApplyAsync(r -> r, MAIN_EXECUTOR);
+    }
+
+    public static <T> void thenOnMain(CompletableFuture<T> future, Consumer<T> consumer) {
+        future.thenAcceptAsync(consumer, MAIN_EXECUTOR);
+    }
+    public static <T> void thenOnMain(CompletableFuture<T> future, BiConsumer<T, Throwable> consumer) {
+        future.whenCompleteAsync(consumer, MAIN_EXECUTOR);
+    }
+
+    public static boolean isMainThread() {
+        return MinecraftServer.getServer().isMainThread();
+    }
+
+    public static org.bukkit.scheduler.BukkitTask scheduleTask(int ticks, Runnable runnable) {
+        return scheduleTask(ticks, runnable, null);
+    }
+
+    public static org.bukkit.scheduler.BukkitTask scheduleTask(int ticks, Runnable runnable, String taskName) {
+        return MinecraftServer.getServer().server.getScheduler().scheduleInternalTask(runnable, ticks, taskName);
+    }
+
+    public static void processQueue() {
+        Runnable runnable;
+        Queue<Runnable> processQueue = getProcessQueue();
+        while ((runnable = processQueue.poll()) != null) {
+            try {
+                runnable.run();
+            } catch (Exception e) {
+                MinecraftServer.LOGGER.error("Error executing task", e);
+            }
+        }
+    }
+    public static <T> T processQueueWhileWaiting(CompletableFuture <T> future) {
+        try {
+            if (isMainThread()) {
+                while (!future.isDone()) {
+                    try {
+                        return future.get(1, TimeUnit.MILLISECONDS);
+                    } catch (TimeoutException ignored) {
+                        processQueue();
+                    }
+                }
+            }
+            return future.get();
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static void ensureMain(Runnable run) {
+        ensureMain(null, run);
+    }
+    /**
+     * Ensures the target code is running on the main thread
+     * @param reason
+     * @param run
+     * @return
+     */
+    public static void ensureMain(String reason, Runnable run) {
+        if (AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread) {
+            if (reason != null) {
+                new IllegalStateException("Asynchronous " + reason + "!").printStackTrace();
+            }
+            getProcessQueue().add(run);
+            return;
+        }
+        run.run();
+    }
+
+    private static Queue<Runnable> getProcessQueue() {
+        return MinecraftServer.getServer().processQueue;
+    }
+
+    public static <T> T ensureMain(Supplier<T> run) {
+        return ensureMain(null, run);
+    }
+    /**
+     * Ensures the target code is running on the main thread
+     * @param reason
+     * @param run
+     * @param <T>
+     * @return
+     */
+    public static <T> T ensureMain(String reason, Supplier<T> run) {
+        if (AsyncCatcher.enabled && Thread.currentThread() != MinecraftServer.getServer().serverThread) {
+            if (reason != null) {
+                new IllegalStateException("Asynchronous " + reason + "! Blocking thread until it returns ").printStackTrace();
+            }
+            Waitable<T> wait = new Waitable<T>() {
+                @Override
+                protected T evaluate() {
+                    return run.get();
+                }
+            };
+            getProcessQueue().add(wait);
+            try {
+                return wait.get();
+            } catch (InterruptedException | ExecutionException e) {
+                e.printStackTrace();
+            }
+            return null;
+        }
+        return run.get();
+    }
+
+    /**
+     * Calculates distance between 2 entities
+     * @param e1
+     * @param e2
+     * @return
+     */
+    public static double distance(Entity e1, Entity e2) {
+        return Math.sqrt(distanceSq(e1, e2));
+    }
+
+
+    /**
+     * Calculates distance between 2 block positions
+     * @param e1
+     * @param e2
+     * @return
+     */
+    public static double distance(BlockPosition e1, BlockPosition e2) {
+        return Math.sqrt(distanceSq(e1, e2));
+    }
+
+    /**
+     * Gets the distance between 2 positions
+     * @param x1
+     * @param y1
+     * @param z1
+     * @param x2
+     * @param y2
+     * @param z2
+     * @return
+     */
+    public static double distance(double x1, double y1, double z1, double x2, double y2, double z2) {
+        return Math.sqrt(distanceSq(x1, y1, z1, x2, y2, z2));
+    }
+
+    /**
+     * Get's the distance squared between 2 entities
+     * @param e1
+     * @param e2
+     * @return
+     */
+    public static double distanceSq(Entity e1, Entity e2) {
+        return distanceSq(e1.locX(),e1.locY(),e1.locZ(), e2.locX(),e2.locY(),e2.locZ());
+    }
+
+    /**
+     * Gets the distance sqaured between 2 block positions
+     * @param pos1
+     * @param pos2
+     * @return
+     */
+    public static double distanceSq(BlockPosition pos1, BlockPosition pos2) {
+        return distanceSq(pos1.getX(), pos1.getY(), pos1.getZ(), pos2.getX(), pos2.getY(), pos2.getZ());
+    }
+
+    /**
+     * Gets the distance squared between 2 positions
+     * @param x1
+     * @param y1
+     * @param z1
+     * @param x2
+     * @param y2
+     * @param z2
+     * @return
+     */
+    public static double distanceSq(double x1, double y1, double z1, double x2, double y2, double z2) {
+        return (x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2) + (z1 - z2) * (z1 - z2);
+    }
+
+    /**
+     * Converts a NMS World/BlockPosition to Bukkit Location
+     * @param world
+     * @param x
+     * @param y
+     * @param z
+     * @return
+     */
+    public static Location toLocation(World world, double x, double y, double z) {
+        return new Location(world.getWorld(), x, y, z);
+    }
+
+    /**
+     * Converts a NMS World/BlockPosition to Bukkit Location
+     * @param world
+     * @param pos
+     * @return
+     */
+    public static Location toLocation(World world, BlockPosition pos) {
+        return new Location(world.getWorld(), pos.getX(), pos.getY(), pos.getZ());
+    }
+
+    /**
+     * Converts an NMS entity's current location to a Bukkit Location
+     * @param entity
+     * @return
+     */
+    public static Location toLocation(Entity entity) {
+        return new Location(entity.getWorld().getWorld(), entity.locX(), entity.locY(), entity.locZ());
+    }
+
+    public static org.bukkit.block.Block toBukkitBlock(World world, BlockPosition pos) {
+        return world.getWorld().getBlockAt(pos.getX(), pos.getY(), pos.getZ());
+    }
+
+    public static BlockPosition toBlockPosition(Location loc) {
+        return new BlockPosition(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
+    }
+
+    public static boolean isEdgeOfChunk(BlockPosition pos) {
+        final int modX = pos.getX() & 15;
+        final int modZ = pos.getZ() & 15;
+        return (modX == 0 || modX == 15 || modZ == 0 || modZ == 15);
+    }
+
+    /**
+     * Posts a task to be executed asynchronously
+     * @param run
+     */
+    public static void scheduleAsyncTask(Runnable run) {
+        asyncExecutor.execute(run);
+    }
+
+    @Nonnull
+    public static WorldServer getNMSWorld(@Nonnull org.bukkit.World world) {
+        return ((CraftWorld) world).getHandle();
+    }
+
+    public static WorldServer getNMSWorld(@Nonnull org.bukkit.entity.Entity entity) {
+        return getNMSWorld(entity.getWorld());
+    }
+
+    public static RayTrace.FluidCollisionOption getNMSFluidCollisionOption(TargetBlockInfo.FluidMode fluidMode) {
+        if (fluidMode == TargetBlockInfo.FluidMode.NEVER) {
+            return RayTrace.FluidCollisionOption.NONE;
+        }
+        if (fluidMode == TargetBlockInfo.FluidMode.SOURCE_ONLY) {
+            return RayTrace.FluidCollisionOption.SOURCE_ONLY;
+        }
+        if (fluidMode == TargetBlockInfo.FluidMode.ALWAYS) {
+            return RayTrace.FluidCollisionOption.ANY;
+        }
+        return null;
+    }
+
+    public static BlockFace toBukkitBlockFace(EnumDirection enumDirection) {
+        switch (enumDirection) {
+            case DOWN:
+                return BlockFace.DOWN;
+            case UP:
+                return BlockFace.UP;
+            case NORTH:
+                return BlockFace.NORTH;
+            case SOUTH:
+                return BlockFace.SOUTH;
+            case WEST:
+                return BlockFace.WEST;
+            case EAST:
+                return BlockFace.EAST;
+            default:
+                return null;
+        }
+    }
+}
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
index 5e2dff9a30615b5580710f872a92ab9f07fca6cf..70604fbf6bdd5166fc91d57fb4db4a8337bc4727 100644
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
@@ -766,6 +766,9 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant<TickTas
             MinecraftServer.LOGGER.error("Failed to unlock level {}", this.convertable.getLevelName(), ioexception1);
         }
         // Spigot start
+        MCUtil.asyncExecutor.shutdown(); // Paper
+        try { MCUtil.asyncExecutor.awaitTermination(30, java.util.concurrent.TimeUnit.SECONDS); // Paper
+        } catch (java.lang.InterruptedException ignored) {} // Paper
         if (org.spigotmc.SpigotConfig.saveUserCacheOnStopOnly) {
             LOGGER.info("Saving usercache.json");
             this.getUserCache().c();
diff --git a/src/main/java/net/minecraft/server/NBTTagCompound.java b/src/main/java/net/minecraft/server/NBTTagCompound.java
index ef2dee1987c45a2c43188584169dd260ee80826c..c16ff6723d3fd191b990002d40dc021d7870555d 100644
--- a/src/main/java/net/minecraft/server/NBTTagCompound.java
+++ b/src/main/java/net/minecraft/server/NBTTagCompound.java
@@ -70,7 +70,7 @@ public class NBTTagCompound implements NBTBase {
             return "TAG_Compound";
         }
     };
-    private final Map<String, NBTBase> map;
+    public final Map<String, NBTBase> map; // Paper
 
     protected NBTTagCompound(Map<String, NBTBase> map) {
         this.map = map;
@@ -133,10 +133,14 @@ public class NBTTagCompound implements NBTBase {
         this.map.put(s, NBTTagLong.a(i));
     }
 
+    public void setUUID(String prefix, UUID uuid) { a(prefix, uuid); } // Paper - OBFHELPER
     public void a(String s, UUID uuid) {
         this.map.put(s, GameProfileSerializer.a(uuid));
     }
 
+
+    @Nullable public UUID getUUID(String prefix) { return a(prefix); } // Paper - OBFHELPER
+    @Nullable
     public UUID a(String s) {
         return GameProfileSerializer.a(this.get(s));
     }
diff --git a/src/main/java/net/minecraft/server/NetworkManager.java b/src/main/java/net/minecraft/server/NetworkManager.java
index 69718caa347bc75ed243041797a6b53d90fc553c..c2fdccfb9192aa7ec55fd67c169cba71a695075c 100644
--- a/src/main/java/net/minecraft/server/NetworkManager.java
+++ b/src/main/java/net/minecraft/server/NetworkManager.java
@@ -158,6 +158,7 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
 
     }
 
+    private void dispatchPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericFutureListener) { this.b(packet, genericFutureListener); } // Paper - OBFHELPER
     private void b(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericfuturelistener) {
         EnumProtocol enumprotocol = EnumProtocol.a(packet);
         EnumProtocol enumprotocol1 = (EnumProtocol) this.channel.attr(NetworkManager.c).get();
@@ -198,6 +199,7 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
 
     }
 
+    private void sendPacketQueue() { this.o(); } // Paper - OBFHELPER
     private void o() {
         if (this.channel != null && this.channel.isOpen()) {
             Queue queue = this.packetQueue;
@@ -326,9 +328,9 @@ public class NetworkManager extends SimpleChannelInboundHandler<Packet<?>> {
 
     static class QueuedPacket {
 
-        private final Packet<?> a;
+        private final Packet<?> a; private final Packet<?> getPacket() { return this.a; } // Paper - OBFHELPER
         @Nullable
-        private final GenericFutureListener<? extends Future<? super Void>> b;
+        private final GenericFutureListener<? extends Future<? super Void>> b; private final GenericFutureListener<? extends Future<? super Void>> getGenericFutureListener() { return this.b; } // Paper - OBFHELPER
 
         public QueuedPacket(Packet<?> packet, @Nullable GenericFutureListener<? extends Future<? super Void>> genericfuturelistener) {
             this.a = packet;
diff --git a/src/main/java/net/minecraft/server/PacketDataSerializer.java b/src/main/java/net/minecraft/server/PacketDataSerializer.java
index f574a28b68cf4d3745ee5f1a3756e19dbc23ca92..6e049c2e2a142ce022b9dc278a3bb302f723e42c 100644
--- a/src/main/java/net/minecraft/server/PacketDataSerializer.java
+++ b/src/main/java/net/minecraft/server/PacketDataSerializer.java
@@ -36,6 +36,7 @@ public class PacketDataSerializer extends ByteBuf {
         this.a = bytebuf;
     }
 
+    public static int countBytes(int i) { return PacketDataSerializer.a(i); } // Paper - OBFHELPER
     public static int a(int i) {
         for (int j = 1; j < 5; ++j) {
             if ((i & -1 << j * 7) == 0) {
diff --git a/src/main/java/net/minecraft/server/PacketEncoder.java b/src/main/java/net/minecraft/server/PacketEncoder.java
index 90223deae3376fd6828eddf3831dab96650afef2..63c4dbd327beb7b6ab42eb44650d68accd3b0de6 100644
--- a/src/main/java/net/minecraft/server/PacketEncoder.java
+++ b/src/main/java/net/minecraft/server/PacketEncoder.java
@@ -42,6 +42,7 @@ public class PacketEncoder extends MessageToByteEncoder<Packet<?>> {
                     packet.b(packetdataserializer);
                 } catch (Throwable throwable) {
                     PacketEncoder.LOGGER.error(throwable);
+                    throwable.printStackTrace(); // Paper - WHAT WAS IT? WHO DID THIS TO YOU? WHAT DID YOU SEE?
                     if (packet.a()) {
                         throw new SkipEncodeException(throwable);
                     } else {
diff --git a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
index cb0b7e7157b629c259b0ccb946c29cbcb3c6fa98..e336437207f9d6adbab69ef2785c129ff2ec1b36 100644
--- a/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
+++ b/src/main/java/net/minecraft/server/PacketPlayOutMapChunk.java
@@ -17,7 +17,7 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
     private NBTTagCompound d;
     @Nullable
     private BiomeStorage e;
-    private byte[] f;
+    private byte[] f; private byte[] getData() { return this.f; } // Paper - OBFHELPER
     private List<NBTTagCompound> g;
     private boolean h;
     private boolean i;
@@ -133,6 +133,7 @@ public class PacketPlayOutMapChunk implements Packet<PacketListenerPlayOut> {
         return bytebuf;
     }
 
+    public int writeChunk(PacketDataSerializer packetDataSerializer, Chunk chunk, int chunkSectionSelector) { return this.a(packetDataSerializer, chunk, chunkSectionSelector); } // Paper - OBFHELPER
     public int a(PacketDataSerializer packetdataserializer, Chunk chunk, int i) {
         int j = 0;
         ChunkSection[] achunksection = chunk.getSections();
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
index dc5e59bcecbe080445f33daa01b81ba747ed6337..ca41e420a1ab22f097dd0b98e156fd51434733d8 100644
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
@@ -19,9 +19,9 @@ public class PlayerChunk {
     private static final List<ChunkStatus> CHUNK_STATUSES = ChunkStatus.a();
     private static final PlayerChunk.State[] CHUNK_STATES = PlayerChunk.State.values();
     private final AtomicReferenceArray<CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>>> statusFutures;
-    private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> fullChunkFuture;
-    private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> tickingFuture;
-    private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> entityTickingFuture;
+    private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> fullChunkFuture; private int fullChunkCreateCount; private volatile boolean isFullChunkReady; // Paper - cache chunk ticking stage
+    private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> tickingFuture; private volatile boolean isTickingReady; // Paper - cache chunk ticking stage
+    private volatile CompletableFuture<Either<Chunk, PlayerChunk.Failure>> entityTickingFuture; private volatile boolean isEntityTickingReady; // Paper - cache chunk ticking stage
     private CompletableFuture<IChunkAccess> chunkSave;
     public int oldTicketLevel;
     private int ticketLevel;
@@ -37,6 +37,8 @@ public class PlayerChunk {
     public final PlayerChunk.d players;
     private boolean hasBeenLoaded;
 
+    private final PlayerChunkMap chunkMap; // Paper
+
     public PlayerChunk(ChunkCoordIntPair chunkcoordintpair, int i, LightEngine lightengine, PlayerChunk.c playerchunk_c, PlayerChunk.d playerchunk_d) {
         this.statusFutures = new AtomicReferenceArray(PlayerChunk.CHUNK_STATUSES.size());
         this.fullChunkFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
@@ -52,8 +54,47 @@ public class PlayerChunk {
         this.ticketLevel = this.oldTicketLevel;
         this.n = this.oldTicketLevel;
         this.a(i);
+        this.chunkMap = (PlayerChunkMap)playerchunk_d; // Paper
+    }
+
+    // Paper start
+    @Nullable
+    public final Chunk getEntityTickingChunk() {
+        CompletableFuture<Either<Chunk, PlayerChunk.Failure>> completablefuture = this.entityTickingFuture;
+        Either<Chunk, PlayerChunk.Failure> either = completablefuture.getNow(null);
+
+        return either == null ? null : either.left().orElse(null);
+    }
+
+    @Nullable
+    public final Chunk getTickingChunk() {
+        CompletableFuture<Either<Chunk, PlayerChunk.Failure>> completablefuture = this.tickingFuture;
+        Either<Chunk, PlayerChunk.Failure> either = completablefuture.getNow(null);
+
+        return either == null ? null : either.left().orElse(null);
+    }
+
+    @Nullable
+    public final Chunk getFullReadyChunk() {
+        CompletableFuture<Either<Chunk, PlayerChunk.Failure>> completablefuture = this.fullChunkFuture;
+        Either<Chunk, PlayerChunk.Failure> either = completablefuture.getNow(null);
+
+        return either == null ? null : either.left().orElse(null);
+    }
+
+    public final boolean isEntityTickingReady() {
+        return this.isEntityTickingReady;
     }
 
+    public final boolean isTickingReady() {
+        return this.isTickingReady;
+    }
+
+    public final boolean isFullChunkReady() {
+        return this.isFullChunkReady;
+    }
+    // Paper end
+
     // CraftBukkit start
     public Chunk getFullChunk() {
         if (!getChunkState(this.oldTicketLevel).isAtLeast(PlayerChunk.State.BORDER)) return null; // note: using oldTicketLevel for isLoaded checks
@@ -62,6 +103,14 @@ public class PlayerChunk {
         return either == null ? null : (Chunk) either.left().orElse(null);
     }
     // CraftBukkit end
+    // Paper start - "real" get full chunk immediately
+    public Chunk getFullChunkIfCached() {
+        // Note: Copied from above without ticket level check
+        CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> statusFuture = this.getStatusFutureUnchecked(ChunkStatus.FULL);
+        Either<IChunkAccess, PlayerChunk.Failure> either = (Either<IChunkAccess, PlayerChunk.Failure>) statusFuture.getNow(null);
+        return either == null ? null : (Chunk) either.left().orElse(null);
+    }
+    // Paper end
 
     public CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> getStatusFutureUnchecked(ChunkStatus chunkstatus) {
         CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>> completablefuture = (CompletableFuture) this.statusFutures.get(chunkstatus.c());
@@ -73,14 +122,17 @@ public class PlayerChunk {
         return getChunkStatus(this.ticketLevel).b(chunkstatus) ? this.getStatusFutureUnchecked(chunkstatus) : PlayerChunk.UNLOADED_CHUNK_ACCESS_FUTURE;
     }
 
+    public final CompletableFuture<Either<Chunk, PlayerChunk.Failure>> getTickingFuture() { return this.a(); } // Paper - OBFHELPER
     public CompletableFuture<Either<Chunk, PlayerChunk.Failure>> a() {
         return this.tickingFuture;
     }
 
+    public final CompletableFuture<Either<Chunk, PlayerChunk.Failure>> getEntityTickingFuture() { return this.b(); } // Paper - OBFHELPER
     public CompletableFuture<Either<Chunk, PlayerChunk.Failure>> b() {
         return this.entityTickingFuture;
     }
 
+    public final CompletableFuture<Either<Chunk, PlayerChunk.Failure>> getFullChunkFuture() { return this.c(); } // Paper - OBFHELPER
     public CompletableFuture<Either<Chunk, PlayerChunk.Failure>> c() {
         return this.fullChunkFuture;
     }
@@ -322,13 +374,27 @@ public class PlayerChunk {
 
         this.hasBeenLoaded |= flag3;
         if (!flag2 && flag3) {
-            this.fullChunkFuture = playerchunkmap.b(this);
+            // Paper start - cache ticking ready status
+            int expectCreateCount = ++this.fullChunkCreateCount;
+            this.fullChunkFuture = playerchunkmap.b(this); this.fullChunkFuture.thenAccept((either) -> {
+                if (either.left().isPresent() && PlayerChunk.this.fullChunkCreateCount == expectCreateCount) {
+                    // note: Here is a very good place to add callbacks to logic waiting on this.
+                    Chunk fullChunk = either.left().get();
+                    PlayerChunk.this.isFullChunkReady = true;
+                    fullChunk.playerChunk = PlayerChunk.this;
+
+
+                }
+            });
+            // Paper end
             this.a(this.fullChunkFuture);
         }
 
         if (flag2 && !flag3) {
             completablefuture = this.fullChunkFuture;
             this.fullChunkFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
+            ++this.fullChunkCreateCount; // Paper - cache ticking ready status
+            this.isFullChunkReady = false; // Paper - cache ticking ready status
             this.a(((CompletableFuture<Either<Chunk, PlayerChunk.Failure>>) completablefuture).thenApply((either1) -> { // CraftBukkit - decompile error
                 playerchunkmap.getClass();
                 return either1.ifLeft(playerchunkmap::a);
@@ -339,12 +405,24 @@ public class PlayerChunk {
         boolean flag5 = playerchunk_state1.isAtLeast(PlayerChunk.State.TICKING);
 
         if (!flag4 && flag5) {
-            this.tickingFuture = playerchunkmap.a(this);
+            // Paper start - cache ticking ready status
+            this.tickingFuture = playerchunkmap.a(this); this.tickingFuture.thenAccept((either) -> {
+                if (either.left().isPresent()) {
+                    // note: Here is a very good place to add callbacks to logic waiting on this.
+                    Chunk tickingChunk = either.left().get();
+                    PlayerChunk.this.isTickingReady = true;
+
+
+
+
+                }
+            });
+            // Paper end
             this.a(this.tickingFuture);
         }
 
         if (flag4 && !flag5) {
-            this.tickingFuture.complete(PlayerChunk.UNLOADED_CHUNK);
+            this.tickingFuture.complete(PlayerChunk.UNLOADED_CHUNK); this.isTickingReady = false; // Paper - cache chunk ticking stage
             this.tickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
         }
 
@@ -356,12 +434,24 @@ public class PlayerChunk {
                 throw (IllegalStateException) SystemUtils.c(new IllegalStateException());
             }
 
-            this.entityTickingFuture = playerchunkmap.b(this.location);
+            // Paper start - cache ticking ready status
+            this.entityTickingFuture = playerchunkmap.b(this.location); this.entityTickingFuture.thenAccept((either) -> {
+                if (either.left().isPresent()) {
+                    // note: Here is a very good place to add callbacks to logic waiting on this.
+                    Chunk entityTickingChunk = either.left().get();
+                    PlayerChunk.this.isEntityTickingReady = true;
+
+
+
+
+                }
+            });
+            // Paper end
             this.a(this.entityTickingFuture);
         }
 
         if (flag6 && !flag7) {
-            this.entityTickingFuture.complete(PlayerChunk.UNLOADED_CHUNK);
+            this.entityTickingFuture.complete(PlayerChunk.UNLOADED_CHUNK); this.isEntityTickingReady = false; // Paper - cache chunk ticking stage
             this.entityTickingFuture = PlayerChunk.UNLOADED_CHUNK_FUTURE;
         }
 
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
index 5f692d719fd270120207ebcf6d0a2a24e8d59f7b..3e2e355177e32856dac07dc8b98658ad1b717045 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -101,6 +101,26 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
     };
     // CraftBukkit end
 
+    // Paper start - distance maps
+    private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<EntityPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
+
+    void addPlayerToDistanceMaps(EntityPlayer player) {
+        int chunkX = MCUtil.getChunkCoordinate(player.locX());
+        int chunkZ = MCUtil.getChunkCoordinate(player.locZ());
+        // Note: players need to be explicitly added to distance maps before they can be updated
+    }
+
+    void removePlayerFromDistanceMaps(EntityPlayer player) {
+
+    }
+
+    void updateMaps(EntityPlayer player) {
+        int chunkX = MCUtil.getChunkCoordinate(player.locX());
+        int chunkZ = MCUtil.getChunkCoordinate(player.locZ());
+        // Note: players need to be explicitly added to distance maps before they can be updated
+    }
+    // Paper end
+
     public PlayerChunkMap(WorldServer worldserver, Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer, DefinedStructureManager definedstructuremanager, Executor executor, IAsyncTaskHandler<Runnable> iasynctaskhandler, ILightAccess ilightaccess, ChunkGenerator chunkgenerator, WorldLoadListener worldloadlistener, Supplier<WorldPersistentData> supplier, int i, boolean flag) {
         super(new File(convertable_conversionsession.a(worldserver.getDimensionKey()), "region"), datafixer, flag);
         this.visibleChunks = this.updatingChunks.clone();
@@ -190,6 +210,14 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
         };
     }
 
+    // Paper start
+    public final int getEffectiveViewDistance() {
+        // TODO this needs to be checked on update
+        // Mojang currently sets it to +1 of the configured view distance. So subtract one to get the one we really want.
+        return this.viewDistance - 1;
+    }
+    // Paper end
+
     private CompletableFuture<Either<List<IChunkAccess>, PlayerChunk.Failure>> a(ChunkCoordIntPair chunkcoordintpair, int i, IntFunction<ChunkStatus> intfunction) {
         List<CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>>> list = Lists.newArrayList();
         int j = chunkcoordintpair.x;
@@ -900,6 +928,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
             if (!flag1) {
                 this.chunkDistanceManager.a(SectionPosition.a((Entity) entityplayer), entityplayer);
             }
+            this.addPlayerToDistanceMaps(entityplayer); // Paper - distance maps
         } else {
             SectionPosition sectionposition = entityplayer.N();
 
@@ -907,6 +936,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
             if (!flag2) {
                 this.chunkDistanceManager.b(sectionposition, entityplayer);
             }
+            this.removePlayerFromDistanceMaps(entityplayer); // Paper - distance maps
         }
 
         for (int k = i - this.viewDistance; k <= i + this.viewDistance; ++k) {
@@ -1017,6 +1047,8 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
             }
         }
 
+        this.updateMaps(entityplayer); // Paper - distance maps
+
     }
 
     @Override
diff --git a/src/main/java/net/minecraft/server/PlayerConnection.java b/src/main/java/net/minecraft/server/PlayerConnection.java
index 92922db3f57877507c6443c49f4c021aee667b1d..b8d3c4d35cee2ba4678f6d22b8ea956d598d9d9b 100644
--- a/src/main/java/net/minecraft/server/PlayerConnection.java
+++ b/src/main/java/net/minecraft/server/PlayerConnection.java
@@ -67,9 +67,9 @@ public class PlayerConnection implements PacketListenerPlayIn {
     private final MinecraftServer minecraftServer;
     public EntityPlayer player;
     private int e;
-    private long lastKeepAlive;
-    private boolean awaitingKeepAlive;
-    private long h;
+    private long lastKeepAlive; private void setLastPing(long lastPing) { this.lastKeepAlive = lastPing;}; private long getLastPing() { return this.lastKeepAlive;}; // Paper - OBFHELPER
+    private boolean awaitingKeepAlive; private void setPendingPing(boolean isPending) { this.awaitingKeepAlive = isPending;}; private boolean isPendingPing() { return this.awaitingKeepAlive;}; // Paper - OBFHELPER
+    private long h; private void setKeepAliveID(long keepAliveID) { this.h = keepAliveID;}; private long getKeepAliveID() {return this.h; };  // Paper - OBFHELPER
     // CraftBukkit start - multithreaded fields
     private volatile int chatThrottle;
     private static final AtomicIntegerFieldUpdater chatSpamField = AtomicIntegerFieldUpdater.newUpdater(PlayerConnection.class, "chatThrottle");
diff --git a/src/main/java/net/minecraft/server/PlayerInventory.java b/src/main/java/net/minecraft/server/PlayerInventory.java
index c027fb94881be14396cba879087861df35023500..3b65711b91c51ac7b4b5b2b0144ffd279fe60eeb 100644
--- a/src/main/java/net/minecraft/server/PlayerInventory.java
+++ b/src/main/java/net/minecraft/server/PlayerInventory.java
@@ -17,7 +17,7 @@ public class PlayerInventory implements IInventory, INamableTileEntity {
     public final NonNullList<ItemStack> items;
     public final NonNullList<ItemStack> armor;
     public final NonNullList<ItemStack> extraSlots;
-    private final List<NonNullList<ItemStack>> f;
+    private final List<NonNullList<ItemStack>> f;List<NonNullList<ItemStack>> getComponents() { return f; } // Paper - OBFHELPER
     public int itemInHandIndex;
     public final EntityHuman player;
     private ItemStack carried;
diff --git a/src/main/java/net/minecraft/server/PotionUtil.java b/src/main/java/net/minecraft/server/PotionUtil.java
index b3824898daa80da791cdc8cfd06900e9a0b3b5b5..bf4172be525d5bdd7c152117afce8bf00106a139 100644
--- a/src/main/java/net/minecraft/server/PotionUtil.java
+++ b/src/main/java/net/minecraft/server/PotionUtil.java
@@ -110,6 +110,7 @@ public class PotionUtil {
         return nbttagcompound == null ? Potions.EMPTY : PotionRegistry.a(nbttagcompound.getString("Potion"));
     }
 
+    public static ItemStack addPotionToItemStack(ItemStack itemstack, PotionRegistry potionregistry) { return a(itemstack, potionregistry); } // Paper - OBFHELPER
     public static ItemStack a(ItemStack itemstack, PotionRegistry potionregistry) {
         MinecraftKey minecraftkey = IRegistry.POTION.getKey(potionregistry);
 
diff --git a/src/main/java/net/minecraft/server/ProtoChunk.java b/src/main/java/net/minecraft/server/ProtoChunk.java
index f92fbf136158336b65217a504ecd422bfcc4964f..070449198273e6c42e72c891882b82361d1c8dbd 100644
--- a/src/main/java/net/minecraft/server/ProtoChunk.java
+++ b/src/main/java/net/minecraft/server/ProtoChunk.java
@@ -81,6 +81,18 @@ public class ProtoChunk implements IChunkAccess {
 
     }
 
+    // Paper start - If loaded util
+    @Override
+    public Fluid getFluidIfLoaded(BlockPosition blockposition) {
+        return this.getFluid(blockposition);
+    }
+
+    @Override
+    public IBlockData getTypeIfLoaded(BlockPosition blockposition) {
+        return this.getType(blockposition);
+    }
+    // Paper end
+
     @Override
     public IBlockData getType(BlockPosition blockposition) {
         int i = blockposition.getY();
diff --git a/src/main/java/net/minecraft/server/RegionFile.java b/src/main/java/net/minecraft/server/RegionFile.java
index 43c5b8258d9cd15d86d4160e9c614c7ca6152962..1ce85ab949213efb9eae6daddca6ac8fb15dd472 100644
--- a/src/main/java/net/minecraft/server/RegionFile.java
+++ b/src/main/java/net/minecraft/server/RegionFile.java
@@ -93,6 +93,7 @@ public class RegionFile implements AutoCloseable {
         return this.d.resolve(s);
     }
 
+    @Nullable public synchronized DataInputStream getReadStream(ChunkCoordIntPair chunkCoordIntPair) throws IOException { return a(chunkCoordIntPair);} // Paper - OBFHELPER
     @Nullable
     public synchronized DataInputStream a(ChunkCoordIntPair chunkcoordintpair) throws IOException {
         int i = this.getOffset(chunkcoordintpair);
diff --git a/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java b/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java
index a2ec45a6b8bd63299508113e3522436cea2507e5..478d252953c65792df9f0068a4c1afd1985151ab 100644
--- a/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java
+++ b/src/main/java/net/minecraft/server/RegionLimitedWorldAccess.java
@@ -101,6 +101,26 @@ public class RegionLimitedWorldAccess implements GeneratorAccessSeed {
         return i >= this.n.x && i <= this.o.x && j >= this.n.z && j <= this.o.z;
     }
 
+    // Paper start - if loaded util
+    @Nullable
+    @Override
+    public IChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+        return this.getChunkAt(x, z, ChunkStatus.FULL, false);
+    }
+
+    @Override
+    public IBlockData getTypeIfLoaded(BlockPosition blockposition) {
+        IChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+        return chunk == null ? null : chunk.getType(blockposition);
+    }
+
+    @Override
+    public Fluid getFluidIfLoaded(BlockPosition blockposition) {
+        IChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+        return chunk == null ? null : chunk.getFluid(blockposition);
+    }
+    // Paper end
+
     @Override
     public IBlockData getType(BlockPosition blockposition) {
         return this.getChunkAt(blockposition.getX() >> 4, blockposition.getZ() >> 4).getType(blockposition);
diff --git a/src/main/java/net/minecraft/server/RegistryBlockID.java b/src/main/java/net/minecraft/server/RegistryBlockID.java
index 4efcb8b595750891b421e524812542f0f67e9f3f..60948afa4ead71010dc27c7cef3e5acdb0ba005a 100644
--- a/src/main/java/net/minecraft/server/RegistryBlockID.java
+++ b/src/main/java/net/minecraft/server/RegistryBlockID.java
@@ -57,6 +57,7 @@ public class RegistryBlockID<T> implements Registry<T> {
         return Iterators.filter(this.c.iterator(), Predicates.notNull());
     }
 
+    public int size() { return this.a(); } // Paper - OBFHELPER
     public int a() {
         return this.b.size();
     }
diff --git a/src/main/java/net/minecraft/server/SystemUtils.java b/src/main/java/net/minecraft/server/SystemUtils.java
index 80ee8d196436e271833503a0e123401d1b3314a9..275c1d2d1eb2649de9a9b5aece6e88c21362efba 100644
--- a/src/main/java/net/minecraft/server/SystemUtils.java
+++ b/src/main/java/net/minecraft/server/SystemUtils.java
@@ -68,7 +68,7 @@ public class SystemUtils {
     }
 
     public static long getMonotonicNanos() {
-        return SystemUtils.a.getAsLong();
+        return System.nanoTime(); // Paper
     }
 
     public static long getTimeMillis() {
diff --git a/src/main/java/net/minecraft/server/TicketType.java b/src/main/java/net/minecraft/server/TicketType.java
index f82db93f88223ffddc55deec8f21efc5b774d900..75ab9f185b3231113dfa387c956a707b403bb2db 100644
--- a/src/main/java/net/minecraft/server/TicketType.java
+++ b/src/main/java/net/minecraft/server/TicketType.java
@@ -21,6 +21,7 @@ public class TicketType<T> {
     public static final TicketType<ChunkCoordIntPair> UNKNOWN = a("unknown", Comparator.comparingLong(ChunkCoordIntPair::pair), 1);
     public static final TicketType<Unit> PLUGIN = a("plugin", (a, b) -> 0); // CraftBukkit
     public static final TicketType<org.bukkit.plugin.Plugin> PLUGIN_TICKET = a("plugin_ticket", (plugin1, plugin2) -> plugin1.getClass().getName().compareTo(plugin2.getClass().getName())); // CraftBukkit
+    public static final TicketType<Long> FUTURE_AWAIT = a("future_await", Long::compareTo); // Paper
 
     public static <T> TicketType<T> a(String s, Comparator<T> comparator) {
         return new TicketType<>(s, comparator, 0L);
diff --git a/src/main/java/net/minecraft/server/VoxelShapes.java b/src/main/java/net/minecraft/server/VoxelShapes.java
index 3c08ae0cf92ab1f5395662e957ab4d9c03e13deb..86f6f082fe2991ea9065b09c9680b76ca1cf7154 100644
--- a/src/main/java/net/minecraft/server/VoxelShapes.java
+++ b/src/main/java/net/minecraft/server/VoxelShapes.java
@@ -21,10 +21,12 @@ public final class VoxelShapes {
     public static final VoxelShape a = create(Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY, Double.POSITIVE_INFINITY);
     private static final VoxelShape c = new VoxelShapeArray(new VoxelShapeBitSet(0, 0, 0), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D}), new DoubleArrayList(new double[]{0.0D}));
 
+    public static final VoxelShape empty() {return a();} // Paper - OBFHELPER
     public static VoxelShape a() {
         return VoxelShapes.c;
     }
 
+    public static final VoxelShape fullCube() {return b();} // Paper - OBFHELPER
     public static VoxelShape b() {
         return VoxelShapes.b;
     }
diff --git a/src/main/java/net/minecraft/server/World.java b/src/main/java/net/minecraft/server/World.java
index 63d3d43f74bed94cd03aa3b7254e66302be861d5..bd5a2eee9cb435a5ee75a39cea231151fba00387 100644
--- a/src/main/java/net/minecraft/server/World.java
+++ b/src/main/java/net/minecraft/server/World.java
@@ -22,6 +22,7 @@ import org.bukkit.craftbukkit.SpigotTimings; // Spigot
 import org.bukkit.craftbukkit.CraftServer;
 import org.bukkit.craftbukkit.CraftWorld;
 import org.bukkit.craftbukkit.block.CapturedBlockState;
+import org.bukkit.craftbukkit.block.CraftBlockState;
 import org.bukkit.craftbukkit.block.data.CraftBlockData;
 import org.bukkit.event.block.BlockPhysicsEvent;
 // CraftBukkit end
@@ -95,7 +96,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
 
     protected World(WorldDataMutable worlddatamutable, ResourceKey<World> resourcekey, ResourceKey<DimensionManager> resourcekey1, DimensionManager dimensionmanager, Supplier<GameProfilerFiller> supplier, boolean flag, boolean flag1, long i, org.bukkit.generator.ChunkGenerator gen, org.bukkit.World.Environment env) {
         this.spigotConfig = new org.spigotmc.SpigotWorldConfig(((WorldDataServer) worlddatamutable).getName()); // Spigot
-        this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig(worlddata.getName(), this.spigotConfig); // Paper
+        this.paperConfig = new com.destroystokyo.paper.PaperWorldConfig((((WorldDataServer)worlddatamutable).getName()), this.spigotConfig); // Paper
         this.generator = gen;
         this.world = new CraftWorld((WorldServer) this, gen, env);
         this.ticksPerAnimalSpawns = this.getServer().getTicksPerAnimalSpawns(); // CraftBukkit
@@ -247,6 +248,39 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
         return (Chunk) this.getChunkAt(i, j, ChunkStatus.FULL);
     }
 
+    // Paper start - if loaded
+    @Nullable
+    @Override
+    public IChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+        return ((WorldServer)this).chunkProvider.getChunkAtIfLoadedImmediately(x, z);
+    }
+
+    @Override
+    public IBlockData getTypeIfLoaded(BlockPosition blockposition) {
+        // CraftBukkit start - tree generation
+        if (captureTreeGeneration) {
+            CraftBlockState previous = capturedBlockStates.get(blockposition);
+            if (previous != null) {
+                return previous.getHandle();
+            }
+        }
+        // CraftBukkit end
+        if (!isValidLocation(blockposition)) {
+            return Blocks.AIR.getBlockData();
+        }
+        IChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+
+        return chunk == null ? null : chunk.getType(blockposition);
+    }
+
+    @Override
+    public Fluid getFluidIfLoaded(BlockPosition blockposition) {
+        IChunkAccess chunk = this.getChunkIfLoadedImmediately(blockposition.getX() >> 4, blockposition.getZ() >> 4);
+
+        return chunk == null ? null : chunk.getFluid(blockposition);
+    }
+    // Paper end
+
     @Override
     public IChunkAccess getChunkAt(int i, int j, ChunkStatus chunkstatus, boolean flag) {
         IChunkAccess ichunkaccess = this.getChunkProvider().getChunkAt(i, j, chunkstatus, flag);
@@ -405,8 +439,9 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
 
     public void a(BlockPosition blockposition, IBlockData iblockdata, IBlockData iblockdata1) {}
 
-    @Override
-    public boolean a(BlockPosition blockposition, boolean flag) {
+    public boolean setAir(BlockPosition blockposition) { return this.a(blockposition, false); } // Paper - OBFHELPER
+    public boolean setAir(BlockPosition blockposition, boolean moved) { return this.a(blockposition, moved); } // Paper - OBFHELPER
+    @Override public boolean a(BlockPosition blockposition, boolean flag) { // Paper - OBFHELPER
         Fluid fluid = this.getFluid(blockposition);
 
         return this.setTypeAndData(blockposition, fluid.getBlockData(), 3 | (flag ? 64 : 0));
diff --git a/src/main/java/net/minecraft/server/WorldBorder.java b/src/main/java/net/minecraft/server/WorldBorder.java
index ddc4570139fd9b82f4b740697e84775f5ff0a75a..d039e715624d33fc3ec9e87d5ad992415e7dc6b9 100644
--- a/src/main/java/net/minecraft/server/WorldBorder.java
+++ b/src/main/java/net/minecraft/server/WorldBorder.java
@@ -37,6 +37,7 @@ public class WorldBorder {
         return this.b(entity.locX(), entity.locZ());
     }
 
+    public final VoxelShape asVoxelShape(){ return c();} // Paper - OBFHELPER
     public VoxelShape c() {
         return this.j.m();
     }
diff --git a/src/main/java/net/minecraft/server/WorldServer.java b/src/main/java/net/minecraft/server/WorldServer.java
index bd6dfddb3c076cf92588dd508975db0eee5c2962..d643ff0a0ae9d45711c9a40fc12af68157604a33 100644
--- a/src/main/java/net/minecraft/server/WorldServer.java
+++ b/src/main/java/net/minecraft/server/WorldServer.java
@@ -56,7 +56,7 @@ public class WorldServer extends World implements GeneratorAccessSeed {
     private final Map<UUID, Entity> entitiesByUUID = Maps.newHashMap();
     private final Queue<Entity> entitiesToAdd = Queues.newArrayDeque();
     private final List<EntityPlayer> players = Lists.newArrayList();
-    private final ChunkProviderServer chunkProvider;
+    public final ChunkProviderServer chunkProvider; // Paper - public
     boolean tickingEntities;
     private final MinecraftServer server;
     public final WorldDataServer worldDataServer; // CraftBukkit - type
diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
index 62cdb32a63cdcbaee80946f73d23817bbb455488..721a1c6bd4505cb132e7004c45b795d4959389e3 100644
--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemStack.java
@@ -85,6 +85,7 @@ public final class CraftItemStack extends ItemStack {
     }
 
     net.minecraft.server.ItemStack handle;
+    public net.minecraft.server.ItemStack getHandle() { return handle; } // Paper
 
     /**
      * Mirror
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
index 9ad17c560c8d99a396543ab9f97c34de648f6544..4bf48f77f3f7cd62a91590543f5af441c8268029 100644
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftScheduler.java
@@ -43,6 +43,7 @@ import org.bukkit.scheduler.BukkitWorker;
  */
 public class CraftScheduler implements BukkitScheduler {
 
+    static Plugin MINECRAFT = new MinecraftInternalPlugin();
     /**
      * Counter for IDs. Order doesn't matter, only uniqueness.
      */
@@ -177,6 +178,11 @@ public class CraftScheduler implements BukkitScheduler {
         runTaskTimer(plugin, (Object) task, delay, period);
     }
 
+    public BukkitTask scheduleInternalTask(Runnable run, int delay, String taskName) {
+        final CraftTask task = new CraftTask(run, nextId(), taskName);
+        return handle(task, delay);
+    }
+
     public BukkitTask runTaskTimer(Plugin plugin, Object runnable, long delay, long period) {
         validate(plugin, runnable);
         if (delay < 0L) {
@@ -400,13 +406,20 @@ public class CraftScheduler implements BukkitScheduler {
                     task.run();
                     task.timings.stopTiming(); // Spigot
                 } catch (final Throwable throwable) {
-                    task.getOwner().getLogger().log(
+                    // Paper start
+                    String msg = String.format(
+                        "Task #%s for %s generated an exception",
+                        task.getTaskId(),
+                        task.getOwner().getDescription().getFullName());
+                    if (task.getOwner() == MINECRAFT) {
+                        net.minecraft.server.MinecraftServer.LOGGER.error(msg, throwable);
+                    } else {
+                        task.getOwner().getLogger().log(
                             Level.WARNING,
-                            String.format(
-                                "Task #%s for %s generated an exception",
-                                task.getTaskId(),
-                                task.getOwner().getDescription().getFullName()),
+                            msg,
                             throwable);
+                    }
+                    // Paper end
                 } finally {
                     currentTask = null;
                 }
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java
index 3f55381c152b9841b524f623c9b32360e97cb8ed..d85e21b75054067b926ecfee89d62c6dd0744189 100644
--- a/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/CraftTask.java
@@ -39,6 +39,21 @@ public class CraftTask implements BukkitTask, Runnable { // Spigot
     CraftTask(final Object task) {
         this(null, task, CraftTask.NO_REPEATING, CraftTask.NO_REPEATING);
     }
+    // Paper start
+    public String taskName = null;
+    boolean internal = false;
+    CraftTask(final Object task, int id, String taskName) {
+        this.rTask = (Runnable) task;
+        this.cTask = null;
+        this.plugin = CraftScheduler.MINECRAFT;
+        this.taskName = taskName;
+        this.internal = true;
+        this.id = id;
+        this.period = CraftTask.NO_REPEATING;
+        this.taskName = taskName;
+        this.timings = null; // Will be changed in later patch
+    }
+    // Paper end
 
     CraftTask(final Plugin plugin, final Object task, final int id, final long period) {
         this.plugin = plugin;
diff --git a/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
new file mode 100644
index 0000000000000000000000000000000000000000..49dc0c441b9dd7e7745cf15ced67f383ebee1f99
--- /dev/null
+++ b/src/main/java/org/bukkit/craftbukkit/scheduler/MinecraftInternalPlugin.java
@@ -0,0 +1,132 @@
+package org.bukkit.craftbukkit.scheduler;
+
+
+import org.bukkit.Server;
+import org.bukkit.command.Command;
+import org.bukkit.command.CommandSender;
+import org.bukkit.configuration.file.FileConfiguration;
+import org.bukkit.generator.ChunkGenerator;
+import org.bukkit.plugin.PluginBase;
+import org.bukkit.plugin.PluginDescriptionFile;
+import org.bukkit.plugin.PluginLoader;
+import org.bukkit.plugin.PluginLogger;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.List;
+
+public class MinecraftInternalPlugin extends PluginBase {
+    private boolean enabled = true;
+
+    private final String pluginName;
+    private PluginDescriptionFile pdf;
+
+    public MinecraftInternalPlugin() {
+        this.pluginName = "Minecraft";
+        pdf = new PluginDescriptionFile(pluginName, "1.0", "nms");
+    }
+
+    public void setEnabled(boolean enabled) {
+        this.enabled = enabled;
+    }
+
+    @Override
+    public File getDataFolder() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public PluginDescriptionFile getDescription() {
+        return pdf;
+    }
+
+    @Override
+    public FileConfiguration getConfig() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public InputStream getResource(String filename) {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public void saveConfig() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public void saveDefaultConfig() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public void saveResource(String resourcePath, boolean replace) {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public void reloadConfig() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public PluginLogger getLogger() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public PluginLoader getPluginLoader() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public Server getServer() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return enabled;
+    }
+
+    @Override
+    public void onDisable() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public void onLoad() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public void onEnable() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public boolean isNaggable() {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public void setNaggable(boolean canNag) {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+
+    @Override
+    public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
+        throw new UnsupportedOperationException("Not supported.");
+    }
+}
diff --git a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
index eb87f3720977e4e980d26e9cfd4fed0a808636dd..b6c39bf77e716addc9d2a1e915687bc770e937f4 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/DummyGeneratorAccess.java
@@ -187,4 +187,22 @@ public class DummyGeneratorAccess implements GeneratorAccess {
     public boolean a(BlockPosition blockposition, boolean flag, Entity entity, int i) {
         throw new UnsupportedOperationException("Not supported yet.");
     }
+
+    // Paper start - if loaded util
+    @javax.annotation.Nullable
+    @Override
+    public IChunkAccess getChunkIfLoadedImmediately(int x, int z) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public IBlockData getTypeIfLoaded(BlockPosition blockposition) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+
+    @Override
+    public Fluid getFluidIfLoaded(BlockPosition blockposition) {
+        throw new UnsupportedOperationException("Not supported yet.");
+    }
+    // Paper end
 }
diff --git a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
index 1aec70a1f1a9d8fd2cd06bde4033e19e769ab331..f72c13bedaa6fa45e26f5dcad564835bdd4af61f 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
@@ -17,7 +17,7 @@ import java.util.RandomAccess;
 public class UnsafeList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
     private static final long serialVersionUID = 8683452581112892191L;
 
-    private transient Object[] data;
+    private transient Object[] data; public final Object[] getRawDataArray() { return this.data; } // Paper - expose for raw get
     private int size;
     private int initialCapacity;