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/com/destroystokyo/paper/util/PooledHashSets.java b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java
new file mode 100644
index 0000000000000000000000000000000000000000..11de56afaf059b00fa5bec293516bcdce7c4b2b9
--- /dev/null
+++ b/src/main/java/com/destroystokyo/paper/util/PooledHashSets.java
@@ -0,0 +1,241 @@
+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/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java
index b3dc360658ec7449c063e7f50b11fd188c5768a0..6a08bd2b13a2c9dcc47a469607d0dd8ff79c0328 100644
--- a/src/main/java/net/minecraft/server/level/ChunkMap.java
+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java
@@ -145,6 +145,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
     private final Long2LongMap chunkSaveCooldowns;
     private final Queue<Runnable> unloadQueue;
     int viewDistance;
+    public final com.destroystokyo.paper.util.misc.PlayerAreaMap playerMobDistanceMap; // Paper
 
     // Paper - rewrite chunk system
 
@@ -157,11 +158,21 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         int chunkX = MCUtil.getChunkCoordinate(player.getX());
         int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
         // Note: players need to be explicitly added to distance maps before they can be updated
+        // Paper start - per player mob spawning
+        if (this.playerMobDistanceMap != null) {
+            this.playerMobDistanceMap.add(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player));
+        }
+        // Paper end - per player mob spawning
     }
 
     void removePlayerFromDistanceMaps(ServerPlayer player) {
         this.playerChunkManager.removePlayer(player); // Paper - replace chunk loader
 
+        // Paper start - per player mob spawning
+        if (this.playerMobDistanceMap != null) {
+            this.playerMobDistanceMap.remove(player);
+        }
+        // Paper end - per player mob spawning
     }
 
     void updateMaps(ServerPlayer player) {
@@ -169,6 +180,11 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         int chunkZ = MCUtil.getChunkCoordinate(player.getZ());
         // Note: players need to be explicitly added to distance maps before they can be updated
         this.playerChunkManager.updatePlayer(player); // Paper - replace chunk loader
+        // Paper start - per player mob spawning
+        if (this.playerMobDistanceMap != null) {
+            this.playerMobDistanceMap.update(player, chunkX, chunkZ, io.papermc.paper.chunk.system.ChunkSystem.getTickViewDistance(player));
+        }
+        // Paper end - per player mob spawning
     }
     // Paper end
     // Paper start
@@ -250,6 +266,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         this.dataRegionManager = new io.papermc.paper.chunk.SingleThreadChunkRegionManager(this.level, 2, (1.0 / 3.0), 1, 6, "Data", DataRegionData::new, DataRegionSectionData::new);
         this.regionManagers.add(this.dataRegionManager);
         // Paper end
+        this.playerMobDistanceMap = this.level.paperConfig().entities.spawning.perPlayerMobSpawns ? new com.destroystokyo.paper.util.misc.PlayerAreaMap(this.pooledLinkedPlayerHashSets) : null; // Paper
     }
 
     protected ChunkGenerator generator() {
@@ -271,6 +288,31 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider
         });
     }
 
+    // Paper start
+    public void updatePlayerMobTypeMap(Entity entity) {
+        if (!this.level.paperConfig().entities.spawning.perPlayerMobSpawns) {
+            return;
+        }
+        int index = entity.getType().getCategory().ordinal();
+
+        final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> inRange = this.playerMobDistanceMap.getObjectsInRange(entity.chunkPosition());
+        if (inRange == null) {
+            return;
+        }
+        final Object[] backingSet = inRange.getBackingSet();
+        for (int i = 0; i < backingSet.length; i++) {
+            if (!(backingSet[i] instanceof final ServerPlayer player)) {
+                continue;
+            }
+            ++player.mobCounts[index];
+        }
+    }
+
+    public int getMobCountNear(ServerPlayer entityPlayer, net.minecraft.world.entity.MobCategory mobCategory) {
+        return entityPlayer.mobCounts[mobCategory.ordinal()];
+    }
+    // Paper end
+
     private static double euclideanDistanceSquared(ChunkPos pos, Entity entity) {
         double d0 = (double) SectionPos.sectionToBlockCoord(pos.x, 8);
         double d1 = (double) SectionPos.sectionToBlockCoord(pos.z, 8);
diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
index 3a2cacbbba15d48428147842590851a57b3f3df7..8b8c8970e1d478edc3a0231556bf92f8263392c1 100644
--- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java
+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java
@@ -692,7 +692,18 @@ public class ServerChunkCache extends ChunkSource {
             gameprofilerfiller.push("naturalSpawnCount");
             this.level.timings.countNaturalMobs.startTiming(); // Paper - timings
             int l = this.distanceManager.getNaturalSpawnChunkCount();
-            NaturalSpawner.SpawnState spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, new LocalMobCapCalculator(this.chunkMap));
+            // Paper start - per player mob spawning
+            NaturalSpawner.SpawnState spawnercreature_d; // moved down
+            if ((this.spawnFriendlies || this.spawnEnemies) && this.chunkMap.playerMobDistanceMap != null) { // don't count mobs when animals and monsters are disabled
+                // re-set mob counts
+                for (ServerPlayer player : this.level.players) {
+                    Arrays.fill(player.mobCounts, 0);
+                }
+                spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, null, true);
+            } else {
+                spawnercreature_d = NaturalSpawner.createState(l, this.level.getAllEntities(), this::getFullChunk, this.chunkMap.playerMobDistanceMap == null ? new LocalMobCapCalculator(this.chunkMap) : null, false);
+            }
+            // Paper end
             this.level.timings.countNaturalMobs.stopTiming(); // Paper - timings
 
             this.lastSpawnState = spawnercreature_d;
diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java
index fc05217186d0af6cb758a189f4287ac812625cd4..ef03173f9399c6047985e4ed85ce6ef480d75cdf 100644
--- a/src/main/java/net/minecraft/server/level/ServerPlayer.java
+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java
@@ -235,6 +235,11 @@ public class ServerPlayer extends Player {
     public boolean queueHealthUpdatePacket = false;
     public net.minecraft.network.protocol.game.ClientboundSetHealthPacket queuedHealthUpdatePacket;
     // Paper end
+    // Paper start - mob spawning rework
+    public static final int MOBCATEGORY_TOTAL_ENUMS = net.minecraft.world.entity.MobCategory.values().length;
+    public final int[] mobCounts = new int[MOBCATEGORY_TOTAL_ENUMS]; // Paper
+    public final com.destroystokyo.paper.util.PooledHashSets.PooledObjectLinkedOpenHashSet<ServerPlayer> cachedSingleMobDistanceMap;
+    // Paper end
 
     // CraftBukkit start
     public String displayName;
@@ -326,6 +331,7 @@ public class ServerPlayer extends Player {
         this.adventure$displayName = net.kyori.adventure.text.Component.text(this.getScoreboardName()); // Paper
         this.bukkitPickUpLoot = 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.
diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
index 2d39b7204a5d3967e2fbbd288eb5ff6bf4e324ce..b031dece541d2765f9488a5ffcb0d339c38ccc9e 100644
--- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java
+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java
@@ -69,6 +69,12 @@ public final class NaturalSpawner {
     private NaturalSpawner() {}
 
     public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper) {
+        // Paper start - add countMobs parameter
+        return createState(spawningChunkCount, entities, chunkSource, densityCapper, false);
+    }
+
+    public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper, boolean countMobs) {
+        // Paper end
         PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator();
         Object2IntOpenHashMap<MobCategory> object2intopenhashmap = new Object2IntOpenHashMap();
         Iterator iterator = entities.iterator();
@@ -103,11 +109,16 @@ public final class NaturalSpawner {
                         spawnercreatureprobabilities.addCharge(entity.blockPosition(), biomesettingsmobs_b.getCharge());
                     }
 
-                    if (entity instanceof Mob) {
+                    if (densityCapper != null && entity instanceof Mob) { // Paper
                         densityCapper.addMob(chunk.getPos(), enumcreaturetype);
                     }
 
                     object2intopenhashmap.addTo(enumcreaturetype, 1);
+                    // Paper start
+                    if (countMobs) {
+                        chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
+                    }
+                    // Paper end
                 });
             }
         }
@@ -142,13 +153,37 @@ public final class NaturalSpawner {
                 continue;
             }
 
-            if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && info.canSpawnForCategory(enumcreaturetype, chunk.getPos(), limit)) {
+            // Paper start - only allow spawns upto the limit per chunk and update count afterwards
+            int currEntityCount = info.mobCategoryCounts.getInt(enumcreaturetype);
+            int k1 = limit * info.getSpawnableChunkCount() / NaturalSpawner.MAGIC_NUMBER;
+            int difference = k1 - currEntityCount;
+
+            if (world.paperConfig().entities.spawning.perPlayerMobSpawns) {
+                int minDiff = Integer.MAX_VALUE;
+                final com.destroystokyo.paper.util.misc.PooledLinkedHashSets.PooledObjectLinkedOpenHashSet<net.minecraft.server.level.ServerPlayer> inRange = world.getChunkSource().chunkMap.playerMobDistanceMap.getObjectsInRange(chunk.getPos());
+                if (inRange != null) {
+                    final Object[] backingSet = inRange.getBackingSet();
+                    for (int k = 0; k < backingSet.length; k++) {
+                        if (!(backingSet[k] instanceof final net.minecraft.server.level.ServerPlayer player)) {
+                            continue;
+                        }
+                        minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear(player, enumcreaturetype), minDiff);
+                    }
+                }
+                difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
+            }
+            if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && difference > 0) {
+                // Paper end
                 // CraftBukkit end
                 Objects.requireNonNull(info);
                 NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn;
 
                 Objects.requireNonNull(info);
-                NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn);
+                // Paper start
+                int spawnCount = NaturalSpawner.spawnCategoryForChunk(enumcreaturetype, world, chunk, spawnercreature_c, info::afterSpawn,
+                    difference, world.paperConfig().entities.spawning.perPlayerMobSpawns ? world.getChunkSource().chunkMap::updatePlayerMobTypeMap : null);
+                info.mobCategoryCounts.mergeInt(enumcreaturetype, spawnCount, Integer::sum);
+                // Paper end
             }
         }
 
@@ -157,11 +192,17 @@ public final class NaturalSpawner {
     }
 
     public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
+        // Paper start - add parameters and int ret type
+        spawnCategoryForChunk(group, world, chunk, checker, runner, Integer.MAX_VALUE, null);
+    }
+    public static int spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer<Entity> trackEntity) {
+        // Paper end - add parameters and int ret type
         BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk);
 
         if (blockposition.getY() >= world.getMinBuildHeight() + 1) {
-            NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner);
+            return NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper
         }
+        return 0; // Paper
     }
 
     @VisibleForDebug
@@ -172,15 +213,21 @@ public final class NaturalSpawner {
         });
     }
 
+    // Paper start - add maxSpawns parameter and return spawned mobs
     public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
+        spawnCategoryForPosition(group, world,chunk, pos, checker, runner, Integer.MAX_VALUE, null);
+    }
+    public static int spawnCategoryForPosition(MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner, int maxSpawns, Consumer<Entity> trackEntity) {
+    // Paper end - add maxSpawns parameter and return spawned mobs
         StructureManager structuremanager = world.structureManager();
         ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator();
         int i = pos.getY();
         BlockState iblockdata = world.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn
+        int j = 0; // Paper - moved up
 
         if (iblockdata != null && !iblockdata.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn
             BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos();
-            int j = 0;
+            //int j = 0; // Paper - moved up
             int k = 0;
 
             while (k < 3) {
@@ -222,14 +269,14 @@ public final class NaturalSpawner {
                                     // Paper start
                                     Boolean doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2);
                                     if (doSpawning == null) {
-                                        return;
+                                        return j; // Paper
                                     }
                                     if (doSpawning && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) {
                                         // Paper end
                                         Mob entityinsentient = NaturalSpawner.getMobForSpawn(world, biomesettingsmobs_c.type);
 
                                         if (entityinsentient == null) {
-                                            return;
+                                            return j; // Paper
                                         }
 
                                         entityinsentient.moveTo(d0, (double) i, d1, world.random.nextFloat() * 360.0F, 0.0F);
@@ -242,10 +289,15 @@ public final class NaturalSpawner {
                                                 ++j;
                                                 ++k1;
                                                 runner.run(entityinsentient, chunk);
+                                                // Paper start
+                                                if (trackEntity != null) {
+                                                    trackEntity.accept(entityinsentient);
+                                                }
+                                                // Paper end
                                             }
                                             // CraftBukkit end
-                                            if (j >= entityinsentient.getMaxSpawnClusterSize()) {
-                                                return;
+                                            if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper
+                                                return j; // Paper
                                             }
 
                                             if (entityinsentient.isMaxGroupSizeReached(k1)) {
@@ -267,6 +319,7 @@ public final class NaturalSpawner {
             }
 
         }
+        return j; // Paper
     }
 
     private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) {
@@ -552,7 +605,7 @@ public final class NaturalSpawner {
             MobCategory enumcreaturetype = entitytypes.getCategory();
 
             this.mobCategoryCounts.addTo(enumcreaturetype, 1);
-            this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype);
+            if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); // Paper
         }
 
         public int getSpawnableChunkCount() {
@@ -568,6 +621,7 @@ public final class NaturalSpawner {
             int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
             // CraftBukkit end
 
+            if (this.localMobCapCalculator == null) return this.mobCategoryCounts.getInt(enumcreaturetype) < i; // Paper
             return this.mobCategoryCounts.getInt(enumcreaturetype) >= i ? false : this.localMobCapCalculator.canSpawn(enumcreaturetype, chunkcoordintpair);
         }
     }