From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <spottedleaf@spottedleaf.dev>
Date: Thu, 27 Aug 2020 09:40:16 -0700
Subject: [PATCH] Optimise closest entity lookup

Rewrites the entity slice storage so that entity by
class lookups look through less entities in total.

Also optimise the nearest entity by class method
used by entity AI as well.

As a sidenote, this entity slice implementation
removes the async catchers because it has been
designed to be MT-Safe for reads off of other
threads.

diff --git a/src/main/java/com/tuinity/tuinity/util/CachedLists.java b/src/main/java/com/tuinity/tuinity/util/CachedLists.java
index 866f6e701c6a10a3ac1e58d12bba966c478d2e77..e0a2f10f92430fd59519e57dee3286035c287bf3 100644
--- a/src/main/java/com/tuinity/tuinity/util/CachedLists.java
+++ b/src/main/java/com/tuinity/tuinity/util/CachedLists.java
@@ -1,6 +1,7 @@
 package com.tuinity.tuinity.util;
 
 import net.minecraft.world.phys.AxisAlignedBB;
+import net.minecraft.world.level.chunk.Chunk;
 import net.minecraft.world.entity.Entity;
 import org.bukkit.Bukkit;
 import org.bukkit.craftbukkit.util.UnsafeList;
@@ -46,8 +47,28 @@ public class CachedLists {
         tempGetEntitiesListInUse = false;
     }
 
+    static final UnsafeList<Chunk> TEMP_GET_CHUNKS_LIST = new UnsafeList<>(1024);
+    static boolean tempGetChunksListInUse;
+
+    public static UnsafeList<Chunk> getTempGetChunksList() {
+        if (!Bukkit.isPrimaryThread() || tempGetChunksListInUse) {
+            return new UnsafeList<>();
+        }
+        tempGetChunksListInUse = true;
+        return TEMP_GET_CHUNKS_LIST;
+    }
+
+    public static void returnTempGetChunksList(List<Chunk> list) {
+        if (list != TEMP_GET_CHUNKS_LIST) {
+            return;
+        }
+        ((UnsafeList)list).setSize(0);
+        tempGetChunksListInUse = false;
+    }
+
     public static void reset() {
         TEMP_COLLISION_LIST.completeReset();
         TEMP_GET_ENTITIES_LIST.completeReset();
+        TEMP_GET_CHUNKS_LIST.completeReset();
     }
 }
diff --git a/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java
new file mode 100644
index 0000000000000000000000000000000000000000..57359791192a90b025e5fea38f3ce37e524aed82
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/world/ChunkEntitySlices.java
@@ -0,0 +1,399 @@
+package com.tuinity.tuinity.world;
+
+import it.unimi.dsi.fastutil.objects.Reference2ObjectMap;
+import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
+import net.minecraft.world.entity.boss.EntityComplexPart;
+import net.minecraft.world.entity.boss.enderdragon.EntityEnderDragon;
+import net.minecraft.world.phys.AxisAlignedBB;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.EntityTypes;
+import net.minecraft.util.MathHelper;
+import net.minecraft.world.level.World;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Predicate;
+
+public final class ChunkEntitySlices {
+
+    private static final int RTREE_THRESHOLD = 20;
+
+    protected final int minSection;
+    protected final int maxSection;
+    protected final int chunkX;
+    protected final int chunkZ;
+    protected final World world;
+
+    protected final EntityCollectionBySection allEntities;
+    protected final EntityCollectionBySection hardCollidingEntities;
+    protected final Reference2ObjectOpenHashMap<Class<? extends Entity>, EntityCollectionBySection> entitiesByClass;
+
+    public ChunkEntitySlices(final World world, final int chunkX, final int chunkZ,
+                             final int minSection, final int maxSection) { // inclusive, inclusive
+        this.minSection = minSection;
+        this.maxSection = maxSection;
+        this.chunkX = chunkX;
+        this.chunkZ = chunkZ;
+        this.world = world;
+
+        this.allEntities = new EntityCollectionBySection(this);
+        this.hardCollidingEntities = new EntityCollectionBySection(this);
+        this.entitiesByClass = new Reference2ObjectOpenHashMap<>();
+    }
+
+    // synchronized is used in this class for write protection, thank you dumbass mods for doing dumb
+    // shit async.
+
+    public synchronized void addEntity(final Entity entity, final int chunkSection) {
+        final int sectionIndex = chunkSection - this.minSection;
+
+        this.allEntities.addEntity(entity, sectionIndex);
+
+        if (entity.hardCollides()) {
+            this.hardCollidingEntities.addEntity(entity, sectionIndex);
+        }
+
+        for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
+             this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+            final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
+
+            if (entry.getKey().isInstance(entity)) {
+                entry.getValue().addEntity(entity, sectionIndex);
+            }
+        }
+    }
+
+    public synchronized void removeEntity(final Entity entity, final int chunkSection) {
+        final int sectionIndex = chunkSection - this.minSection;
+
+        this.allEntities.removeEntity(entity, sectionIndex);
+
+        if (entity.hardCollides()) {
+            this.hardCollidingEntities.removeEntity(entity, sectionIndex);
+        }
+
+        for (final Iterator<Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection>> iterator =
+             this.entitiesByClass.reference2ObjectEntrySet().fastIterator(); iterator.hasNext();) {
+            final Reference2ObjectMap.Entry<Class<? extends Entity>, EntityCollectionBySection> entry = iterator.next();
+
+            if (entry.getKey().isInstance(entity)) {
+                entry.getValue().removeEntity(entity, sectionIndex);
+            }
+        }
+    }
+
+    public void getHardCollidingEntities(final Entity except, final AxisAlignedBB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+        this.hardCollidingEntities.getEntities(except, box, into, predicate);
+    }
+
+    public void getEntities(final Entity except, final AxisAlignedBB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+        this.allEntities.getEntitiesWithEnderDragonParts(except, box, into, predicate);
+    }
+
+    public <T extends Entity> void getEntities(final EntityTypes<?> type, final AxisAlignedBB box, final List<? super T> into,
+                                               final Predicate<? super T> predicate) {
+        this.allEntities.getEntities(type, box, (List)into, (Predicate)predicate);
+    }
+
+    protected EntityCollectionBySection initClass(final Class<? extends Entity> clazz) {
+        final EntityCollectionBySection ret = new EntityCollectionBySection(this);
+
+        for (int sectionIndex = 0; sectionIndex < this.allEntities.entitiesBySection.length; ++sectionIndex) {
+            final BasicEntityList<Entity> sectionEntities = this.allEntities.entitiesBySection[sectionIndex];
+            if (sectionEntities == null) {
+                continue;
+            }
+
+            final Entity[] storage = sectionEntities.storage;
+
+            for (int i = 0, len = Math.min(storage.length, sectionEntities.size()); i < len; ++i) {
+                final Entity entity = storage[i];
+
+                if (clazz.isInstance(entity)) {
+                    ret.addEntity(entity, sectionIndex);
+                }
+            }
+        }
+
+        return ret;
+    }
+
+    public <T extends Entity> void getEntities(final Class<? extends T> clazz, final Entity except, final AxisAlignedBB box, final List<? super T> into,
+                                               final Predicate<? super T> predicate) {
+        EntityCollectionBySection collection = this.entitiesByClass.get(clazz);
+        if (collection != null) {
+            collection.getEntities(except, box, (List)into, (Predicate)predicate);
+        } else {
+            synchronized (this) {
+                this.entitiesByClass.putIfAbsent(clazz, collection = this.initClass(clazz));
+            }
+            collection.getEntities(except, box, (List)into, (Predicate)predicate);
+        }
+    }
+
+    public synchronized void updateEntity(final Entity entity) {
+        /*// TODO
+        if (prev aabb != entity.getBoundingBox()) {
+            this.entityMap.delete(entity, prev aabb);
+            this.entityMap.insert(entity, prev aabb = entity.getBoundingBox());
+        }*/
+    }
+
+    protected static final class BasicEntityList<E extends Entity> {
+
+        protected static final Entity[] EMPTY = new Entity[0];
+        protected static final int DEFAULT_CAPACITY = 4;
+
+        protected E[] storage;
+        protected int size;
+
+        public BasicEntityList() {
+            this(0);
+        }
+
+        public BasicEntityList(final int cap) {
+            this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]);
+        }
+
+        public boolean isEmpty() {
+            return this.size == 0;
+        }
+
+        public int size() {
+            return this.size;
+        }
+
+        private void resize() {
+            if (this.storage == EMPTY) {
+                this.storage = (E[])new Entity[DEFAULT_CAPACITY];
+            } else {
+                this.storage = Arrays.copyOf(this.storage, this.storage.length * 2);
+            }
+        }
+
+        public void add(final E entity) {
+            final int idx = this.size++;
+            if (idx >= this.storage.length) {
+                this.resize();
+                this.storage[idx] = entity;
+            } else {
+                this.storage[idx] = entity;
+            }
+        }
+
+        public int indexOf(final E entity) {
+            final E[] storage = this.storage;
+
+            for (int i = 0, len = Math.min(this.storage.length, this.size); i < len; ++i) {
+                if (storage[i] == entity) {
+                    return i;
+                }
+            }
+
+            return -1;
+        }
+
+        public boolean remove(final E entity) {
+            final int idx = this.indexOf(entity);
+            if (idx == -1) {
+                return false;
+            }
+
+            final int size = --this.size;
+            final E[] storage = this.storage;
+            if (idx != size) {
+                System.arraycopy(storage, idx + 1, storage, idx, size - idx);
+            }
+
+            storage[size] = null;
+
+            return true;
+        }
+
+        public boolean has(final E entity) {
+            return this.indexOf(entity) != -1;
+        }
+    }
+
+    protected static final class EntityCollectionBySection {
+
+        protected final ChunkEntitySlices manager;
+        protected final long[] nonEmptyBitset;
+        protected final BasicEntityList<Entity>[] entitiesBySection;
+        protected int count;
+
+        public EntityCollectionBySection(final ChunkEntitySlices manager) {
+            this.manager = manager;
+
+            final int sectionCount = manager.maxSection - manager.minSection + 1;
+
+            this.nonEmptyBitset = new long[(sectionCount + (Long.SIZE - 1)) >>> 6]; // (sectionCount + (Long.SIZE - 1)) / Long.SIZE
+            this.entitiesBySection = new BasicEntityList[sectionCount];
+        }
+
+        public void addEntity(final Entity entity, final int sectionIndex) {
+            BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
+
+            if (list != null && list.has(entity)) {
+                return;
+            }
+
+            if (list == null) {
+                this.entitiesBySection[sectionIndex] = list = new BasicEntityList<>();
+                this.nonEmptyBitset[sectionIndex >>> 6] |= (1L << (sectionIndex & (Long.SIZE - 1)));
+            }
+
+            list.add(entity);
+            ++this.count;
+        }
+
+        public void removeEntity(final Entity entity, final int sectionIndex) {
+            final BasicEntityList<Entity> list = this.entitiesBySection[sectionIndex];
+
+            if (list == null || !list.remove(entity)) {
+                return;
+            }
+
+            --this.count;
+
+            if (list.isEmpty()) {
+                this.entitiesBySection[sectionIndex] = null;
+                this.nonEmptyBitset[sectionIndex >>> 6] ^= (1L << (sectionIndex & (Long.SIZE - 1)));
+            }
+        }
+
+        public void getEntities(final Entity except, final AxisAlignedBB box, final List<Entity> into, final Predicate<? super Entity> predicate) {
+            if (this.count == 0) {
+                return;
+            }
+
+            final int minSection = this.manager.minSection;
+            final int maxSection = this.manager.maxSection;
+
+            final int min = MathHelper.clamp(MathHelper.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+            final int max = MathHelper.clamp(MathHelper.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+            // TODO use the bitset
+
+            final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+            for (int section = min; section <= max; ++section) {
+                final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+                if (list == null) {
+                    continue;
+                }
+
+                final Entity[] storage = list.storage;
+
+                for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+                    final Entity entity = storage[i];
+
+                    if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+                        continue;
+                    }
+
+                    if (predicate != null && !predicate.test(entity)) {
+                        continue;
+                    }
+
+                    into.add(entity);
+                }
+            }
+        }
+
+        public void getEntitiesWithEnderDragonParts(final Entity except, final AxisAlignedBB box, final List<Entity> into,
+                                                    final Predicate<? super Entity> predicate) {
+            if (this.count == 0) {
+                return;
+            }
+
+            final int minSection = this.manager.minSection;
+            final int maxSection = this.manager.maxSection;
+
+            final int min = MathHelper.clamp(MathHelper.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+            final int max = MathHelper.clamp(MathHelper.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+            // TODO use the bitset
+
+            final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+            for (int section = min; section <= max; ++section) {
+                final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+                if (list == null) {
+                    continue;
+                }
+
+                final Entity[] storage = list.storage;
+
+                for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+                    final Entity entity = storage[i];
+
+                    if (entity == null || entity == except || !entity.getBoundingBox().intersects(box)) {
+                        continue;
+                    }
+
+                    if (predicate == null || predicate.test(entity)) {
+                        into.add(entity);
+                    } // else: continue to test the ender dragon parts
+
+                    if (entity instanceof EntityEnderDragon) {
+                        for (final EntityComplexPart part : ((EntityEnderDragon)entity).children) {
+                            if (part == except || !part.getBoundingBox().intersects(box)) {
+                                continue;
+                            }
+
+                            if (predicate != null && !predicate.test(part)) {
+                                continue;
+                            }
+
+                            into.add(part);
+                        }
+                    }
+                }
+            }
+        }
+
+        public <T extends Entity> void getEntities(final EntityTypes<?> type, final AxisAlignedBB box, final List<? super T> into,
+                                                   final Predicate<? super T> predicate) {
+            if (this.count == 0) {
+                return;
+            }
+
+            final int minSection = this.manager.minSection;
+            final int maxSection = this.manager.maxSection;
+
+            final int min = MathHelper.clamp(MathHelper.floor(box.minY - 2.0) >> 4, minSection, maxSection);
+            final int max = MathHelper.clamp(MathHelper.floor(box.maxY + 2.0) >> 4, minSection, maxSection);
+
+            // TODO use the bitset
+
+            final BasicEntityList<Entity>[] entitiesBySection = this.entitiesBySection;
+
+            for (int section = min; section <= max; ++section) {
+                final BasicEntityList<Entity> list = entitiesBySection[section - minSection];
+
+                if (list == null) {
+                    continue;
+                }
+
+                final Entity[] storage = list.storage;
+
+                for (int i = 0, len = Math.min(storage.length, list.size()); i < len; ++i) {
+                    final Entity entity = storage[i];
+
+                    if (entity == null || (type != null && entity.getEntityType() != type) || !entity.getBoundingBox().intersects(box)) {
+                        continue;
+                    }
+
+                    if (predicate != null && !predicate.test((T)entity)) {
+                        continue;
+                    }
+
+                    into.add((T)entity);
+                }
+            }
+        }
+    }
+}
diff --git a/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java
new file mode 100644
index 0000000000000000000000000000000000000000..3ec78275494f0f0b7ad3c53467ec6c6395cb257d
--- /dev/null
+++ b/src/main/java/com/tuinity/tuinity/world/EntitySliceManager.java
@@ -0,0 +1,115 @@
+package com.tuinity.tuinity.world;
+
+import com.tuinity.tuinity.util.CoordinateUtils;
+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
+import net.minecraft.server.level.WorldServer;
+import java.util.concurrent.locks.StampedLock;
+
+public final class EntitySliceManager {
+
+    protected static final int REGION_SHIFT = 5;
+    protected static final int REGION_MASK = (1 << REGION_SHIFT) - 1;
+    protected static final int REGION_SIZE = 1 << REGION_SHIFT;
+
+    public final WorldServer world;
+
+    private final StampedLock stateLock = new StampedLock();
+    protected final Long2ObjectOpenHashMap<ChunkSlicesRegion> regions = new Long2ObjectOpenHashMap<>(32, 0.7f);
+
+    public EntitySliceManager(final WorldServer world) {
+        this.world = world;
+    }
+
+    public ChunkSlicesRegion getRegion(final int regionX, final int regionZ) {
+        final long key = CoordinateUtils.getChunkKey(regionX, regionZ);
+        final long attempt = this.stateLock.tryOptimisticRead();
+        if (attempt != 0L) {
+            try {
+                final ChunkSlicesRegion ret = this.regions.get(key);
+
+                if (this.stateLock.validate(attempt)) {
+                    return ret;
+                }
+            } catch (final Error error) {
+                throw error;
+            } catch (final Throwable thr) {
+                // ignore
+            }
+        }
+
+        this.stateLock.readLock();
+        try {
+            return this.regions.get(key);
+        } finally {
+            this.stateLock.tryUnlockRead();
+        }
+    }
+
+    public synchronized void removeChunk(final int chunkX, final int chunkZ) {
+        final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+        final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
+
+        final ChunkSlicesRegion region = this.regions.get(key);
+        final int remaining = region.remove(relIndex);
+
+        if (remaining == 0) {
+            this.stateLock.writeLock();
+            try {
+                this.regions.remove(key);
+            } finally {
+                this.stateLock.tryUnlockWrite();
+            }
+        }
+    }
+
+    public synchronized void addChunk(final int chunkX, final int chunkZ, final ChunkEntitySlices slices) {
+        final long key = CoordinateUtils.getChunkKey(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT);
+        final int relIndex = (chunkX & REGION_MASK) | ((chunkZ & REGION_MASK) << REGION_SHIFT);
+
+        ChunkSlicesRegion region = this.regions.get(key);
+        if (region != null) {
+            region.add(relIndex, slices);
+        } else {
+            region = new ChunkSlicesRegion();
+            region.add(relIndex, slices);
+            this.stateLock.writeLock();
+            try {
+                this.regions.put(key, region);
+            } finally {
+                this.stateLock.tryUnlockWrite();
+            }
+        }
+    }
+
+    public static final class ChunkSlicesRegion {
+
+        protected final ChunkEntitySlices[] slices = new ChunkEntitySlices[REGION_SIZE * REGION_SIZE];
+        protected int sliceCount;
+
+        public ChunkEntitySlices get(final int index) {
+            return this.slices[index];
+        }
+
+        public int remove(final int index) {
+            final ChunkEntitySlices slices = this.slices[index];
+            if (slices == null) {
+                throw new IllegalStateException();
+            }
+
+            this.slices[index] = null;
+
+            return --this.sliceCount;
+        }
+
+        public void add(final int index, final ChunkEntitySlices slices) {
+            final ChunkEntitySlices curr = this.slices[index];
+            if (curr != null) {
+                throw new IllegalStateException();
+            }
+
+            this.slices[index] = slices;
+
+            ++this.sliceCount;
+        }
+    }
+}
diff --git a/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java b/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java
index e6988f7ea428f1503e3db63876b13e57f898ee30..88972dd8252bd2d2d8e384d616484ff682949fa8 100644
--- a/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java
+++ b/src/main/java/net/minecraft/world/entity/ai/targeting/PathfinderTargetCondition.java
@@ -55,6 +55,7 @@ public class PathfinderTargetCondition {
         return this;
     }
 
+    public final boolean test(@Nullable EntityLiving entityliving, EntityLiving entityliving1) { return this.a(entityliving, entityliving1); } // Tuinity - OBFHELPER
     public boolean a(@Nullable EntityLiving entityliving, EntityLiving entityliving1) {
         if (entityliving == entityliving1) {
             return false;
diff --git a/src/main/java/net/minecraft/world/level/IEntityAccess.java b/src/main/java/net/minecraft/world/level/IEntityAccess.java
index 69cc7664089c505eb5cfdc437f16b91e9713eada..7c9ed41ae67bce0e5e45b9c091180ca3c99668fc 100644
--- a/src/main/java/net/minecraft/world/level/IEntityAccess.java
+++ b/src/main/java/net/minecraft/world/level/IEntityAccess.java
@@ -223,12 +223,12 @@ public interface IEntityAccess {
     }
 
     @Nullable
-    default <T extends EntityLiving> T a(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+    default <T extends EntityLiving> T a(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { // Tuinity - diff on change, override in World - this should be "get closest entity by class that matches path finder target condition"
         return this.a(this.a(oclass, axisalignedbb, null), pathfindertargetcondition, entityliving, d0, d1, d2); // Paper - decompile fix
     }
 
     @Nullable
-    default <T extends EntityLiving> T b(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+    default <T extends EntityLiving> T b(Class<? extends T> oclass, PathfinderTargetCondition pathfindertargetcondition, @Nullable EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) { // Tuinity - diff on change, override in World - this should be "get closest entity by class that matches path finder target condition"
         return this.a(this.b(oclass, axisalignedbb, null), pathfindertargetcondition, entityliving, d0, d1, d2); // Paper - decompile fix
     }
 
diff --git a/src/main/java/net/minecraft/world/level/World.java b/src/main/java/net/minecraft/world/level/World.java
index 33b7d8247273d7744a241285f8732847d01e8892..23db2bad910795b1335e247521d383c2dd87a23b 100644
--- a/src/main/java/net/minecraft/world/level/World.java
+++ b/src/main/java/net/minecraft/world/level/World.java
@@ -1246,7 +1246,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
                 Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper
 
                 if (chunk != null) {
-                    chunk.a(oclass, axisalignedbb, list, predicate);
+                    chunk.getEntitiesClass(oclass, null, axisalignedbb, (Predicate)predicate, (List)list); // Tuinity - optimise lookup by entity class
                 }
             }
         }
@@ -1269,7 +1269,7 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
                 Chunk chunk = (Chunk)this.getChunkIfLoadedImmediately(i1, j1); // Paper
 
                 if (chunk != null) {
-                    chunk.a(oclass, axisalignedbb, list, predicate);
+                    chunk.getEntitiesClass(oclass, null, axisalignedbb, (Predicate)predicate, (List)list); // Tuinity - optimise lookup by entity class
                 }
             }
         }
@@ -1277,6 +1277,106 @@ public abstract class World implements GeneratorAccess, AutoCloseable {
         return list;
     }
 
+    // Tuinity start
+    @Override
+    public <T extends net.minecraft.world.entity.EntityLiving> T b(Class<? extends T> oclass, net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition pathfindertargetcondition, @Nullable net.minecraft.world.entity.EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+        return this.getClosestEntity(oclass, pathfindertargetcondition, entityliving, d0, d1, d2, axisalignedbb);
+    }
+
+    @Override
+    public <T extends net.minecraft.world.entity.EntityLiving> T a(Class<? extends T> oclass, net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition pathfindertargetcondition, @Nullable net.minecraft.world.entity.EntityLiving entityliving, double d0, double d1, double d2, AxisAlignedBB axisalignedbb) {
+        return this.getClosestEntity(oclass, pathfindertargetcondition, entityliving, d0, d1, d2, axisalignedbb);
+    }
+
+    public final <T extends net.minecraft.world.entity.EntityLiving> T getClosestEntity(Class<? extends T> clazz,
+                                                             net.minecraft.world.entity.ai.targeting.PathfinderTargetCondition condition,
+                                                             @Nullable net.minecraft.world.entity.EntityLiving source,
+                                                             double x, double y, double z,
+                                                             AxisAlignedBB boundingBox) {
+        org.bukkit.craftbukkit.util.UnsafeList<Entity> entities = com.tuinity.tuinity.util.CachedLists.getTempGetEntitiesList();
+        try {
+            int lowerX = MCUtil.fastFloor((boundingBox.minX - 2.0D)) >> 4;
+            int upperX = MCUtil.fastFloor((boundingBox.maxX + 2.0D)) >> 4;
+            int lowerZ = MCUtil.fastFloor((boundingBox.minZ - 2.0D)) >> 4;
+            int upperZ = MCUtil.fastFloor((boundingBox.maxZ + 2.0D)) >> 4;
+
+            org.bukkit.craftbukkit.util.UnsafeList<Chunk> chunks = com.tuinity.tuinity.util.CachedLists.getTempGetChunksList();
+            try {
+                T closest = null;
+                double closestDistance = Double.MAX_VALUE;
+                net.minecraft.server.level.ChunkProviderServer chunkProvider = ((WorldServer)this).getChunkProvider();
+
+                int centerX = (lowerX + upperX) >> 1;
+                int centerZ = (lowerZ + upperZ) >> 1;
+                // Copied from MCUtil.getSpiralOutChunks
+                Chunk temp;
+                if ((temp = chunkProvider.getChunkAtIfLoadedImmediately(centerX, centerZ)) != null && temp.hasEntitiesMaybe(clazz)) {
+                    chunks.add(temp);
+                }
+                int radius = Math.max((upperX - lowerX + 1) >> 1, (upperZ - lowerZ + 1) >> 1);
+                for (int r = 1; r <= radius; r++) {
+                    int ox = -r;
+                    int oz = r;
+
+                    // Iterates the edge of half of the box; then negates for other half.
+                    while (ox <= r && oz > -r) {
+                        {
+                            int cx = centerX + ox;
+                            int cz = centerZ + oz;
+                            if (cx >= lowerX && cx <= upperX && cz >= lowerZ && cz <= upperZ &&
+                                    (temp = chunkProvider.getChunkAtIfLoadedImmediately(cx, cz)) != null &&
+                                    temp.hasEntitiesMaybe(clazz)) {
+                                chunks.add(temp);
+                            }
+                        }
+                        {
+                            int cx = centerX - ox;
+                            int cz = centerZ - oz;
+                            if (cx >= lowerX && cx <= upperX && cz >= lowerZ && cz <= upperZ &&
+                                    (temp = chunkProvider.getChunkAtIfLoadedImmediately(cx, cz)) != null &&
+                                    temp.hasEntitiesMaybe(clazz)) {
+                                chunks.add(temp);
+                            }
+                        }
+
+                        if (ox < r) {
+                            ox++;
+                        } else {
+                            oz--;
+                        }
+                    }
+                }
+
+                Object[] chunkData = chunks.getRawDataArray();
+                for (int cindex = 0, clen = chunks.size(); cindex < clen; ++cindex) {
+                    final Chunk chunk = (Chunk)chunkData[cindex];
+
+                    chunk.getEntitiesClass(clazz, source, boundingBox, null, entities);
+
+                    Object[] entityData = entities.getRawDataArray();
+                    for (int eindex = 0, entities_len = entities.size(); eindex < entities_len; ++eindex) {
+                        T entity = (T)entityData[eindex];
+                        double distance = entity.getDistanceSquared(x, y, z);
+                        // check distance first, as it's the least expensive
+                        if (distance < closestDistance && condition.test(source, entity)) {
+                            closest = entity;
+                            closestDistance = distance;
+                        }
+                    }
+
+                    entities.setSize(0);
+                }
+
+                return closest;
+            } finally {
+                com.tuinity.tuinity.util.CachedLists.returnTempGetChunksList(chunks);
+            }
+        } finally {
+            com.tuinity.tuinity.util.CachedLists.returnTempGetEntitiesList(entities);
+        }
+    }
+    // Tuinity end
+
     @Nullable
     public abstract Entity getEntity(int i);
 
diff --git a/src/main/java/net/minecraft/world/level/chunk/Chunk.java b/src/main/java/net/minecraft/world/level/chunk/Chunk.java
index a0b732a68d9961fc8a42e2aaf0d513605552bd98..eb1e4a6c89b79fa1e2ec367eba441970c694f2d1 100644
--- a/src/main/java/net/minecraft/world/level/chunk/Chunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/Chunk.java
@@ -138,54 +138,24 @@ public class Chunk implements IChunkAccess {
     // Paper end
 
     // Tuinity start - optimise hard collision handling
-    final com.destroystokyo.paper.util.maplist.EntityList[] hardCollidingEntities = new com.destroystokyo.paper.util.maplist.EntityList[16];
-
-    {
-        for (int i = 0, len = this.hardCollidingEntities.length; i < len; ++i) {
-            this.hardCollidingEntities[i] = new com.destroystokyo.paper.util.maplist.EntityList();
-        }
-    }
+    // Tuinity - optimised entity slices
 
     public final void getHardCollidingEntities(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List<Entity> into, Predicate<Entity> predicate) {
-        // copied from getEntities
-        int min = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D);
-        int max = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D);
-
-        min = MathHelper.clamp(min, 0, this.hardCollidingEntities.length - 1);
-        max = MathHelper.clamp(max, 0, this.hardCollidingEntities.length - 1);
-
-        for (int k = min; k <= max; ++k) {
-            com.destroystokyo.paper.util.maplist.EntityList entityList = this.hardCollidingEntities[k];
-            Entity[] entities = entityList.getRawData();
-
-            for (int i = 0, len = entityList.size(); i < len; ++i) {
-                Entity entity1 = entities[i];
-                if (entity1.shouldBeRemoved) continue; // Paper
-
-                if (entity1 != entity && entity1.getBoundingBox().intersects(axisalignedbb)) {
-                    if (predicate == null || predicate.test(entity1)) {
-                        into.add(entity1);
-                    }
-
-                    if (!(entity1 instanceof EntityEnderDragon)) {
-                        continue;
-                    }
+        this.entitySlicesManager.getHardCollidingEntities(entity, axisalignedbb, into, predicate); // Tuinity
+    }
+    // Tuinity end - optimise hard collision handling
 
-                    EntityComplexPart[] aentitycomplexpart = ((EntityEnderDragon)entity1).children;
-                    int l = aentitycomplexpart.length;
+    // Tuinity start - optimised entity slices
+    protected final com.tuinity.tuinity.world.ChunkEntitySlices entitySlicesManager;
 
-                    for (int i1 = 0; i1 < l; ++i1) {
-                        EntityComplexPart entitycomplexpart = aentitycomplexpart[i1];
+    public final boolean hasEntitiesMaybe(Class<?> clazz) { // Tuinity start
+        return true; // Tuinity end
+    }
 
-                        if (entitycomplexpart != entity && entitycomplexpart.getBoundingBox().intersects(axisalignedbb) && (predicate == null || predicate.test(entitycomplexpart))) {
-                            into.add(entitycomplexpart);
-                        }
-                    }
-                }
-            }
-        }
+    public final void getEntitiesClass(Class<?> clazz, Entity entity, AxisAlignedBB boundingBox, Predicate<Entity> predicate, List<Entity> into) {
+        this.entitySlicesManager.getEntities((Class)clazz, entity, boundingBox, (List)into, (Predicate)predicate); // Tuinity
     }
-    // Tuinity end - optimise hard collision handling
+    // Tuinity end - optimised entity slices
 
     public Chunk(World world, ChunkCoordIntPair chunkcoordintpair, BiomeStorage biomestorage, ChunkConverter chunkconverter, TickList<Block> ticklist, TickList<FluidType> ticklist1, long i, @Nullable ChunkSection[] achunksection, @Nullable Consumer<Chunk> consumer) {
         this.sections = new ChunkSection[16];
@@ -230,6 +200,7 @@ public class Chunk implements IChunkAccess {
 
         // CraftBukkit start
         this.bukkitChunk = new org.bukkit.craftbukkit.CraftChunk(this);
+        this.entitySlicesManager = new com.tuinity.tuinity.world.ChunkEntitySlices(this.world, this.loc.x, this.loc.z, 0, 15); // TODO update for 1.17 // Tuinity
     }
 
     public org.bukkit.Chunk bukkitChunk;
@@ -690,8 +661,9 @@ 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); if (entity.hardCollides()) this.hardCollidingEntities[k].add(entity); // Tuinity - optimise hard colliding entities
+        this.entities.add(entity); // Tuinity
+        this.entitySlices[k].add(entity);  // Tuinity
+        this.entitySlicesManager.addEntity(entity, k); // Tuinity
         // Paper start
         if (entity instanceof EntityItem) {
             itemCounts[k]++;
@@ -729,7 +701,8 @@ public class Chunk implements IChunkAccess {
             entity.entitySlice = null;
             entity.inChunk = false;
         }
-        if (entity.hardCollides()) this.hardCollidingEntities[i].remove(entity); if (!this.entitySlices[i].remove(entity)) { // Tuinity - optimise hard colliding entities
+        this.entitySlicesManager.removeEntity(entity, i); // Tuinity
+        if (!this.entitySlices[i].remove(entity)) { // Tuinity - optimise hard colliding entities // Tuinity - entities by class // Tuinity
             return;
         }
         if (entity instanceof EntityItem) {
@@ -980,116 +953,18 @@ public class Chunk implements IChunkAccess {
     }
 
     public void a(@Nullable Entity entity, AxisAlignedBB axisalignedbb, List<Entity> list, @Nullable Predicate<? super Entity> predicate) {
-        org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
-        int i = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D);
-        int j = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D);
-
-        i = MathHelper.clamp(i, 0, this.entitySlices.length - 1);
-        j = MathHelper.clamp(j, 0, this.entitySlices.length - 1);
-
-        for (int k = i; k <= j; ++k) {
-            List<Entity> entityslice = this.entitySlices[k]; // Spigot
-            List<Entity> list1 = entityslice; // Spigot
-            int l = list1.size();
-
-            for (int i1 = 0; i1 < l; ++i1) {
-                Entity entity1 = (Entity) list1.get(i1);
-                if (entity1.shouldBeRemoved) continue; // Paper
-
-                if (entity1.getBoundingBox().c(axisalignedbb) && entity1 != entity) {
-                    if (predicate == null || predicate.test(entity1)) {
-                        list.add(entity1);
-                    }
-
-                    if (entity1 instanceof EntityEnderDragon) {
-                        EntityComplexPart[] aentitycomplexpart = ((EntityEnderDragon) entity1).eJ();
-                        int j1 = aentitycomplexpart.length;
-
-                        for (int k1 = 0; k1 < j1; ++k1) {
-                            EntityComplexPart entitycomplexpart = aentitycomplexpart[k1];
-
-                            if (entitycomplexpart != entity && entitycomplexpart.getBoundingBox().c(axisalignedbb) && (predicate == null || predicate.test(entitycomplexpart))) {
-                                list.add(entitycomplexpart);
-                            }
-                        }
-                    }
-                }
-            }
-        }
+        this.entitySlicesManager.getEntities(entity, axisalignedbb, list, predicate); // Tuinity - optimised entity slices
 
     }
 
     public <T extends Entity> void a(@Nullable EntityTypes<?> entitytypes, AxisAlignedBB axisalignedbb, List<? super T> list, Predicate<? super T> predicate) {
-        org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
-        int i = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D);
-        int j = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D);
-
-        i = MathHelper.clamp(i, 0, this.entitySlices.length - 1);
-        j = MathHelper.clamp(j, 0, this.entitySlices.length - 1);
-
-        for (int k = i; k <= j; ++k) {
-            Iterator iterator = this.entitySlices[k].iterator(); // Spigot
-
-            // Paper start - Don't search for inventories if we have none, and that is all we want
-            /*
-             * We check if they want inventories by seeing if it is the static `IEntitySelector.d`
-             *
-             * Make sure the inventory selector stays in sync.
-             * It should be the one that checks `var1 instanceof IInventory && var1.isAlive()`
-             */
-            if (predicate == IEntitySelector.isInventory() && inventoryEntityCounts[k] <= 0) continue;
-            while (iterator.hasNext()) {
-                T entity = (T) iterator.next(); // CraftBukkit - decompile error
-                if (entity.shouldBeRemoved) continue; // Paper
-
-                if ((entitytypes == null || entity.getEntityType() == entitytypes) && entity.getBoundingBox().c(axisalignedbb) && predicate.test(entity)) {
-                    list.add(entity);
-                }
-            }
-        }
+        this.entitySlicesManager.getEntities(entitytypes, axisalignedbb, (List)list, (Predicate)predicate); // Tuinity - optimised entity slices
 
     }
 
+    public final <T extends Entity> void getEntities(Class<? extends T> oclass, AxisAlignedBB axisalignedbb, List<T> list, @Nullable Predicate<? super T> predicate) { this.a(oclass, axisalignedbb, list, predicate); } // Tuinity - OBFHELPER
     public <T extends Entity> void a(Class<? extends T> oclass, AxisAlignedBB axisalignedbb, List<T> list, @Nullable Predicate<? super T> predicate) {
-        org.spigotmc.AsyncCatcher.catchOp("Chunk getEntities call"); // Spigot
-        int i = MathHelper.floor((axisalignedbb.minY - 2.0D) / 16.0D);
-        int j = MathHelper.floor((axisalignedbb.maxY + 2.0D) / 16.0D);
-
-        i = MathHelper.clamp(i, 0, this.entitySlices.length - 1);
-        j = MathHelper.clamp(j, 0, this.entitySlices.length - 1);
-
-        // Paper start
-        int[] counts;
-        if (EntityItem.class.isAssignableFrom(oclass)) {
-            counts = itemCounts;
-        } else if (IInventory.class.isAssignableFrom(oclass)) {
-            counts = inventoryEntityCounts;
-        } else {
-            counts = null;
-        }
-        // Paper end
-        for (int k = i; k <= j; ++k) {
-            if (counts != null && counts[k] <= 0) continue; // Paper - Don't check a chunk if it doesn't have the type we are looking for
-            Iterator iterator = this.entitySlices[k].iterator(); // Spigot
-
-            // Paper start - Don't search for inventories if we have none, and that is all we want
-            /*
-             * We check if they want inventories by seeing if it is the static `IEntitySelector.d`
-             *
-             * Make sure the inventory selector stays in sync.
-             * It should be the one that checks `var1 instanceof IInventory && var1.isAlive()`
-             */
-            if (predicate == IEntitySelector.isInventory() && inventoryEntityCounts[k] <= 0) continue;
-            // Paper end
-            while (iterator.hasNext()) {
-                T t0 = (T) iterator.next(); // CraftBukkit - decompile error
-                if (t0.shouldBeRemoved) continue; // Paper
-
-                if (oclass.isInstance(t0) && t0.getBoundingBox().c(axisalignedbb) && (predicate == null || predicate.test(t0))) { // Spigot - instance check
-                    list.add(t0);
-                }
-            }
-        }
+        this.entitySlicesManager.getEntities(oclass, null, axisalignedbb, list, predicate); // Tuinity - optimised entity slices
 
     }
 
diff --git a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
index 50f855b931dba60754fff9c7cdf5e0e744f00fdd..7c0d90552eeb6de7dab174e2ba4acfc89a7b3db0 100644
--- a/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
+++ b/src/main/java/org/bukkit/craftbukkit/util/UnsafeList.java
@@ -35,6 +35,13 @@ public class UnsafeList<E> extends AbstractList<E> implements List<E>, RandomAcc
         iterPool[0] = new Itr();
     }
 
+    // Tuinity start
+    @Override
+    public void sort(java.util.Comparator<? super E> c) {
+        Arrays.sort((E[])this.data, 0, size, c);
+    }
+    // Tuinity end
+
     public UnsafeList(int capacity) {
         this(capacity, 5);
     }
