From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Tue, 5 May 2020 20:40:53 -0700
Subject: [PATCH] Optimize isOutsideRange to use distance maps

Use a distance map to find the players in range quickly

diff --git a/src/main/java/net/minecraft/server/ChunkMapDistance.java b/src/main/java/net/minecraft/server/ChunkMapDistance.java
index 275c2b23b4d1ff09ee2b1823d0780700f773659e..ef61f8e784b7ebd26293d627e8b8e1aef1be6e21 100644
--- a/src/main/java/net/minecraft/server/ChunkMapDistance.java
+++ b/src/main/java/net/minecraft/server/ChunkMapDistance.java
@@ -31,7 +31,7 @@ public abstract class ChunkMapDistance {
     private final Long2ObjectMap<ObjectSet<EntityPlayer>> c = new Long2ObjectOpenHashMap();
     public final Long2ObjectOpenHashMap<ArraySetSorted<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
     private final ChunkMapDistance.a ticketLevelTracker = new ChunkMapDistance.a();
-    private final ChunkMapDistance.b f = new ChunkMapDistance.b(8);
+    public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used
     private final ChunkMapDistance.c g = new ChunkMapDistance.c(33);
     // Paper start use a queue, but still keep unique requirement
     public final java.util.Queue<PlayerChunk> pendingChunkUpdates = new java.util.ArrayDeque<PlayerChunk>() {
@@ -50,6 +50,8 @@ public abstract class ChunkMapDistance {
     private final Executor m;
     private long currentTick;
 
+    PlayerChunkMap chunkMap; // Paper
+
     protected ChunkMapDistance(Executor executor, Executor executor1) {
         executor1.getClass();
         Mailbox<Runnable> mailbox = Mailbox.a("player ticket throttler", executor1::execute);
@@ -94,7 +96,7 @@ public abstract class ChunkMapDistance {
     protected abstract PlayerChunk a(long i, int j, @Nullable PlayerChunk playerchunk, int k);
 
     public boolean a(PlayerChunkMap playerchunkmap) {
-        this.f.a();
+        //this.f.a(); // Paper - no longer used
         this.g.a();
         int i = Integer.MAX_VALUE - this.ticketLevelTracker.a(Integer.MAX_VALUE);
         boolean flag = i != 0;
@@ -230,7 +232,7 @@ public abstract class ChunkMapDistance {
         ((ObjectSet) this.c.computeIfAbsent(i, (j) -> {
             return new ObjectOpenHashSet();
         })).add(entityplayer);
-        this.f.update(i, 0, true);
+        //this.f.update(i, 0, true); // Paper - no longer used
         this.g.update(i, 0, true);
     }
 
@@ -241,7 +243,7 @@ public abstract class ChunkMapDistance {
         if (objectset != null) objectset.remove(entityplayer); // Paper - some state corruption happens here, don't crash, clean up gracefully.
         if (objectset == null || objectset.isEmpty()) { // Paper
             this.c.remove(i);
-            this.f.update(i, Integer.MAX_VALUE, false);
+            //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
             this.g.update(i, Integer.MAX_VALUE, false);
         }
 
@@ -265,13 +267,17 @@ public abstract class ChunkMapDistance {
     }
 
     public int b() {
-        this.f.a();
-        return this.f.a.size();
+        // Paper start - use distance map to implement
+        // note: this is the spawn chunk count
+        return this.chunkMap.playerChunkTickRangeMap.size();
+        // Paper end - use distance map to implement
     }
 
     public boolean d(long i) {
-        this.f.a();
-        return this.f.a.containsKey(i);
+        // Paper start - use distance map to implement
+        // note: this is the is spawn chunk method
+        return this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(i) != null;
+        // Paper end - use distance map to implement
     }
 
     public String c() {
diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
index 4e50ef38700918a0efb2c67f5acf98eb66fd8335..b4d8657d37b2a6c02e886ec6de243634d1c08d51 100644
--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
+++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
@@ -729,6 +729,37 @@ public class ChunkProviderServer extends IChunkProvider {
         boolean flag1 = this.world.getGameRules().getBoolean(GameRules.DO_MOB_SPAWNING) && !world.getPlayers().isEmpty(); // CraftBukkit
 
         if (!flag) {
+            // Paper start - optimize isOutisdeRange
+            PlayerChunkMap playerChunkMap = this.playerChunkMap;
+            for (EntityPlayer player : this.world.players) {
+                if (!player.affectsSpawning || player.isSpectator()) {
+                    playerChunkMap.playerMobSpawnMap.remove(player);
+                    continue;
+                }
+
+                int viewDistance = this.playerChunkMap.getEffectiveViewDistance();
+
+                // copied and modified from isOutisdeRange
+                int chunkRange = world.spigotConfig.mobSpawnRange;
+                chunkRange = (chunkRange > viewDistance) ? (byte)viewDistance : chunkRange;
+                chunkRange = (chunkRange > ChunkMapDistance.MOB_SPAWN_RANGE) ? ChunkMapDistance.MOB_SPAWN_RANGE : chunkRange;
+
+                com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(player.getBukkitEntity(), (byte)chunkRange);
+                event.callEvent();
+                if (event.isCancelled() || event.getSpawnRadius() < 0 || playerChunkMap.playerChunkTickRangeMap.getLastViewDistance(player) == -1) {
+                    playerChunkMap.playerMobSpawnMap.remove(player);
+                    continue;
+                }
+
+                int range = Math.min(event.getSpawnRadius(), 32); // limit to max view distance
+                int chunkX = net.minecraft.server.MCUtil.getChunkCoordinate(player.locX());
+                int chunkZ = net.minecraft.server.MCUtil.getChunkCoordinate(player.locZ());
+
+                playerChunkMap.playerMobSpawnMap.addOrUpdate(player, chunkX, chunkZ, range);
+                player.lastEntitySpawnRadiusSquared = (double)((range << 4) * (range << 4)); // used in isOutsideRange
+                player.playerNaturallySpawnedEvent = event;
+            }
+            // Paper end - optimize isOutisdeRange
             this.world.getMethodProfiler().enter("pollingChunks");
             int k = this.world.getGameRules().getInt(GameRules.RANDOM_TICK_SPEED);
             boolean flag2 = world.ticksPerAnimalSpawns != 0L && worlddata.getTime() % world.ticksPerAnimalSpawns == 0L; // CraftBukkit
@@ -758,15 +789,7 @@ public class ChunkProviderServer extends IChunkProvider {
             this.world.getMethodProfiler().exit();
             //List<PlayerChunk> list = Lists.newArrayList(this.playerChunkMap.f()); // Paper
             //Collections.shuffle(list); // Paper
-            //Paper start - call player naturally spawn event
-            int chunkRange = world.spigotConfig.mobSpawnRange;
-            chunkRange = (chunkRange > world.spigotConfig.viewDistance) ? (byte) world.spigotConfig.viewDistance : chunkRange;
-            chunkRange = Math.min(chunkRange, 8);
-            for (EntityPlayer entityPlayer : this.world.getPlayers()) {
-                entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange);
-                entityPlayer.playerNaturallySpawnedEvent.callEvent();
-            };
-            // Paper end
+            // Paper - moved up
             final int[] chunksTicked = {0}; this.playerChunkMap.forEachVisibleChunk((playerchunk) -> { // Paper - safe iterator incase chunk loads, also no wrapping
                 Optional<Chunk> optional = ((Either) playerchunk.a().getNow(PlayerChunk.UNLOADED_CHUNK)).left();
 
@@ -782,9 +805,9 @@ public class ChunkProviderServer extends IChunkProvider {
                         Chunk chunk = (Chunk) optional1.get();
                         ChunkCoordIntPair chunkcoordintpair = playerchunk.i();
 
-                        if (!this.playerChunkMap.isOutsideOfRange(chunkcoordintpair)) {
+                        if (!this.playerChunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange
                             chunk.setInhabitedTime(chunk.getInhabitedTime() + j);
-                            if (flag1 && (this.allowMonsters || this.allowAnimals) && this.world.getWorldBorder().isInBounds(chunk.getPos()) && !this.playerChunkMap.isOutsideOfRange(chunkcoordintpair, true)) { // Spigot
+                            if (flag1 && (this.allowMonsters || this.allowAnimals) && this.world.getWorldBorder().isInBounds(chunk.getPos()) && !this.playerChunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, true)) { // Spigot // Paper - optimise isOutsideOfRange
                                 SpawnerCreature.a(this.world, chunk, spawnercreature_d, this.allowAnimals, this.allowMonsters, flag2);
                             }
 
diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
index b307fca8c4b91f0bf260497bc425f2f10540e36d..38b80b72a583b5d99ad9768d41a4ecfb504fb5f2 100644
--- a/src/main/java/net/minecraft/server/EntityPlayer.java
+++ b/src/main/java/net/minecraft/server/EntityPlayer.java
@@ -117,6 +117,8 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
 
     public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> cachedSingleHashSet; // Paper
 
+    double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks
+
     public EntityPlayer(MinecraftServer minecraftserver, WorldServer worldserver, GameProfile gameprofile, PlayerInteractManager playerinteractmanager) {
         super(worldserver, worldserver.getSpawn(), worldserver.v(), gameprofile);
         this.spawnDimension = World.OVERWORLD;
diff --git a/src/main/java/net/minecraft/server/PlayerChunk.java b/src/main/java/net/minecraft/server/PlayerChunk.java
index 054a3156f7b7b7077422b4951adf7bf45df42bfc..69c7bbf6a83b07a3af62b8fabaa851c7f7dc9a98 100644
--- a/src/main/java/net/minecraft/server/PlayerChunk.java
+++ b/src/main/java/net/minecraft/server/PlayerChunk.java
@@ -45,6 +45,18 @@ public class PlayerChunk {
     long lastAutoSaveTime; // Paper - incremental autosave
     long inactiveTimeStart; // Paper - incremental autosave
 
+    // Paper start - optimise isOutsideOfRange
+    // cached here to avoid a map lookup
+    com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> playersInMobSpawnRange;
+    com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> playersInChunkTickRange;
+
+    void updateRanges() {
+        long key = net.minecraft.server.MCUtil.getCoordinateKey(this.location);
+        this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
+        this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+    }
+    // Paper end - optimise isOutsideOfRange
+
     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;
@@ -61,6 +73,7 @@ public class PlayerChunk {
         this.n = this.oldTicketLevel;
         this.a(i);
         this.chunkMap = (PlayerChunkMap)playerchunk_d; // Paper
+        this.updateRanges(); // Paper - optimise isOutsideOfRange
     }
 
     // Paper start
diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
index 21a9a0364ec6cfd28fcfc0a62d3465993dac1a1c..b4067657eefe6a418b76b599b4c8ffb8359c3f89 100644
--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
+++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
@@ -160,6 +160,17 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
         return MinecraftServer.getServer().applyTrackingRangeScale(vanilla);
     }
     // Paper end - use distance map to optimise tracker
+    // Paper start - optimise PlayerChunkMap#isOutsideRange
+    // A note about the naming used here:
+    // Previously, mojang used a "spawn range" of 8 for controlling both ticking and
+    // mob spawn range. However, spigot makes the spawn range configurable by
+    // checking if the chunk is in the tick range (8) and the spawn range
+    // obviously this means a spawn range > 8 cannot be implemented
+
+    // these maps are named after spigot's uses
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobSpawnMap; // this map is absent from updateMaps since it's controlled at the start of the chunkproviderserver tick
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerChunkTickRangeMap;
+    // Paper end - optimise PlayerChunkMap#isOutsideRange
 
     void addPlayerToDistanceMaps(EntityPlayer player) {
         int chunkX = MCUtil.getChunkCoordinate(player.locX());
@@ -173,6 +184,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
             trackMap.add(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance()));
         }
         // Paper end - use distance map to optimise entity tracker
+        // Paper start - optimise PlayerChunkMap#isOutsideRange
+        this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, ChunkMapDistance.MOB_SPAWN_RANGE);
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
     }
 
     void removePlayerFromDistanceMaps(EntityPlayer player) {
@@ -181,6 +195,10 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
             this.playerEntityTrackerTrackMaps[i].remove(player);
         }
         // Paper end - use distance map to optimise tracker
+        // Paper start - optimise PlayerChunkMap#isOutsideRange
+        this.playerMobSpawnMap.remove(player);
+        this.playerChunkTickRangeMap.remove(player);
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
     }
 
     void updateMaps(EntityPlayer player) {
@@ -195,6 +213,9 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
             trackMap.update(player, chunkX, chunkZ, Math.min(trackRange, this.getEffectiveViewDistance()));
         }
         // Paper end - use distance map to optimise entity tracker
+        // Paper start - optimise PlayerChunkMap#isOutsideRange
+        this.playerChunkTickRangeMap.update(player, chunkX, chunkZ, ChunkMapDistance.MOB_SPAWN_RANGE);
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
     }
     // Paper end
 
@@ -226,7 +247,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
         this.mailboxWorldGen = this.p.a(threadedmailbox, false);
         this.mailboxMain = this.p.a(mailbox, false);
         this.lightEngine = new LightEngineThreaded(ilightaccess, this, this.world.getDimensionManager().hasSkyLight(), threadedmailbox1, this.p.a(threadedmailbox1, false));
-        this.chunkDistanceManager = new PlayerChunkMap.a(executor, iasynctaskhandler);
+        this.chunkDistanceManager = new PlayerChunkMap.a(executor, iasynctaskhandler); this.chunkDistanceManager.chunkMap = this; // Paper
         this.l = supplier;
         this.m = new VillagePlace(new File(this.w, "poi"), datafixer, flag, this.world); // Paper
         this.setViewDistance(i);
@@ -270,6 +291,38 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
             this.playerEntityTrackerTrackMaps[ordinal] = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets);
         }
         // Paper end - use distance map to optimise entity tracker
+        // Paper start - optimise PlayerChunkMap#isOutsideRange
+        this.playerChunkTickRangeMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
+            (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+                PlayerChunk playerChunk = PlayerChunkMap.this.getUpdatingChunk(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInChunkTickRange = newState;
+                }
+            },
+            (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+                PlayerChunk playerChunk = PlayerChunkMap.this.getUpdatingChunk(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInChunkTickRange = newState;
+                }
+            });
+        this.playerMobSpawnMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
+            (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+                PlayerChunk playerChunk = PlayerChunkMap.this.getUpdatingChunk(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInMobSpawnRange = newState;
+                }
+            },
+            (EntityPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> newState) -> {
+                PlayerChunk playerChunk = PlayerChunkMap.this.getUpdatingChunk(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInMobSpawnRange = newState;
+                }
+            });
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
     }
 
     public void updatePlayerMobTypeMap(Entity entity) {
@@ -289,6 +342,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
         return entityPlayer.mobCounts[enumCreatureType.ordinal()];
     }
 
+    private static double getDistanceSquaredFromChunk(ChunkCoordIntPair chunkPos, Entity entity) { return a(chunkPos, entity); } // Paper - OBFHELPER
     private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) {
         double d0 = (double) (chunkcoordintpair.x * 16 + 8);
         double d1 = (double) (chunkcoordintpair.z * 16 + 8);
@@ -467,6 +521,7 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
         } else {
             if (playerchunk != null) {
                 playerchunk.a(j);
+                playerchunk.updateRanges(); // Paper - optimise isOutsideOfRange
             }
 
             if (playerchunk != null) {
@@ -1437,30 +1492,53 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
         return isOutsideOfRange(chunkcoordintpair, false);
     }
 
-    boolean isOutsideOfRange(ChunkCoordIntPair chunkcoordintpair, boolean reducedRange) {
-        int chunkRange = world.spigotConfig.mobSpawnRange;
-        chunkRange = (chunkRange > world.spigotConfig.viewDistance) ? (byte) world.spigotConfig.viewDistance : chunkRange;
-        chunkRange = (chunkRange > 8) ? 8 : chunkRange;
+    // Paper start - optimise isOutsideOfRange
+    final boolean isOutsideOfRange(ChunkCoordIntPair chunkcoordintpair, boolean reducedRange) {
+        return this.isOutsideOfRange(this.getUpdatingChunk(chunkcoordintpair.pair()), chunkcoordintpair, reducedRange);
+    }
 
-        final int finalChunkRange = chunkRange; // Paper for lambda below
-        //double blockRange = (reducedRange) ? Math.pow(chunkRange << 4, 2) : 16384.0D; // Paper - use from event
-        // Spigot end
-        long i = chunkcoordintpair.pair();
+    final boolean isOutsideOfRange(PlayerChunk playerchunk, ChunkCoordIntPair chunkcoordintpair, boolean reducedRange) {
+        // this function is so hot that removing the map lookup call can have an order of magnitude impact on its performance
+        // tested and confirmed via System.nanoTime()
+        com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange;
 
-        return !this.chunkDistanceManager.d(i) ? true : this.playerMap.a(i).noneMatch((entityplayer) -> {
-            // Paper start -
-            com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent event;
-            double blockRange = 16384.0D;
-            if (reducedRange) {
-                event = entityplayer.playerNaturallySpawnedEvent;
-                if (event == null || event.isCancelled()) return false;
-                blockRange = (double) ((event.getSpawnRadius() << 4) * (event.getSpawnRadius() << 4));
-            }
+        if (playersInRange == null) {
+            return true;
+        }
 
-            return (!entityplayer.isSpectator() && a(chunkcoordintpair, (Entity) entityplayer) < blockRange); // Spigot
-            // Paper end
-        });
+        Object[] backingSet = playersInRange.getBackingSet();
+
+        if (reducedRange) {
+            for (int i = 0, len = backingSet.length; i < len; ++i) {
+                Object raw = backingSet[i];
+                if (!(raw instanceof EntityPlayer)) {
+                    continue;
+                }
+                EntityPlayer player = (EntityPlayer) raw;
+                // don't check spectator and whatnot, already handled by mob spawn map update
+                if (player.lastEntitySpawnRadiusSquared > getDistanceSquaredFromChunk(chunkcoordintpair, player)) {
+                    return false; // in range
+                }
+            }
+        } else {
+            final double range = (ChunkMapDistance.MOB_SPAWN_RANGE * 16) * (ChunkMapDistance.MOB_SPAWN_RANGE * 16);
+            // before spigot, mob spawn range was actually mob spawn range + tick range, but it was split
+            for (int i = 0, len = backingSet.length; i < len; ++i) {
+                Object raw = backingSet[i];
+                if (!(raw instanceof EntityPlayer)) {
+                    continue;
+                }
+                EntityPlayer player = (EntityPlayer) raw;
+                // don't check spectator and whatnot, already handled by mob spawn map update
+                if (range > getDistanceSquaredFromChunk(chunkcoordintpair, player)) {
+                    return false; // in range
+                }
+            }
+        }
+        // no players in range
+        return true;
     }
+    // Paper end - optimise isOutsideOfRange
 
     private boolean b(EntityPlayer entityplayer) {
         return entityplayer.isSpectator() && !this.world.getGameRules().getBoolean(GameRules.SPECTATORS_GENERATE_CHUNKS);