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/level/ChunkHolder.java b/src/main/java/net/minecraft/server/level/ChunkHolder.java
index bbb94e8a5e3585701849e025b534a69a6e67949f..7223c6daf6f0eb959a5cab701096324a34b9c88a 100644
--- a/src/main/java/net/minecraft/server/level/ChunkHolder.java
+++ b/src/main/java/net/minecraft/server/level/ChunkHolder.java
@@ -100,6 +100,18 @@ public class ChunkHolder {
     }
     // Paper end
 
+    // Paper start - optimise isOutsideOfRange
+    // cached here to avoid a map lookup
+    com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInMobSpawnRange;
+    com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> playersInChunkTickRange;
+
+    void updateRanges() {
+        long key = net.minecraft.server.MCUtil.getCoordinateKey(this.pos);
+        this.playersInMobSpawnRange = this.chunkMap.playerMobSpawnMap.getObjectsInRange(key);
+        this.playersInChunkTickRange = this.chunkMap.playerChunkTickRangeMap.getObjectsInRange(key);
+    }
+    // Paper end - optimise isOutsideOfRange
+
     public ChunkHolder(ChunkPos pos, int level, LevelHeightAccessor world, LevelLightEngine lightingProvider, ChunkHolder.LevelChangeListener levelUpdateListener, ChunkHolder.PlayerProvider playersWatchingChunkProvider) {
         this.futures = new AtomicReferenceArray(ChunkHolder.CHUNK_STATUSES.size());
         this.fullChunkFuture = ChunkHolder.UNLOADED_LEVEL_CHUNK_FUTURE;
@@ -121,6 +133,7 @@ public class ChunkHolder {
         this.setTicketLevel(level);
         this.changedBlocksPerSection = new ShortSet[world.getSectionsCount()];
         this.chunkMap = (ChunkMap)playersWatchingChunkProvider; // Paper
+        this.updateRanges(); // Paper - optimise isOutsideOfRange
     }
 
     // CraftBukkit start
diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index 04ece67574bb95f575edc6f5337a3492e7260102..1e944b2fdfd382bfcc7f94f9a8daf1782931110b 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -229,6 +229,17 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         return MinecraftServer.getServer().getScaledTrackingDistance(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(ServerPlayer player) {
         int chunkX = MCUtil.getChunkCoordinate(player.getX());
@@ -242,6 +253,12 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
             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.update(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE);
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
+        // Paper start - optimise PlayerChunkMap#isOutsideRange
+        this.playerChunkTickRangeMap.add(player, chunkX, chunkZ, DistanceManager.MOB_SPAWN_RANGE);
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
         // Paper start - no-tick view distance
         int effectiveTickViewDistance = this.getEffectiveViewDistance();
         int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance);
@@ -263,6 +280,10 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
             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
         // Paper start - no-tick view distance
         this.playerViewDistanceBroadcastMap.remove(player);
         this.playerViewDistanceTickMap.remove(player);
@@ -282,6 +303,9 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
             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, DistanceManager.MOB_SPAWN_RANGE);
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
         // Paper start - no-tick view distance
         int effectiveTickViewDistance = this.getEffectiveViewDistance();
         int effectiveNoTickViewDistance = Math.max(this.getEffectiveNoTickViewDistance(), effectiveTickViewDistance);
@@ -330,7 +354,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         this.mainThreadMailbox = this.queueSorter.getProcessor(mailbox, false);
         this.mailboxLight = this.queueSorter.getProcessor(lightthreaded, false);// Paper
         this.lightEngine = new ThreadedLevelLightEngine(chunkProvider, this, this.level.dimensionType().hasSkyLight(), threadedmailbox1, this.queueSorter.getProcessor(threadedmailbox1, false));
-        this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor);
+        this.distanceManager = new ChunkMap.ChunkDistanceManager(executor, mainThreadExecutor); this.distanceManager.chunkMap = this; // Paper
         this.overworldDataStorage = persistentStateManagerFactory;
         this.poiManager = new PoiManager(new File(file, "poi"), dataFixer, dsync, world);
         this.setViewDistance(viewDistance);
@@ -374,6 +398,38 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
             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,
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInChunkTickRange = newState;
+                }
+            },
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInChunkTickRange = newState;
+                }
+            });
+        this.playerMobSpawnMap = 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) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInMobSpawnRange = newState;
+                }
+            },
+            (ServerPlayer player, int rangeX, int rangeZ, int currPosX, int currPosZ, int prevPosX, int prevPosZ,
+             com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> newState) -> {
+                ChunkHolder playerChunk = ChunkMap.this.getUpdatingChunkIfPresent(MCUtil.getCoordinateKey(rangeX, rangeZ));
+                if (playerChunk != null) {
+                    playerChunk.playersInMobSpawnRange = newState;
+                }
+            });
+        // Paper end - optimise PlayerChunkMap#isOutsideRange
         // Paper start - no-tick view distance
         this.setNoTickViewDistance(this.level.paperConfig.noTickViewDistance);
         this.playerViewDistanceTickMap = new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets,
@@ -643,6 +699,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         } else {
             if (holder != null) {
                 holder.setTicketLevel(level);
+                holder.updateRanges(); // Paper - optimise isOutsideOfRange
             }
 
             if (holder != null) {
@@ -1463,29 +1520,50 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         return this.isOutsideOfRange(chunkPos, false);
     }
 
-    boolean isOutsideOfRange(ChunkPos chunkcoordintpair, boolean reducedRange) {
-        int chunkRange = level.spigotConfig.mobSpawnRange;
-        chunkRange = (chunkRange > level.spigotConfig.viewDistance) ? (byte) level.spigotConfig.viewDistance : chunkRange;
-        chunkRange = (chunkRange > 8) ? 8 : chunkRange;
-
-        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.toLong();
+    // Paper start - optimise isOutsideOfRange
+    final boolean isOutsideOfRange(ChunkPos chunkcoordintpair, boolean reducedRange) {
+        return this.isOutsideOfRange(this.getUpdatingChunkIfPresent(chunkcoordintpair.toLong()), chunkcoordintpair, reducedRange);
+    }
+    final boolean isOutsideOfRange(ChunkHolder playerchunk, ChunkPos 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<ServerPlayer> playersInRange = reducedRange ? playerchunk.playersInMobSpawnRange : playerchunk.playersInChunkTickRange;
+        if (playersInRange == null) {
+            return true;
+        }
+        Object[] backingSet = playersInRange.getBackingSet();
 
-        return !this.distanceManager.hasPlayersNearby(i) ? true : this.playerMap.getPlayers(i).noneMatch((entityplayer) -> {
-            // Paper start - add PlayerNaturallySpawnCreaturesEvent
-            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 (reducedRange) {
+            for (int i = 0, len = backingSet.length; i < len; ++i) {
+                Object raw = backingSet[i];
+                if (!(raw instanceof ServerPlayer)) {
+                    continue;
+                }
+                ServerPlayer player = (ServerPlayer) raw;
+                // don't check spectator and whatnot, already handled by mob spawn map update
+                if (player.lastEntitySpawnRadiusSquared > euclideanDistanceSquared(chunkcoordintpair, player)) {
+                    return false; // in range
+                }
             }
-            // Paper end
-            return !entityplayer.isSpectator() && ChunkMap.euclideanDistanceSquared(chunkcoordintpair, (Entity) entityplayer) < blockRange; // Spigot
-        });
+        } else {
+            final double range = (DistanceManager.MOB_SPAWN_RANGE * 16) * (DistanceManager.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 ServerPlayer)) {
+                    continue;
+                }
+                ServerPlayer player = (ServerPlayer) raw;
+                // don't check spectator and whatnot, already handled by mob spawn map update
+                if (range > euclideanDistanceSquared(chunkcoordintpair, player)) {
+                    return false; // in range
+                }
+            }
+        }
+        // no players in range
+        return true;
     }
+    // Paper end - optimise isOutsideOfRange
 
     private boolean skipPlayer(ServerPlayer player) {
         return player.isSpectator() && !this.level.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS);
diff --git a/src/main/java/net/minecraft/server/level/DistanceManager.java b/src/main/java/net/minecraft/server/level/DistanceManager.java
index b49d380ef088aed3204ec71abc437c348ef004fa..577b391dcba1db712c1e2c83296e1c87b3e34ab2 100644
--- a/src/main/java/net/minecraft/server/level/DistanceManager.java
+++ b/src/main/java/net/minecraft/server/level/DistanceManager.java
@@ -45,7 +45,7 @@ public abstract class DistanceManager {
     final Long2ObjectMap<ObjectSet<ServerPlayer>> playersPerChunk = new Long2ObjectOpenHashMap();
     public final Long2ObjectOpenHashMap<SortedArraySet<Ticket<?>>> tickets = new Long2ObjectOpenHashMap();
     private final DistanceManager.ChunkTicketTracker ticketTracker = new DistanceManager.ChunkTicketTracker();
-    private final DistanceManager.FixedPlayerDistanceChunkTracker naturalSpawnChunkCounter = new DistanceManager.FixedPlayerDistanceChunkTracker(8);
+    public static final int MOB_SPAWN_RANGE = 8; // private final ChunkMapDistance.b f = new ChunkMapDistance.b(8); // Paper - no longer used
     private final DistanceManager.PlayerTicketTracker playerTicketManager = new DistanceManager.PlayerTicketTracker(33);
     // Paper start use a queue, but still keep unique requirement
     public final java.util.Queue<ChunkHolder> pendingChunkUpdates = new java.util.ArrayDeque<ChunkHolder>() {
@@ -64,6 +64,8 @@ public abstract class DistanceManager {
     final Executor mainThreadExecutor;
     private long ticketTickCounter;
 
+    ChunkMap chunkMap; // Paper
+
     protected DistanceManager(Executor workerExecutor, Executor mainThreadExecutor) {
         Objects.requireNonNull(mainThreadExecutor);
         ProcessorHandle<Runnable> mailbox = ProcessorHandle.of("player ticket throttler", mainThreadExecutor::execute);
@@ -108,7 +110,7 @@ public abstract class DistanceManager {
     protected abstract ChunkHolder updateChunkScheduling(long pos, int level, @Nullable ChunkHolder holder, int k);
 
     public boolean runAllUpdates(ChunkMap playerchunkmap) {
-        this.naturalSpawnChunkCounter.runAllUpdates();
+        //this.f.a(); // Paper - no longer used
         this.playerTicketManager.runAllUpdates();
         int i = Integer.MAX_VALUE - this.ticketTracker.runDistanceUpdates(Integer.MAX_VALUE);
         boolean flag = i != 0;
@@ -244,7 +246,7 @@ public abstract class DistanceManager {
         ((ObjectSet) this.playersPerChunk.computeIfAbsent(i, (j) -> {
             return new ObjectOpenHashSet();
         })).add(player);
-        this.naturalSpawnChunkCounter.update(i, 0, true);
+        //this.f.update(i, 0, true); // Paper - no longer used
         this.playerTicketManager.update(i, 0, true);
     }
 
@@ -256,7 +258,7 @@ public abstract class DistanceManager {
         if (objectset != null) objectset.remove(player); // Paper - some state corruption happens here, don't crash, clean up gracefully.
         if (objectset == null || objectset.isEmpty()) { // Paper
             this.playersPerChunk.remove(i);
-            this.naturalSpawnChunkCounter.update(i, Integer.MAX_VALUE, false);
+            //this.f.update(i, Integer.MAX_VALUE, false); // Paper - no longer used
             this.playerTicketManager.update(i, Integer.MAX_VALUE, false);
         }
 
@@ -280,13 +282,17 @@ public abstract class DistanceManager {
     }
 
     public int getNaturalSpawnChunkCount() {
-        this.naturalSpawnChunkCounter.runAllUpdates();
-        return this.naturalSpawnChunkCounter.chunks.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 hasPlayersNearby(long i) {
-        this.naturalSpawnChunkCounter.runAllUpdates();
-        return this.naturalSpawnChunkCounter.chunks.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 getDebugStatus() {
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index 5009a782d80d5c358acb5a412d63a567fd7db3ab..d02eaf281b752f5b1442d06471b80ce28361f31a 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -733,6 +733,37 @@ public class ServerChunkCache extends ChunkSource {
         boolean flag1 = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit
 
         if (!flag) {
+            // Paper start - optimize isOutisdeRange
+            ChunkMap playerChunkMap = this.chunkMap;
+            for (ServerPlayer player : this.level.players) {
+                if (!player.affectsSpawning || player.isSpectator()) {
+                    playerChunkMap.playerMobSpawnMap.remove(player);
+                    continue;
+                }
+
+                int viewDistance = this.chunkMap.getEffectiveViewDistance();
+
+                // copied and modified from isOutisdeRange
+                int chunkRange = level.spigotConfig.mobSpawnRange;
+                chunkRange = (chunkRange > viewDistance) ? (byte)viewDistance : chunkRange;
+                chunkRange = (chunkRange > DistanceManager.MOB_SPAWN_RANGE) ? DistanceManager.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.getX());
+                int chunkZ = net.minecraft.server.MCUtil.getChunkCoordinate(player.getZ());
+
+                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.level.getProfiler().push("pollingChunks");
             int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING);
             boolean flag2 = level.ticksPerAnimalSpawns != 0L && worlddata.getGameTime() % level.ticksPerAnimalSpawns == 0L; // CraftBukkit
@@ -763,15 +794,7 @@ public class ServerChunkCache extends ChunkSource {
             List<ChunkHolder> list = Lists.newArrayList(this.chunkMap.visibleChunkMap.values()); // Paper
 
             Collections.shuffle(list);
-            //Paper start - call player naturally spawn event
-            int chunkRange = level.spigotConfig.mobSpawnRange;
-            chunkRange = (chunkRange > level.spigotConfig.viewDistance) ? (byte) level.spigotConfig.viewDistance : chunkRange;
-            chunkRange = Math.min(chunkRange, 8);
-            for (ServerPlayer entityPlayer : this.level.players()) {
-                entityPlayer.playerNaturallySpawnedEvent = new com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent(entityPlayer.getBukkitEntity(), (byte) chunkRange);
-                entityPlayer.playerNaturallySpawnedEvent.callEvent();
-            };
-            // Paper end
+            // Paper - moved natural spawn event up
             this.level.timings.chunkTicks.startTiming(); // Paper
             final int[] chunksTicked = {0}; // Paper
             list.forEach((playerchunk) -> {
@@ -781,9 +804,9 @@ public class ServerChunkCache extends ChunkSource {
                     LevelChunk chunk = (LevelChunk) optional.get();
                     ChunkPos chunkcoordintpair = chunk.getPos();
 
-                    if (this.level.isPositionEntityTicking(chunkcoordintpair) && !this.chunkMap.noPlayersCloseForSpawning(chunkcoordintpair)) {
+                    if (this.level.isPositionEntityTicking(chunkcoordintpair) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, false)) { // Paper - optimise isOutsideOfRange
                         chunk.setInhabitedTime(chunk.getInhabitedTime() + j);
-                        if (flag1 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunk.getPos()) && !this.chunkMap.isOutsideOfRange(chunkcoordintpair, true)) { // Spigot
+                        if (flag1 && (this.spawnEnemies || this.spawnFriendlies) && this.level.getWorldBorder().isWithinBounds(chunk.getPos()) && !this.chunkMap.isOutsideOfRange(playerchunk, chunkcoordintpair, true)) { // Spigot // Paper - optimise isOutsideOfRange
                             NaturalSpawner.spawnForChunk(this.level, chunk, spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag2);
                             if (chunksTicked[0]++ % 10 == 0) this.level.getServer().midTickLoadChunks(); // Paper
                         }
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index 30f8f4a8314c849a66143a545c1af3440965c6aa..cf09bd17b9d2be04f79edef6debdd815b5f7f86c 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -250,6 +250,7 @@ public class ServerPlayer extends Player {
     // CraftBukkit end
     public PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper
 
+    public double lastEntitySpawnRadiusSquared; // Paper - optimise isOutsideRange, this field is in blocks
     public final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleHashSet; // Paper
     boolean needsChunkCenterUpdate; // Paper - no-tick view distance