From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Thu, 27 Aug 2020 16:22:52 -0700
Subject: [PATCH] Optimise nearby player lookups

Use a distance map to map out close players.
Note that it's important that we cache the distance map value per chunk
since the penalty of a map lookup could outweigh the benefits of
searching less players (as it basically did in the outside range patch).

diff --git a/src/main/java/net/minecraft/server/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index 4ae1ba645d9fdc1eb6d5a3e4f8ceed9b4841e003..e2202389a2c4133a183cca59c4e909fc419379ab 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -89,6 +89,12 @@ public class ChunkHolder {
             this.chunkMap.needsChangeBroadcasting.add(this);
         }
         // Paper end - optimise chunk tick iteration
+        // Paper start - optimise checkDespawn
+        LevelChunk chunk = this.getFullChunkNowUnchecked();
+        if (chunk != null) {
+            chunk.updateGeneralAreaCache();
+        }
+        // Paper end - optimise checkDespawn
     }
 
     public void onChunkRemove() {
@@ -101,6 +107,12 @@ public class ChunkHolder {
             this.chunkMap.needsChangeBroadcasting.remove(this);
         }
         // Paper end - optimise chunk tick iteration
+        // Paper start - optimise checkDespawn
+        LevelChunk chunk = this.getFullChunkNowUnchecked();
+        if (chunk != null) {
+            chunk.removeGeneralAreaCache();
+        }
+        // Paper end - optimise checkDespawn
     }
     // Paper end
 
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 6b8a318d9793b290798d44acdcd0d93687c8747d..f0347600b2f07105ce4802273b0cfe8631ee8876 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -157,6 +157,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
     public final ReferenceOpenHashSet<ChunkHolder> needsChangeBroadcasting = new ReferenceOpenHashSet<>();
 
     // Paper - rewrite chunk system
+    // Paper start - optimise checkDespawn
+    public static final int GENERAL_AREA_MAP_SQUARE_RADIUS = 40;
+    public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE = 16.0 * (GENERAL_AREA_MAP_SQUARE_RADIUS - 1);
+    public static final double GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE_SQUARED = GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE * GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE;
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerGeneralAreaMap;
+    // Paper end - optimise checkDespawn
 
     // Paper start - distance maps
     private final com.destroystokyo.paper.util.misc.PooledLinkedHashSets<ServerPlayer> pooledLinkedPlayerHashSets = new com.destroystokyo.paper.util.misc.PooledLinkedHashSets<>();
@@ -208,6 +214,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
             trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player)));
         }
         // Paper end - use distance map to optimise entity tracker
+        this.playerGeneralAreaMap.add(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn
     }
 
     void removePlayerFromDistanceMaps(ServerPlayer player) {
@@ -217,6 +224,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         this.playerMobSpawnMap.remove(player);
         this.playerChunkTickRangeMap.remove(player);
         // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        this.playerGeneralAreaMap.remove(player); // Paper - optimise checkDespawns
         // Paper start - per player mob spawning
         if (this.playerMobDistanceMap != null) {
             this.playerMobDistanceMap.remove(player);
@@ -248,6 +256,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
             trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, io.papermc.paper.chunk.system.ChunkSystem.getSendViewDistance(player)));
         }
         // Paper end - use distance map to optimise entity tracker
+        this.playerGeneralAreaMap.update(player, chunkX, chunkZ, GENERAL_AREA_MAP_SQUARE_RADIUS); // Paper - optimise checkDespawn
     }
     // Paper end
     // Paper start
@@ -405,6 +414,23 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
                 }
             });
         // Paper end - optimise ChunkMap#anyPlayerCloseEnoughForSpawning
+        // Paper start - optimise checkDespawn
+        this.playerGeneralAreaMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ);
+                if (chunk != null) {
+                    chunk.updateGeneralAreaCache(newState);
+                }
+            },
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                LevelChunk chunk = ChunkMap.this.level.getChunkSource().getChunkAtIfCachedImmediately(rangeX, rangeZ);
+                if (chunk != null) {
+                    chunk.updateGeneralAreaCache(newState);
+                }
+            });
+        // Paper end - optimise checkDespawn
     }
 
     protected ChunkGenerator generator() {
diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java
index f1ae9c3b3db3a7540650365b1c8a9b8274b25644..ac3d978b81f0f238ace9724f817c25cbdd866e83 100644
--- a/src/main/java/net/minecraft/server/level/ServerLevel.java
+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java
@@ -562,6 +562,84 @@ public class ServerLevel extends Level implements WorldGenLevel {
     }
     // Paper end
 
+    // Paper start - optimise checkDespawn
+    public final List<ServerPlayer> playersAffectingSpawning = new java.util.ArrayList<>();
+    // Paper end - optimise checkDespawn
+    // Paper start - optimise get nearest players for entity AI
+    @Override
+    public final ServerPlayer getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, @Nullable LivingEntity source,
+                                               double centerX, double centerY, double centerZ) {
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> nearby;
+        nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4);
+
+        if (nearby == null) {
+            return null;
+        }
+
+        Object[] backingSet = nearby.getBackingSet();
+
+        double closestDistanceSquared = Double.MAX_VALUE;
+        ServerPlayer closest = null;
+
+        for (int i = 0, len = backingSet.length; i < len; ++i) {
+            Object _player = backingSet[i];
+            if (!(_player instanceof ServerPlayer)) {
+                continue;
+            }
+            ServerPlayer player = (ServerPlayer)_player;
+
+            double distanceSquared = player.distanceToSqr(centerX, centerY, centerZ);
+            if (distanceSquared < closestDistanceSquared && condition.test(source, player)) {
+                closest = player;
+                closestDistanceSquared = distanceSquared;
+            }
+        }
+
+        return closest;
+    }
+
+    @Override
+    public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition, LivingEntity entityliving) {
+        return this.getNearestPlayer(pathfindertargetcondition, entityliving, entityliving.getX(), entityliving.getY(), entityliving.getZ());
+    }
+
+    @Override
+    public Player getNearestPlayer(net.minecraft.world.entity.ai.targeting.TargetingConditions pathfindertargetcondition,
+                                   double d0, double d1, double d2) {
+        return this.getNearestPlayer(pathfindertargetcondition, null, d0, d1, d2);
+    }
+
+    @Override
+    public List<Player> getNearbyPlayers(net.minecraft.world.entity.ai.targeting.TargetingConditions condition, LivingEntity source, AABB axisalignedbb) {
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> nearby;
+        double centerX = (axisalignedbb.maxX + axisalignedbb.minX) * 0.5;
+        double centerZ = (axisalignedbb.maxZ + axisalignedbb.minZ) * 0.5;
+        nearby = this.getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(Mth.floor(centerX) >> 4, Mth.floor(centerZ) >> 4);
+
+        List<Player> ret = new java.util.ArrayList<>();
+
+        if (nearby == null) {
+            return ret;
+        }
+
+        Object[] backingSet = nearby.getBackingSet();
+
+        for (int i = 0, len = backingSet.length; i < len; ++i) {
+            Object _player = backingSet[i];
+            if (!(_player instanceof ServerPlayer)) {
+                continue;
+            }
+            ServerPlayer player = (ServerPlayer)_player;
+
+            if (axisalignedbb.contains(player.getX(), player.getY(), player.getZ()) && condition.test(source, player)) {
+                ret.add(player);
+            }
+        }
+
+        return ret;
+    }
+    // Paper end - optimise get nearest players for entity AI
+
     // Add env and gen to constructor, IWorldDataServer -> WorldDataServer
     public ServerLevel(MinecraftServer minecraftserver, Executor executor, LevelStorageSource.LevelStorageAccess convertable_conversionsession, PrimaryLevelData iworlddataserver, ResourceKey<Level> resourcekey, LevelStem worlddimension, ChunkProgressListener worldloadlistener, boolean flag, long i, List<CustomSpawner> list, boolean flag1, @Nullable RandomSequences randomsequences, org.bukkit.World.Environment env, org.bukkit.generator.ChunkGenerator gen, org.bukkit.generator.BiomeProvider biomeProvider) {
         // IRegistryCustom.Dimension iregistrycustom_dimension = minecraftserver.registryAccess(); // CraftBukkit - decompile error
@@ -687,6 +765,14 @@ public class ServerLevel extends Level implements WorldGenLevel {
     }
 
     public void tick(BooleanSupplier shouldKeepTicking) {
+        // Paper start - optimise checkDespawn
+        this.playersAffectingSpawning.clear();
+        for (ServerPlayer player : this.players) {
+            if (net.minecraft.world.entity.EntitySelector.PLAYER_AFFECTS_SPAWNING.test(player)) {
+                this.playersAffectingSpawning.add(player);
+            }
+        }
+        // Paper end - optimise checkDespawn
         ProfilerFiller gameprofilerfiller = this.getProfiler();
 
         this.handlingTick = true;
diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java
index b6e48531a2a1316eef786e0476574eb1c3c29a9e..e2a25c29ec74147b3e66aa0b3deb85a8f6ee53a5 100644
--- a/src/main/java/net/minecraft/world/entity/Mob.java
+++ b/src/main/java/net/minecraft/world/entity/Mob.java
@@ -858,7 +858,12 @@ public abstract class Mob extends LivingEntity implements Targeting {
         if (this.level().getDifficulty() == Difficulty.PEACEFUL && this.shouldDespawnInPeaceful()) {
             this.discard();
         } else if (!this.isPersistenceRequired() && !this.requiresCustomPersistence()) {
-            Player entityhuman = this.level().findNearbyPlayer(this, -1.0D, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper
+            // Paper start - optimise checkDespawn
+            Player entityhuman = this.level().findNearbyPlayer(this, level().paperConfig().entities.spawning.despawnRanges.get(this.getType().getCategory()).hard() + 1, EntitySelector.PLAYER_AFFECTS_SPAWNING); // Paper
+            if (entityhuman == null) {
+                entityhuman = ((ServerLevel)this.level()).playersAffectingSpawning.isEmpty() ? null : ((ServerLevel)this.level()).playersAffectingSpawning.get(0);
+            }
+            // Paper end - optimise checkDespawn
 
             if (entityhuman != null) {
                 double d0 = entityhuman.distanceToSqr((Entity) this);
diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java
index 4847b6358e823bd53ecfaf7d82ed9e36d45b1233..799d31874be8a7498e71e7e01f84aa81a7f1eed4 100644
--- a/src/main/java/net/minecraft/world/level/Level.java
+++ b/src/main/java/net/minecraft/world/level/Level.java
@@ -206,6 +206,69 @@ public abstract class Level implements LevelAccessor, AutoCloseable {
         return this.getChunkIfLoaded(chunkX, chunkZ) != null;
     }
     // Paper end
+    // Paper start - optimise checkDespawn
+    public final List<net.minecraft.server.level.ServerPlayer> getNearbyPlayers(@Nullable Entity source, double sourceX, double sourceY,
+                                                                                double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+        LevelChunk chunk;
+        if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE ||
+            (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) {
+            return this.getNearbyPlayersSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate);
+        }
+
+        List<net.minecraft.server.level.ServerPlayer> ret = new java.util.ArrayList<>();
+        chunk.getNearestPlayers(sourceX, sourceY, sourceZ, predicate, maxRange, ret);
+        return ret;
+    }
+
+    private List<net.minecraft.server.level.ServerPlayer> getNearbyPlayersSlow(@Nullable Entity source, double sourceX, double sourceY,
+                                                                               double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+        List<net.minecraft.server.level.ServerPlayer> ret = new java.util.ArrayList<>();
+        double maxRangeSquared = maxRange * maxRange;
+
+        for (net.minecraft.server.level.ServerPlayer player : (List<net.minecraft.server.level.ServerPlayer>)this.players()) {
+            if ((maxRange < 0.0 || player.distanceToSqr(sourceX, sourceY, sourceZ) < maxRangeSquared)) {
+                if (predicate == null || predicate.test(player)) {
+                    ret.add(player);
+                }
+            }
+        }
+
+        return ret;
+    }
+
+    private net.minecraft.server.level.ServerPlayer getNearestPlayerSlow(@Nullable Entity source, double sourceX, double sourceY,
+                                                                         double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+        net.minecraft.server.level.ServerPlayer closest = null;
+        double closestRangeSquared = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange;
+
+        for (net.minecraft.server.level.ServerPlayer player : (List<net.minecraft.server.level.ServerPlayer>)this.players()) {
+            double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ);
+            if (distanceSquared < closestRangeSquared && (predicate == null || predicate.test(player))) {
+                closest = player;
+                closestRangeSquared = distanceSquared;
+            }
+        }
+
+        return closest;
+    }
+
+
+    public final net.minecraft.server.level.ServerPlayer getNearestPlayer(@Nullable Entity source, double sourceX, double sourceY,
+                                                                          double sourceZ, double maxRange, @Nullable Predicate<Entity> predicate) {
+        LevelChunk chunk;
+        if (maxRange < 0.0 || maxRange >= net.minecraft.server.level.ChunkMap.GENERAL_AREA_MAP_ACCEPTABLE_SEARCH_RANGE ||
+            (chunk = (LevelChunk)this.getChunkIfLoadedImmediately(Mth.floor(sourceX) >> 4, Mth.floor(sourceZ) >> 4)) == null) {
+            return this.getNearestPlayerSlow(source, sourceX, sourceY, sourceZ, maxRange, predicate);
+        }
+
+        return chunk.findNearestPlayer(sourceX, sourceY, sourceZ, maxRange, predicate);
+    }
+
+    @Override
+    public @Nullable Player getNearestPlayer(double d0, double d1, double d2, double d3, @Nullable Predicate<Entity> predicate) {
+        return this.getNearestPlayer(null, d0, d1, d2, d3, predicate);
+    }
+    // Paper end - optimise checkDespawn
 
     public abstract ResourceKey<LevelStem> getTypeKey();
 
diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
index d4f99f4592a86e2d8344bc2c44711fef1543acdf..089dd93d4cd4c1f72e63c4944b3b82c1e2ba732d 100644
--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
@@ -260,7 +260,7 @@ public final class NaturalSpawner {
                             blockposition_mutableblockposition.set(l, i, i1);
                             double d0 = (double) l + 0.5D;
                             double d1 = (double) i1 + 0.5D;
-                            Player entityhuman = world.getNearestPlayer(d0, (double) i, d1, -1.0D, false);
+                            Player entityhuman = (chunk instanceof LevelChunk) ? ((LevelChunk)chunk).findNearestPlayer(d0, i, d1, 576.0D, net.minecraft.world.entity.EntitySelector.NO_SPECTATORS) : world.getNearestPlayer(d0, (double) i, d1, -1.0D, false); // Paper - use chunk's player cache to optimize search in range
 
                             if (entityhuman != null) {
                                 double d2 = entityhuman.distanceToSqr(d0, (double) i, d1);
@@ -334,7 +334,7 @@ public final class NaturalSpawner {
     }
 
     private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) {
-        return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerToCenterThan(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isNaturalSpawningAllowed((BlockPos) pos));
+        return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerToCenterThan(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isNaturalSpawningAllowed((BlockPos) pos)); // Paper - diff on change, copy into caller
     }
 
     private static Boolean isValidSpawnPostitionForType(ServerLevel world, MobCategory group, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobSpawnSettings.SpawnerData spawnEntry, BlockPos.MutableBlockPos pos, double squaredDistance) { // Paper
diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
index b90697fbf292f73fc661c46edcc71c1bbce99d94..17398a48ff84ba1b21bd64f7857e3a326fcc54cf 100644
--- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java
@@ -221,6 +221,98 @@ public class LevelChunk extends ChunkAccess {
         }
     }
     // Paper end
+    // Paper start - optimise checkDespawn
+    private boolean playerGeneralAreaCacheSet;
+    private com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> playerGeneralAreaCache;
+
+    public com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> getPlayerGeneralAreaCache() {
+        if (!this.playerGeneralAreaCacheSet) {
+            this.updateGeneralAreaCache();
+        }
+        return this.playerGeneralAreaCache;
+    }
+
+    public void updateGeneralAreaCache() {
+        this.updateGeneralAreaCache(((ServerLevel)this.level).getChunkSource().chunkMap.playerGeneralAreaMap.getObjectsInRange(this.coordinateKey));
+    }
+
+    public void removeGeneralAreaCache() {
+        this.playerGeneralAreaCacheSet = false;
+        this.playerGeneralAreaCache = null;
+    }
+
+    public void updateGeneralAreaCache(com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> value) {
+        this.playerGeneralAreaCacheSet = true;
+        this.playerGeneralAreaCache = value;
+    }
+
+    public net.minecraft.server.level.ServerPlayer findNearestPlayer(double sourceX, double sourceY, double sourceZ,
+                                                                     double maxRange, java.util.function.Predicate<Entity> predicate) {
+        if (!this.playerGeneralAreaCacheSet) {
+            this.updateGeneralAreaCache();
+        }
+
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> nearby = this.playerGeneralAreaCache;
+
+        if (nearby == null) {
+            return null;
+        }
+
+        Object[] backingSet = nearby.getBackingSet();
+        double closestDistance = maxRange < 0.0 ? Double.MAX_VALUE : maxRange * maxRange;
+        net.minecraft.server.level.ServerPlayer closest = null;
+        for (int i = 0, len = backingSet.length; i < len; ++i) {
+            Object _player = backingSet[i];
+            if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) {
+                continue;
+            }
+            net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player;
+
+            double distance = player.distanceToSqr(sourceX, sourceY, sourceZ);
+            if (distance < closestDistance && predicate.test(player)) {
+                closest = player;
+                closestDistance = distance;
+            }
+        }
+
+        return closest;
+    }
+
+    public void getNearestPlayers(double sourceX, double sourceY, double sourceZ, java.util.function.Predicate<Entity> predicate,
+                                  double range, java.util.List<net.minecraft.server.level.ServerPlayer> ret) {
+        if (!this.playerGeneralAreaCacheSet) {
+            this.updateGeneralAreaCache();
+        }
+
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> nearby = this.playerGeneralAreaCache;
+
+        if (nearby == null) {
+            return;
+        }
+
+        double rangeSquared = range * range;
+
+        Object[] backingSet = nearby.getBackingSet();
+        for (int i = 0, len = backingSet.length; i < len; ++i) {
+            Object _player = backingSet[i];
+            if (!(_player instanceof net.minecraft.server.level.ServerPlayer)) {
+                continue;
+            }
+            net.minecraft.server.level.ServerPlayer player = (net.minecraft.server.level.ServerPlayer)_player;
+
+            if (range >= 0.0) {
+                double distanceSquared = player.distanceToSqr(sourceX, sourceY, sourceZ);
+                if (distanceSquared > rangeSquared) {
+                    continue;
+                }
+            }
+
+            if (predicate == null || predicate.test(player)) {
+                ret.add(player);
+            }
+        }
+    }
+    // Paper end - optimise checkDespawn
 
     public LevelChunk(ServerLevel world, ProtoChunk protoChunk, @Nullable LevelChunk.PostLoadProcessor entityLoader) {
         this(world, protoChunk.getPos(), protoChunk.getUpgradeData(), protoChunk.unpackBlockTicks(), protoChunk.unpackFluidTicks(), protoChunk.getInhabitedTime(), protoChunk.getSections(), entityLoader, protoChunk.getBlendingData());