diff --git a/Spigot-Server-Patches/Generator-Settings.patch b/Spigot-Server-Patches/Generator-Settings.patch
index 9a058217ab..f4cda5030d 100644
--- a/Spigot-Server-Patches/Generator-Settings.patch
+++ b/Spigot-Server-Patches/Generator-Settings.patch
@@ -9,8 +9,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 --- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
 +++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
 @@ -0,0 +0,0 @@ public class PaperWorldConfig {
-             }
-         }
+     private void perPlayerMobSpawns() {
+         perPlayerMobSpawns = getBoolean("per-player-mob-spawns", false);
      }
 +
 +    public boolean generateFlatBedrock;
diff --git a/Spigot-Server-Patches/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch b/Spigot-Server-Patches/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
index ff59d05fa0..ee4cbb798c 100644
--- a/Spigot-Server-Patches/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
+++ b/Spigot-Server-Patches/Implement-Chunk-Priority-Urgency-System-for-Chunks.patch
@@ -587,8 +587,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      private int lastFoodSent = -99999999;
      private boolean lastSentSaturationZero = true;
 @@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
-         this.canPickUpLoot = true;
          this.maxHealthCache = this.getMaxHealth();
+         this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
      }
 +    // Paper start
 +    public BlockPosition getPointInFront(double inFront) {
@@ -1069,8 +1069,8 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +    }
 +    // Paper end
  
-     private static double getDistanceSquaredFromChunk(ChunkCoordIntPair chunkPos, Entity entity) { return a(chunkPos, entity); } // Paper - OBFHELPER
-     private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) {
+     public void updatePlayerMobTypeMap(Entity entity) {
+         if (!this.world.paperConfig.perPlayerMobSpawns) {
 @@ -0,0 +0,0 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
          List<CompletableFuture<Either<IChunkAccess, PlayerChunk.Failure>>> list = Lists.newArrayList();
          int j = chunkcoordintpair.x;
diff --git a/Spigot-Server-Patches/No-Tick-view-distance-implementation.patch b/Spigot-Server-Patches/No-Tick-view-distance-implementation.patch
index b497bd6820..47ac87a183 100644
--- a/Spigot-Server-Patches/No-Tick-view-distance-implementation.patch
+++ b/Spigot-Server-Patches/No-Tick-view-distance-implementation.patch
@@ -353,7 +353,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - no-tick view distance
      }
  
-     private static double getDistanceSquaredFromChunk(ChunkCoordIntPair chunkPos, Entity entity) { return a(chunkPos, entity); } // Paper - OBFHELPER
+     public void updatePlayerMobTypeMap(Entity entity) {
 @@ -0,0 +0,0 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
          completablefuture1.thenAcceptAsync((either) -> {
              either.mapLeft((chunk) -> {
diff --git a/Spigot-Server-Patches/Optimize-isOutsideRange-to-use-distance-maps.patch b/Spigot-Server-Patches/Optimize-isOutsideRange-to-use-distance-maps.patch
index bcb3df5f67..a536028223 100644
--- a/Spigot-Server-Patches/Optimize-isOutsideRange-to-use-distance-maps.patch
+++ b/Spigot-Server-Patches/Optimize-isOutsideRange-to-use-distance-maps.patch
@@ -290,6 +290,11 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - optimise PlayerChunkMap#isOutsideRange
      }
  
+     public void updatePlayerMobTypeMap(Entity entity) {
+@@ -0,0 +0,0 @@ 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);
diff --git a/Spigot-Server-Patches/Use-distance-map-to-optimise-entity-tracker.patch b/Spigot-Server-Patches/Use-distance-map-to-optimise-entity-tracker.patch
index 66bc7675c8..2a7b598adc 100644
--- a/Spigot-Server-Patches/Use-distance-map-to-optimise-entity-tracker.patch
+++ b/Spigot-Server-Patches/Use-distance-map-to-optimise-entity-tracker.patch
@@ -117,9 +117,9 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
      // Paper end
  
 @@ -0,0 +0,0 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
-         this.l = supplier;
          this.m = new VillagePlace(new File(this.w, "poi"), datafixer, flag, this.world); // Paper
          this.setViewDistance(i);
+         this.playerMobDistanceMap = this.world.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper
 +        // Paper start - use distance map to optimise entity tracker
 +        this.playerEntityTrackerTrackMaps = new com.destroystokyo.paper.util.misc.PlayerAreaMap[TRACKING_RANGE_TYPES.length];
 +        this.entityTrackerTrackRanges = new int[TRACKING_RANGE_TYPES.length];
@@ -161,7 +161,7 @@ index 0000000000000000000000000000000000000000..00000000000000000000000000000000
 +        // Paper end - use distance map to optimise entity tracker
      }
  
-     private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) {
+     public void updatePlayerMobTypeMap(Entity entity) {
 @@ -0,0 +0,0 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
      }
  
diff --git a/Spigot-Server-Patches/implement-optional-per-player-mob-spawns.patch b/Spigot-Server-Patches/implement-optional-per-player-mob-spawns.patch
new file mode 100644
index 0000000000..187c91562a
--- /dev/null
+++ b/Spigot-Server-Patches/implement-optional-per-player-mob-spawns.patch
@@ -0,0 +1,823 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: kickash32 <kickash32@gmail.com>
+Date: Mon, 19 Aug 2019 01:27:58 +0500
+Subject: [PATCH] implement optional per player mob spawns
+
+
+diff --git a/src/main/java/co/aikar/timings/WorldTimingsHandler.java b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/co/aikar/timings/WorldTimingsHandler.java
++++ b/src/main/java/co/aikar/timings/WorldTimingsHandler.java
+@@ -0,0 +0,0 @@ public class WorldTimingsHandler {
+ 
+ 
+     public final Timing miscMobSpawning;
++    public final Timing playerMobDistanceMapUpdate;
+ 
+     public final Timing poiUnload;
+     public final Timing chunkUnload;
+@@ -0,0 +0,0 @@ public class WorldTimingsHandler {
+ 
+ 
+         miscMobSpawning = Timings.ofSafe(name + "Mob spawning - Misc");
++        playerMobDistanceMapUpdate = Timings.ofSafe(name + "Per Player Mob Spawning - Distance Map Update");
+ 
+         poiUnload = Timings.ofSafe(name + "Chunk unload - POI");
+         chunkUnload = Timings.ofSafe(name + "Chunk unload - Chunk");
+diff --git a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
++++ b/src/main/java/com/destroystokyo/paper/PaperWorldConfig.java
+@@ -0,0 +0,0 @@ public class PaperWorldConfig {
+             }
+         }
+     }
++
++    public boolean perPlayerMobSpawns = false;
++    private void perPlayerMobSpawns() {
++        perPlayerMobSpawns = getBoolean("per-player-mob-spawns", false);
++    }
+ }
+diff --git a/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/util/PlayerMobDistanceMap.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.util;
++
++import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
++import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
++import net.minecraft.server.ChunkCoordIntPair;
++import net.minecraft.server.EntityPlayer;
++import net.minecraft.server.SectionPosition;
++import org.spigotmc.AsyncCatcher;
++import java.util.HashMap;
++import java.util.List;
++import java.util.Map;
++import java.util.Set;
++
++/** @author Spottedleaf */
++public final class PlayerMobDistanceMap {
++
++    private static final PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> EMPTY_SET = new PooledHashSets.PooledObjectLinkedOpenHashSet<>();
++
++    private final Map<EntityPlayer, SectionPosition> players = new HashMap<>();
++    // we use linked for better iteration.
++    private final Long2ObjectOpenHashMap<PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer>> playerMap = new Long2ObjectOpenHashMap<>(32, 0.5f);
++    private int viewDistance;
++
++    private final PooledHashSets<EntityPlayer> pooledHashSets = new PooledHashSets<>();
++
++    public PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> getPlayersInRange(final ChunkCoordIntPair chunkPos) {
++        return this.getPlayersInRange(chunkPos.x, chunkPos.z);
++    }
++
++    public PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> getPlayersInRange(final int chunkX, final int chunkZ) {
++        return this.playerMap.getOrDefault(ChunkCoordIntPair.pair(chunkX, chunkZ), EMPTY_SET);
++    }
++
++    public void update(final List<EntityPlayer> currentPlayers, final int newViewDistance) {
++        AsyncCatcher.catchOp("Distance map update");
++        final ObjectLinkedOpenHashSet<EntityPlayer> gone = new ObjectLinkedOpenHashSet<>(this.players.keySet());
++
++        final int oldViewDistance = this.viewDistance;
++        this.viewDistance = newViewDistance;
++
++        for (final EntityPlayer player : currentPlayers) {
++            if (player.isSpectator() || !player.affectsSpawning) {
++                continue; // will be left in 'gone' (or not added at all)
++            }
++
++            gone.remove(player);
++
++            final SectionPosition newPosition = player.getPlayerMapSection();
++            final SectionPosition oldPosition = this.players.put(player, newPosition);
++
++            if (oldPosition == null) {
++                this.addNewPlayer(player, newPosition, newViewDistance);
++            } else {
++                this.updatePlayer(player, oldPosition, newPosition, oldViewDistance, newViewDistance);
++            }
++            //this.validatePlayer(player, newViewDistance); // debug only
++        }
++
++        for (final EntityPlayer player : gone) {
++            final SectionPosition oldPosition = this.players.remove(player);
++            if (oldPosition != null) {
++                this.removePlayer(player, oldPosition, oldViewDistance);
++            }
++        }
++    }
++
++    // expensive op, only for debug
++    private void validatePlayer(final EntityPlayer player, final int viewDistance) {
++        int entiesGot = 0;
++        int expectedEntries = (2 * viewDistance + 1);
++        expectedEntries *= expectedEntries;
++
++        final SectionPosition currPosition = player.getPlayerMapSection();
++
++        final int centerX = currPosition.getX();
++        final int centerZ = currPosition.getZ();
++
++        for (final Long2ObjectLinkedOpenHashMap.Entry<PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer>> entry : this.playerMap.long2ObjectEntrySet()) {
++            final long key = entry.getLongKey();
++            final PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> map = entry.getValue();
++
++            if (map.referenceCount == 0) {
++                throw new IllegalStateException("Invalid map");
++            }
++
++            if (map.set.contains(player)) {
++                ++entiesGot;
++
++                final int chunkX = ChunkCoordIntPair.getX(key);
++                final int chunkZ = ChunkCoordIntPair.getZ(key);
++
++                final int dist = Math.max(Math.abs(chunkX - centerX), Math.abs(chunkZ - centerZ));
++
++                if (dist > viewDistance) {
++                    throw new IllegalStateException("Expected view distance " + viewDistance + ", got " + dist);
++                }
++            }
++        }
++
++        if (entiesGot != expectedEntries) {
++            throw new IllegalStateException("Expected " + expectedEntries + ", got " + entiesGot);
++        }
++    }
++
++    private void addPlayerTo(final EntityPlayer player, final int chunkX, final int chunkZ) {
++       this.playerMap.compute(ChunkCoordIntPair.pair(chunkX, chunkZ), (final Long key, final PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> players) -> {
++           if (players == null) {
++               return player.cachedSingleMobDistanceMap;
++           } else {
++               return PlayerMobDistanceMap.this.pooledHashSets.findMapWith(players, player);
++           }
++        });
++    }
++
++    private void removePlayerFrom(final EntityPlayer player, final int chunkX, final int chunkZ) {
++        this.playerMap.compute(ChunkCoordIntPair.pair(chunkX, chunkZ), (final Long keyInMap, final PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> players) -> {
++            return PlayerMobDistanceMap.this.pooledHashSets.findMapWithout(players, player); // rets null instead of an empty map
++        });
++    }
++
++    private void updatePlayer(final EntityPlayer player, final SectionPosition oldPosition, final SectionPosition newPosition, final int oldViewDistance, final int newViewDistance) {
++        final int toX = newPosition.getX();
++        final int toZ = newPosition.getZ();
++        final int fromX = oldPosition.getX();
++        final int fromZ = oldPosition.getZ();
++
++        final int dx = toX - fromX;
++        final int dz = toZ - fromZ;
++
++        final int totalX = Math.abs(fromX - toX);
++        final int totalZ = Math.abs(fromZ - toZ);
++
++        if (Math.max(totalX, totalZ) > (2 * oldViewDistance)) {
++            // teleported?
++            this.removePlayer(player, oldPosition, oldViewDistance);
++            this.addNewPlayer(player, newPosition, newViewDistance);
++            return;
++        }
++
++        // x axis is width
++        // z axis is height
++        // right refers to the x axis of where we moved
++        // top refers to the z axis of where we moved
++
++        if (oldViewDistance == newViewDistance) {
++            // same view distance
++
++            // used for relative positioning
++            final int up = 1 | (dz >> (Integer.SIZE - 1)); // 1 if dz >= 0, -1 otherwise
++            final int right = 1 | (dx >> (Integer.SIZE - 1)); // 1 if dx >= 0, -1 otherwise
++
++            // The area excluded by overlapping the two view distance squares creates four rectangles:
++            // Two on the left, and two on the right. The ones on the left we consider the "removed" section
++            // and on the right the "added" section.
++            // https://i.imgur.com/MrnOBgI.png is a reference image. Note that the outside border is not actually
++            // exclusive to the regions they surround.
++
++            // 4 points of the rectangle
++            int maxX; // exclusive
++            int minX; // inclusive
++            int maxZ; // exclusive
++            int minZ; // inclusive
++
++            if (dx != 0) {
++                // handle right addition
++
++                maxX = toX + (oldViewDistance * right) + right; // exclusive
++                minX = fromX + (oldViewDistance * right) + right; // inclusive
++                maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++                minZ = toZ - (oldViewDistance * up); // inclusive
++
++                for (int currX = minX; currX != maxX; currX += right) {
++                    for (int currZ = minZ; currZ != maxZ; currZ += up) {
++                        this.addPlayerTo(player, currX, currZ);
++                    }
++                }
++            }
++
++            if (dz != 0) {
++                // handle up addition
++
++                maxX = toX + (oldViewDistance * right) + right; // exclusive
++                minX = toX - (oldViewDistance * right); // inclusive
++                maxZ = toZ + (oldViewDistance * up) + up; // exclusive
++                minZ = fromZ + (oldViewDistance * up) + up; // inclusive
++
++                for (int currX = minX; currX != maxX; currX += right) {
++                    for (int currZ = minZ; currZ != maxZ; currZ += up) {
++                        this.addPlayerTo(player, currX, currZ);
++                    }
++                }
++            }
++
++            if (dx != 0) {
++                // handle left removal
++
++                maxX = toX - (oldViewDistance * right); // exclusive
++                minX = fromX - (oldViewDistance * right); // inclusive
++                maxZ = fromZ + (oldViewDistance * up) + up; // exclusive
++                minZ = toZ - (oldViewDistance * up); // inclusive
++
++                for (int currX = minX; currX != maxX; currX += right) {
++                    for (int currZ = minZ; currZ != maxZ; currZ += up) {
++                        this.removePlayerFrom(player, currX, currZ);
++                    }
++                }
++            }
++
++            if (dz != 0) {
++                // handle down removal
++
++                maxX = fromX + (oldViewDistance * right) + right; // exclusive
++                minX = fromX - (oldViewDistance * right); // inclusive
++                maxZ = toZ - (oldViewDistance * up); // exclusive
++                minZ = fromZ - (oldViewDistance * up); // inclusive
++
++                for (int currX = minX; currX != maxX; currX += right) {
++                    for (int currZ = minZ; currZ != maxZ; currZ += up) {
++                        this.removePlayerFrom(player, currX, currZ);
++                    }
++                }
++            }
++        } else {
++            // different view distance
++            // for now :)
++            this.removePlayer(player, oldPosition, oldViewDistance);
++            this.addNewPlayer(player, newPosition, newViewDistance);
++        }
++    }
++
++    private void removePlayer(final EntityPlayer player, final SectionPosition position, final int viewDistance) {
++        final int x = position.getX();
++        final int z = position.getZ();
++
++        for (int xoff = -viewDistance; xoff <= viewDistance; ++xoff) {
++            for (int zoff = -viewDistance; zoff <= viewDistance; ++zoff) {
++                this.removePlayerFrom(player, x + xoff, z + zoff);
++            }
++        }
++    }
++
++    private void addNewPlayer(final EntityPlayer player, final SectionPosition position, final int viewDistance) {
++        final int x = position.getX();
++        final int z = position.getZ();
++
++        for (int xoff = -viewDistance; xoff <= viewDistance; ++xoff) {
++            for (int zoff = -viewDistance; zoff <= viewDistance; ++zoff) {
++                this.addPlayerTo(player, x + xoff, z + zoff);
++            }
++        }
++    }
++}
+diff --git a/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java
+new file mode 100644
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000
+--- /dev/null
++++ b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java
+@@ -0,0 +0,0 @@
++package com.destroystokyo.paper.util;
++
++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
++import it.unimi.dsi.fastutil.objects.ObjectLinkedOpenHashSet;
++import java.lang.ref.WeakReference;
++import java.util.Iterator;
++
++/** @author Spottedleaf */
++public class PooledHashSets<E> {
++
++    // we really want to avoid that equals() check as much as possible...
++    protected final Object2ObjectOpenHashMap<PooledObjectLinkedOpenHashSet<E>, PooledObjectLinkedOpenHashSet<E>> mapPool = new Object2ObjectOpenHashMap<>(64, 0.25f);
++
++    protected void decrementReferenceCount(final PooledObjectLinkedOpenHashSet<E> current) {
++        if (current.referenceCount == 0) {
++            throw new IllegalStateException("Cannot decrement reference count for " + current);
++        }
++        if (current.referenceCount == -1 || --current.referenceCount > 0) {
++            return;
++        }
++
++        this.mapPool.remove(current);
++        return;
++    }
++
++    public PooledObjectLinkedOpenHashSet<E> findMapWith(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
++        final PooledObjectLinkedOpenHashSet<E> cached = current.getAddCache(object);
++
++        if (cached != null) {
++            if (cached.referenceCount != -1) {
++                ++cached.referenceCount;
++            }
++
++            decrementReferenceCount(current);
++
++            return cached;
++        }
++
++        if (!current.add(object)) {
++            return current;
++        }
++
++        // we use get/put since we use a different key on put
++        PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
++
++        if (ret == null) {
++            ret = new PooledObjectLinkedOpenHashSet<>(current);
++            current.remove(object);
++            this.mapPool.put(ret, ret);
++            ret.referenceCount = 1;
++        } else {
++            if (ret.referenceCount != -1) {
++                ++ret.referenceCount;
++            }
++            current.remove(object);
++        }
++
++        current.updateAddCache(object, ret);
++
++        decrementReferenceCount(current);
++        return ret;
++    }
++
++    // rets null if current.size() == 1
++    public PooledObjectLinkedOpenHashSet<E> findMapWithout(final PooledObjectLinkedOpenHashSet<E> current, final E object) {
++        if (current.set.size() == 1) {
++            decrementReferenceCount(current);
++            return null;
++        }
++
++        final PooledObjectLinkedOpenHashSet<E> cached = current.getRemoveCache(object);
++
++        if (cached != null) {
++            if (cached.referenceCount != -1) {
++                ++cached.referenceCount;
++            }
++
++            decrementReferenceCount(current);
++
++            return cached;
++        }
++
++        if (!current.remove(object)) {
++            return current;
++        }
++
++        // we use get/put since we use a different key on put
++        PooledObjectLinkedOpenHashSet<E> ret = this.mapPool.get(current);
++
++        if (ret == null) {
++            ret = new PooledObjectLinkedOpenHashSet<>(current);
++            current.add(object);
++            this.mapPool.put(ret, ret);
++            ret.referenceCount = 1;
++        } else {
++            if (ret.referenceCount != -1) {
++                ++ret.referenceCount;
++            }
++            current.add(object);
++        }
++
++        current.updateRemoveCache(object, ret);
++
++        decrementReferenceCount(current);
++        return ret;
++    }
++
++    public static final class PooledObjectLinkedOpenHashSet<E> implements Iterable<E> {
++
++        private static final WeakReference NULL_REFERENCE = new WeakReference(null);
++
++        final ObjectLinkedOpenHashSet<E> set;
++        int referenceCount; // -1 if special
++        int hash; // optimize hashcode
++
++        // add cache
++        WeakReference<E> lastAddObject = NULL_REFERENCE;
++        WeakReference<PooledObjectLinkedOpenHashSet<E>> lastAddMap = NULL_REFERENCE;
++
++        // remove cache
++        WeakReference<E> lastRemoveObject = NULL_REFERENCE;
++        WeakReference<PooledObjectLinkedOpenHashSet<E>> lastRemoveMap = NULL_REFERENCE;
++
++        public PooledObjectLinkedOpenHashSet() {
++            this.set = new ObjectLinkedOpenHashSet<>(2, 0.6f);
++        }
++
++        public PooledObjectLinkedOpenHashSet(final E single) {
++            this();
++            this.referenceCount = -1;
++            this.add(single);
++        }
++
++        public PooledObjectLinkedOpenHashSet(final PooledObjectLinkedOpenHashSet<E> other) {
++            this.set = other.set.clone();
++            this.hash = other.hash;
++        }
++
++        // from https://github.com/Spottedleaf/ConcurrentUtil/blob/master/src/main/java/ca/spottedleaf/concurrentutil/util/IntegerUtil.java
++        // generated by https://github.com/skeeto/hash-prospector
++        static int hash0(int x) {
++            x *= 0x36935555;
++            x ^= x >>> 16;
++            return x;
++        }
++
++        public PooledObjectLinkedOpenHashSet<E> getAddCache(final E element) {
++            final E currentAdd = this.lastAddObject.get();
++
++            if (currentAdd == null || !(currentAdd == element || currentAdd.equals(element))) {
++                return null;
++            }
++
++            final PooledObjectLinkedOpenHashSet<E> map = this.lastAddMap.get();
++            if (map == null || map.referenceCount == 0) {
++                // we need to ret null if ref count is zero as calling code will assume the map is in use
++                return null;
++            }
++
++            return map;
++        }
++
++        public PooledObjectLinkedOpenHashSet<E> getRemoveCache(final E element) {
++            final E currentRemove = this.lastRemoveObject.get();
++
++            if (currentRemove == null || !(currentRemove == element || currentRemove.equals(element))) {
++                return null;
++            }
++
++            final PooledObjectLinkedOpenHashSet<E> map = this.lastRemoveMap.get();
++            if (map == null || map.referenceCount == 0) {
++                // we need to ret null if ref count is zero as calling code will assume the map is in use
++                return null;
++            }
++
++            return map;
++        }
++
++        public void updateAddCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
++            this.lastAddObject = new WeakReference<>(element);
++            this.lastAddMap = new WeakReference<>(map);
++        }
++
++        public void updateRemoveCache(final E element, final PooledObjectLinkedOpenHashSet<E> map) {
++            this.lastRemoveObject = new WeakReference<>(element);
++            this.lastRemoveMap = new WeakReference<>(map);
++        }
++
++        boolean add(final E element) {
++            boolean added =  this.set.add(element);
++
++            if (added) {
++                this.hash += hash0(element.hashCode());
++            }
++
++            return added;
++        }
++
++        boolean remove(Object element) {
++            boolean removed = this.set.remove(element);
++
++            if (removed) {
++                this.hash -= hash0(element.hashCode());
++            }
++
++            return removed;
++        }
++
++        @Override
++        public Iterator<E> iterator() {
++            return this.set.iterator();
++        }
++
++        @Override
++        public int hashCode() {
++            return this.hash;
++        }
++
++        @Override
++        public boolean equals(final Object other) {
++            if (!(other instanceof PooledObjectLinkedOpenHashSet)) {
++                return false;
++            }
++            if (this.referenceCount == 0) {
++                return other == this;
++            } else {
++                if (other == this) {
++                    // Unfortunately we are never equal to our own instance while in use!
++                    return false;
++                }
++                return this.hash == ((PooledObjectLinkedOpenHashSet)other).hash && this.set.equals(((PooledObjectLinkedOpenHashSet)other).set);
++            }
++        }
++
++        @Override
++        public String toString() {
++            return "PooledHashSet: size: " + this.set.size() + ", reference count: " + this.referenceCount + ", hash: " +
++                this.hashCode() + ", identity: " + System.identityHashCode(this) + " map: " + this.set.toString();
++        }
++    }
++}
+diff --git a/src/main/java/net/minecraft/server/ChunkProviderServer.java b/src/main/java/net/minecraft/server/ChunkProviderServer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/ChunkProviderServer.java
++++ b/src/main/java/net/minecraft/server/ChunkProviderServer.java
+@@ -0,0 +0,0 @@ public class ChunkProviderServer extends IChunkProvider {
+             this.world.getMethodProfiler().enter("naturalSpawnCount");
+             this.world.timings.countNaturalMobs.startTiming(); // Paper - timings
+             int l = this.chunkMapDistance.b();
+-            SpawnerCreature.d spawnercreature_d = SpawnerCreature.a(l, this.world.z(), this::a);
++            // Paper start - per player mob spawning
++            SpawnerCreature.d spawnercreature_d; // moved down
++            if (this.playerChunkMap.playerMobDistanceMap != null) {
++                // update distance map
++                this.world.timings.playerMobDistanceMapUpdate.startTiming();
++                this.playerChunkMap.playerMobDistanceMap.update(this.world.players, this.playerChunkMap.viewDistance);
++                this.world.timings.playerMobDistanceMapUpdate.stopTiming();
++                // re-set mob counts
++                for (EntityPlayer player : this.world.players) {
++                    Arrays.fill(player.mobCounts, 0);
++                }
++                spawnercreature_d = SpawnerCreature.countMobs(l, this.world.z(), this::a, true);
++            } else {
++                spawnercreature_d = SpawnerCreature.countMobs(l, this.world.z(), this::a, false);
++            }
++            // Paper end
+             this.world.timings.countNaturalMobs.stopTiming(); // Paper - timings
+ 
+             this.p = spawnercreature_d;
+diff --git a/src/main/java/net/minecraft/server/EntityPlayer.java b/src/main/java/net/minecraft/server/EntityPlayer.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/EntityPlayer.java
++++ b/src/main/java/net/minecraft/server/EntityPlayer.java
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+     public boolean queueHealthUpdatePacket = false;
+     public net.minecraft.server.PacketPlayOutUpdateHealth queuedHealthUpdatePacket;
+     // Paper end
++    // Paper start - mob spawning rework
++    public static final int ENUMCREATURETYPE_TOTAL_ENUMS = EnumCreatureType.values().length;
++    public final int[] mobCounts = new int[ENUMCREATURETYPE_TOTAL_ENUMS]; // Paper
++    public final com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<EntityPlayer> cachedSingleMobDistanceMap;
++    // Paper end
+ 
+     // CraftBukkit start
+     public String displayName;
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+         this.displayName = this.getName();
+         this.canPickUpLoot = true;
+         this.maxHealthCache = this.getMaxHealth();
++        this.cachedSingleMobDistanceMap = new com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<>(this); // Paper
+     }
+ 
+     // Yes, this doesn't match Vanilla, but it's the best we can do for now.
+@@ -0,0 +0,0 @@ public class EntityPlayer extends EntityHuman implements ICrafting {
+ 
+     }
+ 
++    public final SectionPosition getPlayerMapSection() { return this.N(); } // Paper - OBFHELPER
+     public SectionPosition N() {
+         return this.cq;
+     }
+diff --git a/src/main/java/net/minecraft/server/EntityTypes.java b/src/main/java/net/minecraft/server/EntityTypes.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/EntityTypes.java
++++ b/src/main/java/net/minecraft/server/EntityTypes.java
+@@ -0,0 +0,0 @@ public class EntityTypes<T extends Entity> {
+         return this.bk;
+     }
+ 
++    public final EnumCreatureType getEnumCreatureType() { return this.e(); } // Paper - OBFHELPER
+     public EnumCreatureType e() {
+         return this.bf;
+     }
+diff --git a/src/main/java/net/minecraft/server/PlayerChunkMap.java b/src/main/java/net/minecraft/server/PlayerChunkMap.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/PlayerChunkMap.java
++++ b/src/main/java/net/minecraft/server/PlayerChunkMap.java
+@@ -0,0 +0,0 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+     public final Int2ObjectMap<PlayerChunkMap.EntityTracker> trackedEntities;
+     private final Long2ByteMap z;
+     private final Queue<Runnable> A; private final Queue<Runnable> getUnloadQueueTasks() { return this.A; } // Paper - OBFHELPER
+-    private int viewDistance;
++    int viewDistance; // Paper - private -> package private
++    public final com.destroystokyo.paper.util.PlayerMobDistanceMap playerMobDistanceMap; // Paper
+ 
+     // CraftBukkit start - recursion-safe executor for Chunk loadCallback() and unloadCallback()
+     public final CallbackExecutor callbackExecutor = new CallbackExecutor();
+@@ -0,0 +0,0 @@ public class PlayerChunkMap extends IChunkLoader implements PlayerChunk.d {
+         this.l = supplier;
+         this.m = new VillagePlace(new File(this.w, "poi"), datafixer, flag, this.world); // Paper
+         this.setViewDistance(i);
++        this.playerMobDistanceMap = this.world.paperConfig.perPlayerMobSpawns ? new com.destroystokyo.paper.util.PlayerMobDistanceMap() : null; // Paper
++    }
++
++    public void updatePlayerMobTypeMap(Entity entity) {
++        if (!this.world.paperConfig.perPlayerMobSpawns) {
++            return;
++        }
++        int chunkX = (int)Math.floor(entity.locX()) >> 4;
++        int chunkZ = (int)Math.floor(entity.locZ()) >> 4;
++        int index = entity.getEntityType().getEnumCreatureType().ordinal();
++
++        for (EntityPlayer player : this.playerMobDistanceMap.getPlayersInRange(chunkX, chunkZ)) {
++            ++player.mobCounts[index];
++        }
++    }
++
++    public int getMobCountNear(EntityPlayer entityPlayer, EnumCreatureType enumCreatureType) {
++        return entityPlayer.mobCounts[enumCreatureType.ordinal()];
+     }
+ 
+     private static double a(ChunkCoordIntPair chunkcoordintpair, Entity entity) {
+diff --git a/src/main/java/net/minecraft/server/SpawnerCreature.java b/src/main/java/net/minecraft/server/SpawnerCreature.java
+index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 100644
+--- a/src/main/java/net/minecraft/server/SpawnerCreature.java
++++ b/src/main/java/net/minecraft/server/SpawnerCreature.java
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+     });
+ 
+     public static SpawnerCreature.d a(int i, Iterable<Entity> iterable, SpawnerCreature.b spawnercreature_b) {
++        // Paper start - add countMobs parameter
++        return countMobs(i, iterable, spawnercreature_b, false);
++    }
++    public static SpawnerCreature.d countMobs(int i, Iterable<Entity> iterable, SpawnerCreature.b spawnercreature_b, boolean countMobs) {
++        // Paper end - add countMobs parameter
+         SpawnerCreatureProbabilities spawnercreatureprobabilities = new SpawnerCreatureProbabilities();
+         Object2IntOpenHashMap<EnumCreatureType> object2intopenhashmap = new Object2IntOpenHashMap();
+         Iterator iterator = iterable.iterator();
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+                     }
+ 
+                     object2intopenhashmap.addTo(enumcreaturetype, 1);
++                    // Paper start
++                    if (countMobs) {
++                        ((WorldServer)chunk.world).getChunkProvider().playerChunkMap.updatePlayerMobTypeMap(entity);
++                    }
++                    // Paper end
+                 });
+             }
+         }
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+                 continue;
+             }
+ 
++            // Paper start - only allow spawns upto the limit per chunk and update count afterwards
++            int currEntityCount = spawnercreature_d.getEntityCountsByType().getInt(enumcreaturetype);
++            int k1 = limit * spawnercreature_d.getSpawnerChunks() / SpawnerCreature.b;
++            int difference = k1 - currEntityCount;
++
++            if (worldserver.paperConfig.perPlayerMobSpawns) {
++                int minDiff = Integer.MAX_VALUE;
++                for (EntityPlayer entityplayer : worldserver.getChunkProvider().playerChunkMap.playerMobDistanceMap.getPlayersInRange(chunk.getPos())) {
++                    minDiff = Math.min(limit - worldserver.getChunkProvider().playerChunkMap.getMobCountNear(entityplayer, enumcreaturetype), minDiff);
++                }
++                difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
++            }
++            // Paper end
++
++            if (difference > 0) { // Paper
+             if ((flag || !enumcreaturetype.d()) && (flag1 || enumcreaturetype.d()) && (flag2 || !enumcreaturetype.e()) && spawnercreature_d.a(enumcreaturetype, limit)) {
+                 // CraftBukkit end
+-                a(enumcreaturetype, worldserver, chunk, (entitytypes, blockposition, ichunkaccess) -> {
++                int spawnCount = spawnMobs(enumcreaturetype, worldserver, chunk, (entitytypes, blockposition, ichunkaccess) -> {
+                     return spawnercreature_d.a(entitytypes, blockposition, ichunkaccess);
+                 }, (entityinsentient, ichunkaccess) -> {
+                     spawnercreature_d.a(entityinsentient, ichunkaccess);
++                },
++                limit, worldserver.paperConfig.perPlayerMobSpawns ? worldserver.getChunkProvider().playerChunkMap::updatePlayerMobTypeMap : null);
++                spawnercreature_d.getEntityCountsByType().mergeInt(enumcreaturetype, 0, (keyInMap, valueInMap) -> {
++                    return Integer.valueOf(spawnCount + valueInMap.intValue());
+                 });
++                } // Paper
+             }
+         }
+ 
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+     }
+ 
+     public static void a(EnumCreatureType enumcreaturetype, WorldServer worldserver, Chunk chunk, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a) {
++        // Paper start - add parameters and int ret type
++        spawnMobs(enumcreaturetype, worldserver, chunk, spawnercreature_c, spawnercreature_a, Integer.MAX_VALUE, null);
++    }
++    public static int spawnMobs(EnumCreatureType enumcreaturetype, WorldServer worldserver, Chunk chunk, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a, int maxSpawns, Consumer<Entity> trackEntity) {
++        // Paper end - add parameters and int ret type
+         BlockPosition blockposition = getRandomPosition(worldserver, chunk);
+ 
+         if (blockposition.getY() >= 1) {
+-            a(enumcreaturetype, worldserver, (IChunkAccess) chunk, blockposition, spawnercreature_c, spawnercreature_a);
++            return spawnMobsInternal(enumcreaturetype, worldserver, (IChunkAccess) chunk, blockposition, spawnercreature_c, spawnercreature_a, maxSpawns, trackEntity);
+         }
++        return 0; // Paper
+     }
+ 
+     public static void a(EnumCreatureType enumcreaturetype, WorldServer worldserver, IChunkAccess ichunkaccess, BlockPosition blockposition, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a) {
++        // Paper start - add maxSpawns parameter and return spawned mobs
++        spawnMobsInternal(enumcreaturetype, worldserver, ichunkaccess, blockposition, spawnercreature_c, spawnercreature_a, Integer.MAX_VALUE, null);
++    }
++    public static int spawnMobsInternal(EnumCreatureType enumcreaturetype, WorldServer worldserver, IChunkAccess ichunkaccess, BlockPosition blockposition, SpawnerCreature.c spawnercreature_c, SpawnerCreature.a spawnercreature_a, int maxSpawns, Consumer<Entity> trackEntity) {
++        // Paper end - add maxSpawns parameter and return spawned mobs
+         StructureManager structuremanager = worldserver.getStructureManager();
+         ChunkGenerator chunkgenerator = worldserver.getChunkProvider().getChunkGenerator();
+         int i = blockposition.getY();
+         IBlockData iblockdata = worldserver.getTypeIfLoadedAndInBounds(blockposition); // Paper - don't load chunks for mob spawn
++        int j = 0; // Paper - moved up
+ 
+         if (iblockdata != null && !iblockdata.isOccluding(ichunkaccess, blockposition)) { // Paper - don't load chunks for mob spawn
+             BlockPosition.MutableBlockPosition blockposition_mutableblockposition = new BlockPosition.MutableBlockPosition();
+-            int j = 0;
++            // Paper - moved up
+             int k = 0;
+ 
+             while (k < 3) {
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+                                     // Paper start
+                                     Boolean doSpawning = a(worldserver, enumcreaturetype, structuremanager, chunkgenerator, biomebase_biomemeta, blockposition_mutableblockposition, d2);
+                                     if (doSpawning == null) {
+-                                        return;
++                                        return j; // Paper
+                                     }
+                                     if (doSpawning.booleanValue() && spawnercreature_c.test(biomebase_biomemeta.c, blockposition_mutableblockposition, ichunkaccess)) { // Paper end
+                                         EntityInsentient entityinsentient = a(worldserver, biomebase_biomemeta.c);
+ 
+                                         if (entityinsentient == null) {
+-                                            return;
++                                            return j; // Paper
+                                         }
+ 
+                                         entityinsentient.setPositionRotation(d0, (double) i, d1, worldserver.random.nextFloat() * 360.0F, 0.0F);
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+                                             groupdataentity = entityinsentient.prepare(worldserver, worldserver.getDamageScaler(entityinsentient.getChunkCoordinates()), EnumMobSpawn.NATURAL, groupdataentity, (NBTTagCompound) null);
+                                             // CraftBukkit start
+                                             if (worldserver.addEntity(entityinsentient, SpawnReason.NATURAL)) {
+-                                                ++j;
++                                                ++j; // Paper - force diff on name change - we expect this to be the total amount spawned
+                                                 ++k1;
+                                                 spawnercreature_a.run(entityinsentient, ichunkaccess);
++                                                // Paper start
++                                                if (trackEntity != null) {
++                                                    trackEntity.accept(entityinsentient);
++                                                }
++                                                // Paper end
+                                             }
+                                             // CraftBukkit end
+-                                            if (j >= entityinsentient.getMaxSpawnGroup()) {
+-                                                return;
++                                            if (j >= entityinsentient.getMaxSpawnGroup() || j >= maxSpawns) { // Paper
++                                                return j; // Paper
+                                             }
+ 
+                                             if (entityinsentient.c(k1)) {
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+             }
+ 
+         }
++        return j; // Paper
+     }
+ 
+     private static boolean a(WorldServer worldserver, IChunkAccess ichunkaccess, BlockPosition.MutableBlockPosition blockposition_mutableblockposition, double d0) {
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+ 
+     public static class d {
+ 
+-        private final int a;
+-        private final Object2IntOpenHashMap<EnumCreatureType> b;
++        private final int a; final int getSpawnerChunks() { return this.a; } // Paper - OBFHELPER
++        private final Object2IntOpenHashMap<EnumCreatureType> b; final Object2IntMap<EnumCreatureType> getEntityCountsByType() { return this.b; } // Paper - OBFHELPER
+         private final SpawnerCreatureProbabilities c;
+         private final Object2IntMap<EnumCreatureType> d;
+         @Nullable
+@@ -0,0 +0,0 @@ public final class SpawnerCreature {
+ 
+         // CraftBukkit start
+         private boolean a(EnumCreatureType enumcreaturetype, int limit) {
+-            int i = limit * this.a / SpawnerCreature.b;
++            int i = limit * this.a / SpawnerCreature.b; // Paper - diff on change, needed in the spawn method
+             // CraftBukkit end
+ 
+             return this.b.getInt(enumcreaturetype) < i;