--- a/net/minecraft/world/level/NaturalSpawner.java
+++ b/net/minecraft/world/level/NaturalSpawner.java
@@ -4,6 +4,7 @@
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2IntMaps;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
+import java.util.Iterator;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.function.Consumer;
@@ -42,319 +43,427 @@
 import net.minecraft.world.level.levelgen.structure.Structure;
 import net.minecraft.world.level.levelgen.structure.structures.NetherFortressStructure;
 import net.minecraft.world.level.material.FluidState;
+import net.minecraft.world.level.storage.LevelData;
 import net.minecraft.world.phys.Vec3;
 import org.slf4j.Logger;
+import org.bukkit.craftbukkit.util.CraftSpawnCategory;
+import org.bukkit.entity.SpawnCategory;
+import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
+// CraftBukkit end
 
 public final class NaturalSpawner {
+
     private static final Logger LOGGER = LogUtils.getLogger();
     private static final int MIN_SPAWN_DISTANCE = 24;
     public static final int SPAWN_DISTANCE_CHUNK = 8;
     public static final int SPAWN_DISTANCE_BLOCK = 128;
-    static final int MAGIC_NUMBER = (int)Math.pow(17.0, 2.0);
-    public static final MobCategory[] SPAWNING_CATEGORIES = Stream.of(MobCategory.values())
-        .filter(spawnGroup -> spawnGroup != MobCategory.MISC)
-        .toArray(MobCategory[]::new);
+    static final int MAGIC_NUMBER = (int) Math.pow(17.0D, 2.0D);
+    public static final MobCategory[] SPAWNING_CATEGORIES = (MobCategory[]) Stream.of(MobCategory.values()).filter((enumcreaturetype) -> {
+        return enumcreaturetype != MobCategory.MISC;
+    }).toArray((i) -> {
+        return new MobCategory[i];
+    });
 
-    private NaturalSpawner() {
+    private NaturalSpawner() {}
+
+    public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper) {
+        // Paper start - Optional per player mob spawns
+        return createState(spawningChunkCount, entities, chunkSource, densityCapper, false);
     }
 
-    public static NaturalSpawner.SpawnState createState(
-        int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper
-    ) {
-        PotentialCalculator potentialCalculator = new PotentialCalculator();
-        Object2IntOpenHashMap<MobCategory> object2IntOpenHashMap = new Object2IntOpenHashMap<>();
+    public static NaturalSpawner.SpawnState createState(int spawningChunkCount, Iterable<Entity> entities, NaturalSpawner.ChunkGetter chunkSource, LocalMobCapCalculator densityCapper, boolean countMobs) {
+        // Paper end - Optional per player mob spawns
+        PotentialCalculator spawnercreatureprobabilities = new PotentialCalculator();
+        Object2IntOpenHashMap<MobCategory> object2intopenhashmap = new Object2IntOpenHashMap();
+        Iterator iterator = entities.iterator();
 
-        for (Entity entity : entities) {
-            if (entity instanceof Mob mob && (mob.isPersistenceRequired() || mob.requiresCustomPersistence())) {
-                continue;
+        while (iterator.hasNext()) {
+            Entity entity = (Entity) iterator.next();
+
+            if (entity instanceof Mob entityinsentient) {
+                if (entityinsentient.isPersistenceRequired() || entityinsentient.requiresCustomPersistence()) {
+                    continue;
+                }
             }
 
-            MobCategory mobCategory = entity.getType().getCategory();
-            if (mobCategory != MobCategory.MISC) {
-                BlockPos blockPos = entity.blockPosition();
-                chunkSource.query(ChunkPos.asLong(blockPos), chunk -> {
-                    MobSpawnSettings.MobSpawnCost mobSpawnCost = getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType());
-                    if (mobSpawnCost != null) {
-                        potentialCalculator.addCharge(entity.blockPosition(), mobSpawnCost.charge());
+            MobCategory enumcreaturetype = entity.getType().getCategory();
+
+            if (enumcreaturetype != MobCategory.MISC) {
+                // Paper start - Only count natural spawns
+                if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning &&
+                    !(entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL ||
+                        entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) {
+                    continue;
+                }
+                // Paper end - Only count natural spawns
+                BlockPos blockposition = entity.blockPosition();
+
+                chunkSource.query(ChunkPos.asLong(blockposition), (chunk) -> {
+                    MobSpawnSettings.MobSpawnCost biomesettingsmobs_b = NaturalSpawner.getRoughBiome(blockposition, chunk).getMobSettings().getMobSpawnCost(entity.getType());
+
+                    if (biomesettingsmobs_b != null) {
+                        spawnercreatureprobabilities.addCharge(entity.blockPosition(), biomesettingsmobs_b.charge());
                     }
 
-                    if (entity instanceof Mob) {
-                        densityCapper.addMob(chunk.getPos(), mobCategory);
+                    if (densityCapper != null && entity instanceof Mob) { // Paper - Optional per player mob spawns
+                        densityCapper.addMob(chunk.getPos(), enumcreaturetype);
                     }
 
-                    object2IntOpenHashMap.addTo(mobCategory, 1);
+                    object2intopenhashmap.addTo(enumcreaturetype, 1);
+                    // Paper start - Optional per player mob spawns
+                    if (countMobs) {
+                        chunk.level.getChunkSource().chunkMap.updatePlayerMobTypeMap(entity);
+                    }
+                    // Paper end - Optional per player mob spawns
                 });
             }
         }
 
-        return new NaturalSpawner.SpawnState(spawningChunkCount, object2IntOpenHashMap, potentialCalculator, densityCapper);
+        return new NaturalSpawner.SpawnState(spawningChunkCount, object2intopenhashmap, spawnercreatureprobabilities, densityCapper);
     }
 
     static Biome getRoughBiome(BlockPos pos, ChunkAccess chunk) {
-        return chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value();
+        return (Biome) chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value();
     }
 
-    public static void spawnForChunk(
-        ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnState info, boolean spawnAnimals, boolean spawnMonsters, boolean rareSpawn
-    ) {
+    public static void spawnForChunk(ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnState info, boolean spawnAnimals, boolean spawnMonsters, boolean rareSpawn) {
         world.getProfiler().push("spawner");
+        world.timings.mobSpawn.startTiming(); // Spigot
+        MobCategory[] aenumcreaturetype = NaturalSpawner.SPAWNING_CATEGORIES;
+        int i = aenumcreaturetype.length;
 
-        for (MobCategory mobCategory : SPAWNING_CATEGORIES) {
-            if ((spawnAnimals || !mobCategory.isFriendly())
-                && (spawnMonsters || mobCategory.isFriendly())
-                && (rareSpawn || !mobCategory.isPersistent())
-                && info.canSpawnForCategory(mobCategory, chunk.getPos())) {
-                spawnCategoryForChunk(mobCategory, world, chunk, info::canSpawn, info::afterSpawn);
+        LevelData worlddata = world.getLevelData(); // CraftBukkit - Other mob type spawn tick rate
+
+        for (int j = 0; j < i; ++j) {
+            MobCategory enumcreaturetype = aenumcreaturetype[j];
+            // CraftBukkit start - Use per-world spawn limits
+            boolean spawnThisTick = true;
+            int limit = enumcreaturetype.getMaxInstancesPerChunk();
+            SpawnCategory spawnCategory = CraftSpawnCategory.toBukkit(enumcreaturetype);
+            if (CraftSpawnCategory.isValidForLimits(spawnCategory)) {
+                spawnThisTick = world.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % world.ticksPerSpawnCategory.getLong(spawnCategory) == 0;
+                limit = world.getWorld().getSpawnLimit(spawnCategory);
+            }
+
+            if (!spawnThisTick || limit == 0) {
+                continue;
+            }
+
+            // Paper start - Optional per player mob spawns; 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.maplist.ReferenceList<net.minecraft.server.level.ServerPlayer> inRange =
+                    world.chunkSource.chunkMap.getNearbyPlayers().getPlayers(chunk.getPos(), io.papermc.paper.util.player.NearbyPlayers.NearbyMapType.TICK_VIEW_DISTANCE);
+                if (inRange != null) {
+                    final Object[] backingSet = inRange.getRawData();
+                    for (int k = 0, len = inRange.size(); k < len; k++) {
+                        minDiff = Math.min(limit - world.getChunkSource().chunkMap.getMobCountNear((net.minecraft.server.level.ServerPlayer)backingSet[k], enumcreaturetype), minDiff);
+                    }
+                }
+                difference = (minDiff == Integer.MAX_VALUE) ? 0 : minDiff;
+            }
+            if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rareSpawn || !enumcreaturetype.isPersistent()) && difference > 0) {
+                // Paper end - Optional per player mob spawns
+                // CraftBukkit end
+                Objects.requireNonNull(info);
+                NaturalSpawner.SpawnPredicate spawnercreature_c = info::canSpawn;
+
+                Objects.requireNonNull(info);
+                // Paper start - Optional per player mob spawns
+                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 - Optional per player mob spawns
             }
         }
 
+        world.timings.mobSpawn.stopTiming(); // Spigot
         world.getProfiler().pop();
     }
 
-    public static void spawnCategoryForChunk(
-        MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner
-    ) {
-        BlockPos blockPos = getRandomPosWithin(world, chunk);
-        if (blockPos.getY() >= world.getMinBuildHeight() + 1) {
-            spawnCategoryForPosition(group, world, chunk, blockPos, checker, runner);
+    // Paper start - Add mobcaps commands
+    public static int globalLimitForCategory(final ServerLevel level, final MobCategory category, final int spawnableChunkCount) {
+        final int categoryLimit = level.getWorld().getSpawnLimitUnsafe(CraftSpawnCategory.toBukkit(category));
+        if (categoryLimit < 1) {
+            return categoryLimit;
         }
+        return categoryLimit * spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
+    }
+    // Paper end - Add mobcaps commands
+
+    public static void spawnCategoryForChunk(MobCategory group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) {
+        // Paper start - Optional per player mob spawns
+        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 - Optional per player mob spawns
+        BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk);
+
+        if (blockposition.getY() >= world.getMinBuildHeight() + 1) {
+            return NaturalSpawner.spawnCategoryForPosition(group, world, chunk, blockposition, checker, runner, maxSpawns, trackEntity); // Paper - Optional per player mob spawns
+        }
+        return 0; // Paper - Optional per player mob spawns
     }
 
     @VisibleForDebug
     public static void spawnCategoryForPosition(MobCategory group, ServerLevel world, BlockPos pos) {
-        spawnCategoryForPosition(group, world, world.getChunk(pos), pos, (type, posx, chunk) -> true, (entity, chunk) -> {
+        NaturalSpawner.spawnCategoryForPosition(group, world, world.getChunk(pos), pos, (entitytypes, blockposition1, ichunkaccess) -> {
+            return true;
+        }, (entityinsentient, ichunkaccess) -> {
         });
     }
 
-    public static void spawnCategoryForPosition(
-        MobCategory group, ServerLevel world, ChunkAccess chunk, BlockPos pos, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner
-    ) {
-        StructureManager structureManager = world.structureManager();
-        ChunkGenerator chunkGenerator = world.getChunkSource().getGenerator();
+    // Paper start - Optional per player mob spawns
+    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 - Optional per player mob spawns
+        StructureManager structuremanager = world.structureManager();
+        ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator();
         int i = pos.getY();
-        BlockState blockState = chunk.getBlockState(pos);
-        if (!blockState.isRedstoneConductor(chunk, pos)) {
-            BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
-            int j = 0;
+        BlockState iblockdata = world.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn
+        int j = 0; // Paper - Optional per player mob spawns; moved up
 
-            for (int k = 0; k < 3; k++) {
+        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; // Paper - Optional per player mob spawns; moved up
+            int k = 0;
+
+            while (k < 3) {
                 int l = pos.getX();
-                int m = pos.getZ();
-                int n = 6;
-                MobSpawnSettings.SpawnerData spawnerData = null;
-                SpawnGroupData spawnGroupData = null;
-                int o = Mth.ceil(world.random.nextFloat() * 4.0F);
-                int p = 0;
+                int i1 = pos.getZ();
+                boolean flag = true;
+                MobSpawnSettings.SpawnerData biomesettingsmobs_c = null;
+                SpawnGroupData groupdataentity = null;
+                int j1 = Mth.ceil(world.random.nextFloat() * 4.0F);
+                int k1 = 0;
+                int l1 = 0;
 
-                for (int q = 0; q < o; q++) {
-                    l += world.random.nextInt(6) - world.random.nextInt(6);
-                    m += world.random.nextInt(6) - world.random.nextInt(6);
-                    mutableBlockPos.set(l, i, m);
-                    double d = (double)l + 0.5;
-                    double e = (double)m + 0.5;
-                    Player player = world.getNearestPlayer(d, (double)i, e, -1.0, false);
-                    if (player != null) {
-                        double f = player.distanceToSqr(d, (double)i, e);
-                        if (isRightDistanceToPlayerAndSpawnPoint(world, chunk, mutableBlockPos, f)) {
-                            if (spawnerData == null) {
-                                Optional<MobSpawnSettings.SpawnerData> optional = getRandomSpawnMobAt(
-                                    world, structureManager, chunkGenerator, group, world.random, mutableBlockPos
-                                );
-                                if (optional.isEmpty()) {
-                                    break;
-                                }
+                while (true) {
+                    if (l1 < j1) {
+                        label53:
+                        {
+                            l += world.random.nextInt(6) - world.random.nextInt(6);
+                            i1 += world.random.nextInt(6) - world.random.nextInt(6);
+                            blockposition_mutableblockposition.set(l, i, i1);
+                            double d0 = (double) l + 0.5D;
+                            double d1 = (double) i1 + 0.5D;
+                            Player entityhuman = world.getNearestPlayer(d0, (double) i, d1, -1.0D, false);
 
-                                spawnerData = optional.get();
-                                o = spawnerData.minCount + world.random.nextInt(1 + spawnerData.maxCount - spawnerData.minCount);
-                            }
+                            if (entityhuman != null) {
+                                double d2 = entityhuman.distanceToSqr(d0, (double) i, d1);
 
-                            if (isValidSpawnPostitionForType(world, group, structureManager, chunkGenerator, spawnerData, mutableBlockPos, f)
-                                && checker.test(spawnerData.type, mutableBlockPos, chunk)) {
-                                Mob mob = getMobForSpawn(world, spawnerData.type);
-                                if (mob == null) {
-                                    return;
-                                }
+                                if (world.isLoadedAndInBounds(blockposition_mutableblockposition) && NaturalSpawner.isRightDistanceToPlayerAndSpawnPoint(world, chunk, blockposition_mutableblockposition, d2)) { // Paper - don't load chunks for mob spawn
+                                    if (biomesettingsmobs_c == null) {
+                                        Optional<MobSpawnSettings.SpawnerData> optional = NaturalSpawner.getRandomSpawnMobAt(world, structuremanager, chunkgenerator, group, world.random, blockposition_mutableblockposition);
 
-                                mob.moveTo(d, (double)i, e, world.random.nextFloat() * 360.0F, 0.0F);
-                                if (isValidPositionForMob(world, mob, f)) {
-                                    spawnGroupData = mob.finalizeSpawn(
-                                        world, world.getCurrentDifficultyAt(mob.blockPosition()), MobSpawnType.NATURAL, spawnGroupData
-                                    );
-                                    j++;
-                                    p++;
-                                    world.addFreshEntityWithPassengers(mob);
-                                    runner.run(mob, chunk);
-                                    if (j >= mob.getMaxSpawnClusterSize()) {
-                                        return;
+                                        if (optional.isEmpty()) {
+                                            break label53;
+                                        }
+
+                                        biomesettingsmobs_c = (MobSpawnSettings.SpawnerData) optional.get();
+                                        j1 = biomesettingsmobs_c.minCount + world.random.nextInt(1 + biomesettingsmobs_c.maxCount - biomesettingsmobs_c.minCount);
                                     }
 
-                                    if (mob.isMaxGroupSizeReached(p)) {
-                                        break;
+                                    // Paper start - PreCreatureSpawnEvent
+                                    PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2);
+                                    // Paper start - per player mob count backoff
+                                    if (doSpawning == PreSpawnStatus.ABORT || doSpawning == PreSpawnStatus.CANCELLED) {
+                                        world.getChunkSource().chunkMap.updateFailurePlayerMobTypeMap(blockposition_mutableblockposition.getX() >> 4, blockposition_mutableblockposition.getZ() >> 4, group);
+                                    }
+                                    // Paper end - per player mob count backoff
+                                    if (doSpawning == PreSpawnStatus.ABORT) {
+                                        return j; // Paper - Optional per player mob spawns
+                                    }
+                                    if (doSpawning == PreSpawnStatus.SUCCESS && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) {
+                                        // Paper end - PreCreatureSpawnEvent
+                                        Mob entityinsentient = NaturalSpawner.getMobForSpawn(world, biomesettingsmobs_c.type);
+
+                                        if (entityinsentient == null) {
+                                            return j; // Paper - Optional per player mob spawns
+                                        }
+
+                                        entityinsentient.moveTo(d0, (double) i, d1, world.random.nextFloat() * 360.0F, 0.0F);
+                                        if (NaturalSpawner.isValidPositionForMob(world, entityinsentient, d2)) {
+                                            groupdataentity = entityinsentient.finalizeSpawn(world, world.getCurrentDifficultyAt(entityinsentient.blockPosition()), MobSpawnType.NATURAL, groupdataentity);
+                                            // CraftBukkit start
+                                            // SPIGOT-7045: Give ocelot babies back their special spawn reason. Note: This is the only modification required as ocelots count as monsters which means they only spawn during normal chunk ticking and do not spawn during chunk generation as starter mobs.
+                                            world.addFreshEntityWithPassengers(entityinsentient, (entityinsentient instanceof net.minecraft.world.entity.animal.Ocelot && !((org.bukkit.entity.Ageable) entityinsentient.getBukkitEntity()).isAdult()) ? SpawnReason.OCELOT_BABY : SpawnReason.NATURAL);
+                                            if (!entityinsentient.isRemoved()) {
+                                                ++j;
+                                                ++k1;
+                                                runner.run(entityinsentient, chunk);
+                                                // Paper start - Optional per player mob spawns
+                                                if (trackEntity != null) {
+                                                    trackEntity.accept(entityinsentient);
+                                                }
+                                                // Paper end - Optional per player mob spawns
+                                            }
+                                            // CraftBukkit end
+                                            if (j >= entityinsentient.getMaxSpawnClusterSize() || j >= maxSpawns) { // Paper - Optional per player mob spawns
+                                                return j; // Paper - Optional per player mob spawns
+                                            }
+
+                                            if (entityinsentient.isMaxGroupSizeReached(k1)) {
+                                                break label53;
+                                            }
+                                        }
                                     }
                                 }
                             }
+
+                            ++l1;
+                            continue;
                         }
                     }
+
+                    ++k;
+                    break;
                 }
             }
+
         }
+        return j; // Paper - Optional per player mob spawns
     }
 
     private static boolean isRightDistanceToPlayerAndSpawnPoint(ServerLevel world, ChunkAccess chunk, BlockPos.MutableBlockPos pos, double squaredDistance) {
-        return !(squaredDistance <= 576.0)
-            && !world.getSharedSpawnPos().closerToCenterThan(new Vec3((double)pos.getX() + 0.5, (double)pos.getY(), (double)pos.getZ() + 0.5), 24.0)
-            && (Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isNaturalSpawningAllowed(pos));
+        return squaredDistance <= 576.0D ? false : (world.getSharedSpawnPos().closerToCenterThan(new Vec3((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D), 24.0D) ? false : Objects.equals(new ChunkPos(pos), chunk.getPos()) || world.isNaturalSpawningAllowed((BlockPos) pos));
     }
 
-    private static boolean isValidSpawnPostitionForType(
-        ServerLevel world,
-        MobCategory group,
-        StructureManager structureAccessor,
-        ChunkGenerator chunkGenerator,
-        MobSpawnSettings.SpawnerData spawnEntry,
-        BlockPos.MutableBlockPos pos,
-        double squaredDistance
-    ) {
-        EntityType<?> entityType = spawnEntry.type;
-        return entityType.getCategory() != MobCategory.MISC
-            && (
-                entityType.canSpawnFarFromPlayer()
-                    || !(squaredDistance > (double)(entityType.getCategory().getDespawnDistance() * entityType.getCategory().getDespawnDistance()))
-            )
-            && entityType.canSummon()
-            && canSpawnMobAt(world, structureAccessor, chunkGenerator, group, spawnEntry, pos)
-            && SpawnPlacements.isSpawnPositionOk(entityType, world, pos)
-            && SpawnPlacements.checkSpawnRules(entityType, world, MobSpawnType.NATURAL, pos, world.random)
-            && world.noCollision(entityType.getSpawnAABB((double)pos.getX() + 0.5, (double)pos.getY(), (double)pos.getZ() + 0.5));
+    // Paper start - PreCreatureSpawnEvent
+    private enum PreSpawnStatus {
+        FAIL,
+        SUCCESS,
+        CANCELLED,
+        ABORT
+    }
+    private static PreSpawnStatus isValidSpawnPostitionForType(ServerLevel world, MobCategory group, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobSpawnSettings.SpawnerData spawnEntry, BlockPos.MutableBlockPos pos, double squaredDistance) {
+        // Paper end - PreCreatureSpawnEvent
+        EntityType<?> entitytypes = spawnEntry.type;
+
+        // Paper start - PreCreatureSpawnEvent
+        com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent(
+            io.papermc.paper.util.MCUtil.toLocation(world, pos),
+            org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entitytypes), SpawnReason.NATURAL
+        );
+        if (!event.callEvent()) {
+            if (event.shouldAbortSpawn()) {
+                return PreSpawnStatus.ABORT;
+            }
+            return PreSpawnStatus.CANCELLED;
+        }
+        // Paper end - PreCreatureSpawnEvent
+
+        return entitytypes.getCategory() == MobCategory.MISC ? PreSpawnStatus.FAIL : (!entitytypes.canSpawnFarFromPlayer() && squaredDistance > (double) (entitytypes.getCategory().getDespawnDistance() * entitytypes.getCategory().getDespawnDistance()) ? PreSpawnStatus.FAIL : (entitytypes.canSummon() && NaturalSpawner.canSpawnMobAt(world, structureAccessor, chunkGenerator, group, spawnEntry, pos) ? (!SpawnPlacements.isSpawnPositionOk(entitytypes, world, pos) ? PreSpawnStatus.FAIL : (!SpawnPlacements.checkSpawnRules(entitytypes, world, MobSpawnType.NATURAL, pos, world.random) ? PreSpawnStatus.FAIL : world.noCollision(entitytypes.getSpawnAABB((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)) ? PreSpawnStatus.SUCCESS : PreSpawnStatus.FAIL)) : PreSpawnStatus.FAIL)); // Paper - PreCreatureSpawnEvent
     }
 
     @Nullable
     private static Mob getMobForSpawn(ServerLevel world, EntityType<?> type) {
         try {
-            Entity var3 = type.create(world);
-            if (var3 instanceof Mob) {
-                return (Mob)var3;
+            Entity entity = type.create(world);
+
+            if (entity instanceof Mob entityinsentient) {
+                return entityinsentient;
             }
 
-            LOGGER.warn("Can't spawn entity of type: {}", BuiltInRegistries.ENTITY_TYPE.getKey(type));
-        } catch (Exception var4) {
-            LOGGER.warn("Failed to create mob", (Throwable)var4);
+            NaturalSpawner.LOGGER.warn("Can't spawn entity of type: {}", BuiltInRegistries.ENTITY_TYPE.getKey(type));
+        } catch (Exception exception) {
+            NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
+            com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
         }
 
         return null;
     }
 
     private static boolean isValidPositionForMob(ServerLevel world, Mob entity, double squaredDistance) {
-        return (
-                !(squaredDistance > (double)(entity.getType().getCategory().getDespawnDistance() * entity.getType().getCategory().getDespawnDistance()))
-                    || !entity.removeWhenFarAway(squaredDistance)
-            )
-            && entity.checkSpawnRules(world, MobSpawnType.NATURAL)
-            && entity.checkSpawnObstruction(world);
+        return squaredDistance > (double) (entity.getType().getCategory().getDespawnDistance() * entity.getType().getCategory().getDespawnDistance()) && entity.removeWhenFarAway(squaredDistance) ? false : entity.checkSpawnRules(world, MobSpawnType.NATURAL) && entity.checkSpawnObstruction(world);
     }
 
-    private static Optional<MobSpawnSettings.SpawnerData> getRandomSpawnMobAt(
-        ServerLevel world, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobCategory spawnGroup, RandomSource random, BlockPos pos
-    ) {
+    private static Optional<MobSpawnSettings.SpawnerData> getRandomSpawnMobAt(ServerLevel world, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobCategory spawnGroup, RandomSource random, BlockPos pos) {
         Holder<Biome> holder = world.getBiome(pos);
-        return spawnGroup == MobCategory.WATER_AMBIENT && holder.is(BiomeTags.REDUCED_WATER_AMBIENT_SPAWNS) && random.nextFloat() < 0.98F
-            ? Optional.empty()
-            : mobsAt(world, structureAccessor, chunkGenerator, spawnGroup, pos, holder).getRandom(random);
+
+        return spawnGroup == MobCategory.WATER_AMBIENT && holder.is(BiomeTags.REDUCED_WATER_AMBIENT_SPAWNS) && random.nextFloat() < 0.98F ? Optional.empty() : NaturalSpawner.mobsAt(world, structureAccessor, chunkGenerator, spawnGroup, pos, holder).getRandom(random);
     }
 
-    private static boolean canSpawnMobAt(
-        ServerLevel world,
-        StructureManager structureAccessor,
-        ChunkGenerator chunkGenerator,
-        MobCategory spawnGroup,
-        MobSpawnSettings.SpawnerData spawnEntry,
-        BlockPos pos
-    ) {
-        return mobsAt(world, structureAccessor, chunkGenerator, spawnGroup, pos, null).unwrap().contains(spawnEntry);
+    private static boolean canSpawnMobAt(ServerLevel world, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobCategory spawnGroup, MobSpawnSettings.SpawnerData spawnEntry, BlockPos pos) {
+        return NaturalSpawner.mobsAt(world, structureAccessor, chunkGenerator, spawnGroup, pos, (Holder) null).unwrap().contains(spawnEntry);
     }
 
-    private static WeightedRandomList<MobSpawnSettings.SpawnerData> mobsAt(
-        ServerLevel world,
-        StructureManager structureAccessor,
-        ChunkGenerator chunkGenerator,
-        MobCategory spawnGroup,
-        BlockPos pos,
-        @Nullable Holder<Biome> biomeEntry
-    ) {
-        return isInNetherFortressBounds(pos, world, spawnGroup, structureAccessor)
-            ? NetherFortressStructure.FORTRESS_ENEMIES
-            : chunkGenerator.getMobsAt(biomeEntry != null ? biomeEntry : world.getBiome(pos), structureAccessor, spawnGroup, pos);
+    private static WeightedRandomList<MobSpawnSettings.SpawnerData> mobsAt(ServerLevel world, StructureManager structureAccessor, ChunkGenerator chunkGenerator, MobCategory spawnGroup, BlockPos pos, @Nullable Holder<Biome> biomeEntry) {
+        return NaturalSpawner.isInNetherFortressBounds(pos, world, spawnGroup, structureAccessor) ? NetherFortressStructure.FORTRESS_ENEMIES : chunkGenerator.getMobsAt(biomeEntry != null ? biomeEntry : world.getBiome(pos), structureAccessor, spawnGroup, pos);
     }
 
     public static boolean isInNetherFortressBounds(BlockPos pos, ServerLevel world, MobCategory spawnGroup, StructureManager structureAccessor) {
         if (spawnGroup == MobCategory.MONSTER && world.getBlockState(pos.below()).is(Blocks.NETHER_BRICKS)) {
-            Structure structure = structureAccessor.registryAccess().registryOrThrow(Registries.STRUCTURE).get(BuiltinStructures.FORTRESS);
-            return structure != null && structureAccessor.getStructureAt(pos, structure).isValid();
+            Structure structure = (Structure) structureAccessor.registryAccess().registryOrThrow(Registries.STRUCTURE).get(BuiltinStructures.FORTRESS);
+
+            return structure == null ? false : structureAccessor.getStructureAt(pos, structure).isValid();
         } else {
             return false;
         }
     }
 
     private static BlockPos getRandomPosWithin(Level world, LevelChunk chunk) {
-        ChunkPos chunkPos = chunk.getPos();
-        int i = chunkPos.getMinBlockX() + world.random.nextInt(16);
-        int j = chunkPos.getMinBlockZ() + world.random.nextInt(16);
+        ChunkPos chunkcoordintpair = chunk.getPos();
+        int i = chunkcoordintpair.getMinBlockX() + world.random.nextInt(16);
+        int j = chunkcoordintpair.getMinBlockZ() + world.random.nextInt(16);
         int k = chunk.getHeight(Heightmap.Types.WORLD_SURFACE, i, j) + 1;
         int l = Mth.randomBetweenInclusive(world.random, world.getMinBuildHeight(), k);
+
         return new BlockPos(i, l, j);
     }
 
     public static boolean isValidEmptySpawnBlock(BlockGetter blockView, BlockPos pos, BlockState state, FluidState fluidState, EntityType<?> entityType) {
-        return !state.isCollisionShapeFullBlock(blockView, pos)
-            && !state.isSignalSource()
-            && fluidState.isEmpty()
-            && !state.is(BlockTags.PREVENT_MOB_SPAWNING_INSIDE)
-            && !entityType.isBlockDangerous(state);
+        return state.isCollisionShapeFullBlock(blockView, pos) ? false : (state.isSignalSource() ? false : (!fluidState.isEmpty() ? false : (state.is(BlockTags.PREVENT_MOB_SPAWNING_INSIDE) ? false : !entityType.isBlockDangerous(state))));
     }
 
     public static void spawnMobsForChunkGeneration(ServerLevelAccessor world, Holder<Biome> biomeEntry, ChunkPos chunkPos, RandomSource random) {
-        MobSpawnSettings mobSpawnSettings = biomeEntry.value().getMobSettings();
-        WeightedRandomList<MobSpawnSettings.SpawnerData> weightedRandomList = mobSpawnSettings.getMobs(MobCategory.CREATURE);
-        if (!weightedRandomList.isEmpty()) {
+        MobSpawnSettings biomesettingsmobs = ((Biome) biomeEntry.value()).getMobSettings();
+        WeightedRandomList<MobSpawnSettings.SpawnerData> weightedrandomlist = biomesettingsmobs.getMobs(MobCategory.CREATURE);
+
+        if (!weightedrandomlist.isEmpty()) {
             int i = chunkPos.getMinBlockX();
             int j = chunkPos.getMinBlockZ();
 
-            while (random.nextFloat() < mobSpawnSettings.getCreatureProbability()) {
-                Optional<MobSpawnSettings.SpawnerData> optional = weightedRandomList.getRandom(random);
+            while (random.nextFloat() < biomesettingsmobs.getCreatureProbability()) {
+                Optional<MobSpawnSettings.SpawnerData> optional = weightedrandomlist.getRandom(random);
+
                 if (!optional.isEmpty()) {
-                    MobSpawnSettings.SpawnerData spawnerData = optional.get();
-                    int k = spawnerData.minCount + random.nextInt(1 + spawnerData.maxCount - spawnerData.minCount);
-                    SpawnGroupData spawnGroupData = null;
+                    MobSpawnSettings.SpawnerData biomesettingsmobs_c = (MobSpawnSettings.SpawnerData) optional.get();
+                    int k = biomesettingsmobs_c.minCount + random.nextInt(1 + biomesettingsmobs_c.maxCount - biomesettingsmobs_c.minCount);
+                    SpawnGroupData groupdataentity = null;
                     int l = i + random.nextInt(16);
-                    int m = j + random.nextInt(16);
-                    int n = l;
-                    int o = m;
+                    int i1 = j + random.nextInt(16);
+                    int j1 = l;
+                    int k1 = i1;
 
-                    for (int p = 0; p < k; p++) {
-                        boolean bl = false;
+                    for (int l1 = 0; l1 < k; ++l1) {
+                        boolean flag = false;
 
-                        for (int q = 0; !bl && q < 4; q++) {
-                            BlockPos blockPos = getTopNonCollidingPos(world, spawnerData.type, l, m);
-                            if (spawnerData.type.canSummon() && SpawnPlacements.isSpawnPositionOk(spawnerData.type, world, blockPos)) {
-                                float f = spawnerData.type.getWidth();
-                                double d = Mth.clamp((double)l, (double)i + (double)f, (double)i + 16.0 - (double)f);
-                                double e = Mth.clamp((double)m, (double)j + (double)f, (double)j + 16.0 - (double)f);
-                                if (!world.noCollision(spawnerData.type.getSpawnAABB(d, (double)blockPos.getY(), e))
-                                    || !SpawnPlacements.checkSpawnRules(
-                                        spawnerData.type,
-                                        world,
-                                        MobSpawnType.CHUNK_GENERATION,
-                                        BlockPos.containing(d, (double)blockPos.getY(), e),
-                                        world.getRandom()
-                                    )) {
+                        for (int i2 = 0; !flag && i2 < 4; ++i2) {
+                            BlockPos blockposition = NaturalSpawner.getTopNonCollidingPos(world, biomesettingsmobs_c.type, l, i1);
+
+                            if (biomesettingsmobs_c.type.canSummon() && SpawnPlacements.isSpawnPositionOk(biomesettingsmobs_c.type, world, blockposition)) {
+                                float f = biomesettingsmobs_c.type.getWidth();
+                                double d0 = Mth.clamp((double) l, (double) i + (double) f, (double) i + 16.0D - (double) f);
+                                double d1 = Mth.clamp((double) i1, (double) j + (double) f, (double) j + 16.0D - (double) f);
+
+                                if (!world.noCollision(biomesettingsmobs_c.type.getSpawnAABB(d0, (double) blockposition.getY(), d1)) || !SpawnPlacements.checkSpawnRules(biomesettingsmobs_c.type, world, MobSpawnType.CHUNK_GENERATION, BlockPos.containing(d0, (double) blockposition.getY(), d1), world.getRandom())) {
                                     continue;
                                 }
 
                                 Entity entity;
+
                                 try {
-                                    entity = spawnerData.type.create(world.getLevel());
-                                } catch (Exception var27) {
-                                    LOGGER.warn("Failed to create mob", (Throwable)var27);
+                                    entity = biomesettingsmobs_c.type.create(world.getLevel());
+                                } catch (Exception exception) {
+                                    NaturalSpawner.LOGGER.warn("Failed to create mob", exception);
+                                    com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent
                                     continue;
                                 }
 
@@ -362,63 +471,56 @@
                                     continue;
                                 }
 
-                                entity.moveTo(d, (double)blockPos.getY(), e, random.nextFloat() * 360.0F, 0.0F);
-                                if (entity instanceof Mob mob && mob.checkSpawnRules(world, MobSpawnType.CHUNK_GENERATION) && mob.checkSpawnObstruction(world)) {
-                                    spawnGroupData = mob.finalizeSpawn(
-                                        world, world.getCurrentDifficultyAt(mob.blockPosition()), MobSpawnType.CHUNK_GENERATION, spawnGroupData
-                                    );
-                                    world.addFreshEntityWithPassengers(mob);
-                                    bl = true;
+                                entity.moveTo(d0, (double) blockposition.getY(), d1, random.nextFloat() * 360.0F, 0.0F);
+                                if (entity instanceof Mob) {
+                                    Mob entityinsentient = (Mob) entity;
+
+                                    if (entityinsentient.checkSpawnRules(world, MobSpawnType.CHUNK_GENERATION) && entityinsentient.checkSpawnObstruction(world)) {
+                                        groupdataentity = entityinsentient.finalizeSpawn(world, world.getCurrentDifficultyAt(entityinsentient.blockPosition()), MobSpawnType.CHUNK_GENERATION, groupdataentity);
+                                        world.addFreshEntityWithPassengers(entityinsentient, SpawnReason.CHUNK_GEN); // CraftBukkit
+                                        flag = true;
+                                    }
                                 }
                             }
 
                             l += random.nextInt(5) - random.nextInt(5);
 
-                            for (m += random.nextInt(5) - random.nextInt(5);
-                                l < i || l >= i + 16 || m < j || m >= j + 16;
-                                m = o + random.nextInt(5) - random.nextInt(5)
-                            ) {
-                                l = n + random.nextInt(5) - random.nextInt(5);
+                            for (i1 += random.nextInt(5) - random.nextInt(5); l < i || l >= i + 16 || i1 < j || i1 >= j + 16; i1 = k1 + random.nextInt(5) - random.nextInt(5)) {
+                                l = j1 + random.nextInt(5) - random.nextInt(5);
                             }
                         }
                     }
                 }
             }
+
         }
     }
 
     private static BlockPos getTopNonCollidingPos(LevelReader world, EntityType<?> entityType, int x, int z) {
-        int i = world.getHeight(SpawnPlacements.getHeightmapType(entityType), x, z);
-        BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(x, i, z);
+        int k = world.getHeight(SpawnPlacements.getHeightmapType(entityType), x, z);
+        BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(x, k, z);
+
         if (world.dimensionType().hasCeiling()) {
             do {
-                mutableBlockPos.move(Direction.DOWN);
-            } while (!world.getBlockState(mutableBlockPos).isAir());
+                blockposition_mutableblockposition.move(Direction.DOWN);
+            } while (!world.getBlockState(blockposition_mutableblockposition).isAir());
 
             do {
-                mutableBlockPos.move(Direction.DOWN);
-            } while (world.getBlockState(mutableBlockPos).isAir() && mutableBlockPos.getY() > world.getMinBuildHeight());
+                blockposition_mutableblockposition.move(Direction.DOWN);
+            } while (world.getBlockState(blockposition_mutableblockposition).isAir() && blockposition_mutableblockposition.getY() > world.getMinBuildHeight());
         }
 
-        return SpawnPlacements.getPlacementType(entityType).adjustSpawnPosition(world, mutableBlockPos.immutable());
-    }
-
-    @FunctionalInterface
-    public interface AfterSpawnCallback {
-        void run(Mob entity, ChunkAccess chunk);
+        return SpawnPlacements.getPlacementType(entityType).adjustSpawnPosition(world, blockposition_mutableblockposition.immutable());
     }
 
     @FunctionalInterface
     public interface ChunkGetter {
-        void query(long pos, Consumer<LevelChunk> chunkConsumer);
-    }
 
-    @FunctionalInterface
-    public interface SpawnPredicate {
-        boolean test(EntityType<?> type, BlockPos pos, ChunkAccess chunk);
+        void query(long pos, Consumer<LevelChunk> chunkConsumer);
     }
 
     public static class SpawnState {
+
         private final int spawnableChunkCount;
         private final Object2IntOpenHashMap<MobCategory> mobCategoryCounts;
         private final PotentialCalculator spawnPotential;
@@ -430,9 +532,7 @@
         private EntityType<?> lastCheckedType;
         private double lastCharge;
 
-        SpawnState(
-            int spawningChunkCount, Object2IntOpenHashMap<MobCategory> groupToCount, PotentialCalculator densityField, LocalMobCapCalculator densityCapper
-        ) {
+        SpawnState(int spawningChunkCount, Object2IntOpenHashMap<MobCategory> groupToCount, PotentialCalculator densityField, LocalMobCapCalculator densityCapper) {
             this.spawnableChunkCount = spawningChunkCount;
             this.mobCategoryCounts = groupToCount;
             this.spawnPotential = densityField;
@@ -443,37 +543,43 @@
         private boolean canSpawn(EntityType<?> type, BlockPos pos, ChunkAccess chunk) {
             this.lastCheckedPos = pos;
             this.lastCheckedType = type;
-            MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(pos, chunk).getMobSettings().getMobSpawnCost(type);
-            if (mobSpawnCost == null) {
-                this.lastCharge = 0.0;
+            MobSpawnSettings.MobSpawnCost biomesettingsmobs_b = NaturalSpawner.getRoughBiome(pos, chunk).getMobSettings().getMobSpawnCost(type);
+
+            if (biomesettingsmobs_b == null) {
+                this.lastCharge = 0.0D;
                 return true;
             } else {
-                double d = mobSpawnCost.charge();
-                this.lastCharge = d;
-                double e = this.spawnPotential.getPotentialEnergyChange(pos, d);
-                return e <= mobSpawnCost.energyBudget();
+                double d0 = biomesettingsmobs_b.charge();
+
+                this.lastCharge = d0;
+                double d1 = this.spawnPotential.getPotentialEnergyChange(pos, d0);
+
+                return d1 <= biomesettingsmobs_b.energyBudget();
             }
         }
 
         private void afterSpawn(Mob entity, ChunkAccess chunk) {
-            EntityType<?> entityType = entity.getType();
-            BlockPos blockPos = entity.blockPosition();
-            double d;
-            if (blockPos.equals(this.lastCheckedPos) && entityType == this.lastCheckedType) {
-                d = this.lastCharge;
+            EntityType<?> entitytypes = entity.getType();
+            BlockPos blockposition = entity.blockPosition();
+            double d0;
+
+            if (blockposition.equals(this.lastCheckedPos) && entitytypes == this.lastCheckedType) {
+                d0 = this.lastCharge;
             } else {
-                MobSpawnSettings.MobSpawnCost mobSpawnCost = NaturalSpawner.getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entityType);
-                if (mobSpawnCost != null) {
-                    d = mobSpawnCost.charge();
+                MobSpawnSettings.MobSpawnCost biomesettingsmobs_b = NaturalSpawner.getRoughBiome(blockposition, chunk).getMobSettings().getMobSpawnCost(entitytypes);
+
+                if (biomesettingsmobs_b != null) {
+                    d0 = biomesettingsmobs_b.charge();
                 } else {
-                    d = 0.0;
+                    d0 = 0.0D;
                 }
             }
 
-            this.spawnPotential.addCharge(blockPos, d);
-            MobCategory mobCategory = entityType.getCategory();
-            this.mobCategoryCounts.addTo(mobCategory, 1);
-            this.localMobCapCalculator.addMob(new ChunkPos(blockPos), mobCategory);
+            this.spawnPotential.addCharge(blockposition, d0);
+            MobCategory enumcreaturetype = entitytypes.getCategory();
+
+            this.mobCategoryCounts.addTo(enumcreaturetype, 1);
+            if (this.localMobCapCalculator != null) this.localMobCapCalculator.addMob(new ChunkPos(blockposition), enumcreaturetype); // Paper - Optional per player mob spawns
         }
 
         public int getSpawnableChunkCount() {
@@ -484,9 +590,25 @@
             return this.unmodifiableMobCategoryCounts;
         }
 
-        boolean canSpawnForCategory(MobCategory group, ChunkPos chunkPos) {
-            int i = group.getMaxInstancesPerChunk() * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
-            return this.mobCategoryCounts.getInt(group) < i && this.localMobCapCalculator.canSpawn(group, chunkPos);
+        // CraftBukkit start
+        boolean canSpawnForCategory(MobCategory enumcreaturetype, ChunkPos chunkcoordintpair, int limit) {
+            int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER;
+            // CraftBukkit end
+
+            if (this.localMobCapCalculator == null) return this.mobCategoryCounts.getInt(enumcreaturetype) < i; // Paper - Optional per player mob spawns
+            return this.mobCategoryCounts.getInt(enumcreaturetype) >= i ? false : this.localMobCapCalculator.canSpawn(enumcreaturetype, chunkcoordintpair);
         }
     }
+
+    @FunctionalInterface
+    public interface SpawnPredicate {
+
+        boolean test(EntityType<?> type, BlockPos pos, ChunkAccess chunk);
+    }
+
+    @FunctionalInterface
+    public interface AfterSpawnCallback {
+
+        void run(Mob entity, ChunkAccess chunk);
+    }
 }
