--- a/net/minecraft/world/level/NaturalSpawner.java +++ b/net/minecraft/world/level/NaturalSpawner.java @@ -49,6 +_,13 @@ import net.minecraft.world.phys.Vec3; import org.slf4j.Logger; +// CraftBukkit start +import net.minecraft.world.level.storage.LevelData; +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; @@ -72,6 +_,13 @@ if (!(entity instanceof Mob mob && (mob.isPersistenceRequired() || mob.requiresCustomPersistence()))) { MobCategory category = entity.getType().getCategory(); if (category != 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 blockPos = entity.blockPosition(); chunkGetter.query(ChunkPos.asLong(blockPos), chunk -> { MobSpawnSettings.MobSpawnCost mobSpawnCost = getRoughBiome(blockPos, chunk).getMobSettings().getMobSpawnCost(entity.getType()); @@ -96,17 +_,37 @@ return chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value(); } + // CraftBukkit start - add server public static List getFilteredSpawningCategories( - NaturalSpawner.SpawnState spawnState, boolean spawnFriendlies, boolean spawnEnemies, boolean spawnPassives + NaturalSpawner.SpawnState spawnState, boolean spawnFriendlies, boolean spawnEnemies, boolean spawnPassives, ServerLevel worldserver ) { + LevelData worlddata = worldserver.getLevelData(); // CraftBukkit - Other mob type spawn tick rate + // CraftBukkit end List list = new ArrayList<>(SPAWNING_CATEGORIES.length); - - for (MobCategory mobCategory : SPAWNING_CATEGORIES) { - if ((spawnFriendlies || !mobCategory.isFriendly()) - && (spawnEnemies || mobCategory.isFriendly()) - && (spawnPassives || !mobCategory.isPersistent()) - && spawnState.canSpawnForCategoryGlobal(mobCategory)) { - list.add(mobCategory); + MobCategory[] aenumcreaturetype = NaturalSpawner.SPAWNING_CATEGORIES; + int i = aenumcreaturetype.length; + + for (int j = 0; j < i; ++j) { + MobCategory enumcreaturetype = SPAWNING_CATEGORIES[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 = worldserver.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % worldserver.ticksPerSpawnCategory.getLong(spawnCategory) == 0; + limit = worldserver.getWorld().getSpawnLimit(spawnCategory); + } + + if (!spawnThisTick || limit == 0) { + continue; + } + + if ((spawnFriendlies || !enumcreaturetype.isFriendly()) + && (spawnEnemies || enumcreaturetype.isFriendly()) + && (spawnPassives || !enumcreaturetype.isPersistent()) + && spawnState.canSpawnForCategoryGlobal(enumcreaturetype, limit)) { // Paper - Optional per player mob spawns; remove global check, check later during the local one + list.add(enumcreaturetype); + // CraftBukkit end } } @@ -126,6 +_,16 @@ profilerFiller.pop(); } + // 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 category, ServerLevel level, LevelChunk chunk, NaturalSpawner.SpawnPredicate filter, NaturalSpawner.AfterSpawnCallback callback ) { @@ -151,8 +_,8 @@ StructureManager structureManager = level.structureManager(); ChunkGenerator generator = level.getChunkSource().getGenerator(); int y = pos.getY(); - BlockState blockState = chunk.getBlockState(pos); - if (!blockState.isRedstoneConductor(chunk, pos)) { + BlockState blockState = level.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn + if (blockState != null && !blockState.isRedstoneConductor(chunk, pos)) { // Paper - don't load chunks for mob spawn BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); int i = 0; @@ -174,7 +_,7 @@ Player nearestPlayer = level.getNearestPlayer(d, y, d1, -1.0, false); if (nearestPlayer != null) { double d2 = nearestPlayer.distanceToSqr(d, y, d1); - if (isRightDistanceToPlayerAndSpawnPoint(level, chunk, mutableBlockPos, d2)) { + if (level.isLoadedAndInBounds(mutableBlockPos) && isRightDistanceToPlayerAndSpawnPoint(level, chunk, mutableBlockPos, d2)) { // Paper - don't load chunks for mob spawn if (spawnerData == null) { Optional randomSpawnMobAt = getRandomSpawnMobAt( level, structureManager, generator, category, level.random, mutableBlockPos @@ -187,8 +_,13 @@ ceil = spawnerData.minCount + level.random.nextInt(1 + spawnerData.maxCount - spawnerData.minCount); } - if (isValidSpawnPostitionForType(level, category, structureManager, generator, spawnerData, mutableBlockPos, d2) - && filter.test(spawnerData.type, mutableBlockPos, chunk)) { + // Paper start - PreCreatureSpawnEvent + PreSpawnStatus doSpawning = isValidSpawnPostitionForType(level, category, structureManager, generator, spawnerData, mutableBlockPos, d2); + if (doSpawning == PreSpawnStatus.ABORT) { + return; + } + if (doSpawning == PreSpawnStatus.SUCCESS && filter.test(spawnerData.type, mutableBlockPos, chunk)) { + // Paper end - PreCreatureSpawnEvent Mob mobForSpawn = getMobForSpawn(level, spawnerData.type); if (mobForSpawn == null) { return; @@ -199,10 +_,15 @@ spawnGroupData = mobForSpawn.finalizeSpawn( level, level.getCurrentDifficultyAt(mobForSpawn.blockPosition()), EntitySpawnReason.NATURAL, spawnGroupData ); - i++; - i3++; - level.addFreshEntityWithPassengers(mobForSpawn); - callback.run(mobForSpawn, chunk); + // 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. + level.addFreshEntityWithPassengers(mobForSpawn, (mobForSpawn instanceof net.minecraft.world.entity.animal.Ocelot && !((org.bukkit.entity.Ageable) mobForSpawn.getBukkitEntity()).isAdult()) ? SpawnReason.OCELOT_BABY : SpawnReason.NATURAL); + if (!mobForSpawn.isRemoved()) { + ++i; + ++i3; + callback.run(mobForSpawn, chunk); + } + // CraftBukkit end if (i >= mobForSpawn.getMaxSpawnClusterSize()) { return; } @@ -225,7 +_,15 @@ && (Objects.equals(new ChunkPos(pos), chunk.getPos()) || level.isNaturalSpawningAllowed(pos)); } - private static boolean isValidSpawnPostitionForType( + // Paper start - PreCreatureSpawnEvent + private enum PreSpawnStatus { + FAIL, + SUCCESS, + CANCELLED, + ABORT + } + private static PreSpawnStatus isValidSpawnPostitionForType( + // Paper end - PreCreatureSpawnEvent ServerLevel level, MobCategory category, StructureManager structureManager, @@ -235,16 +_,20 @@ double distance ) { EntityType entityType = data.type; - return entityType.getCategory() != MobCategory.MISC - && ( - entityType.canSpawnFarFromPlayer() - || !(distance > entityType.getCategory().getDespawnDistance() * entityType.getCategory().getDespawnDistance()) - ) - && entityType.canSummon() - && canSpawnMobAt(level, structureManager, generator, category, data, pos) - && SpawnPlacements.isSpawnPositionOk(entityType, level, pos) - && SpawnPlacements.checkSpawnRules(entityType, level, EntitySpawnReason.NATURAL, pos, level.random) - && level.noCollision(entityType.getSpawnAABB(pos.getX() + 0.5, pos.getY(), pos.getZ() + 0.5)); + + // Paper start - PreCreatureSpawnEvent + com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent event = new com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent( + io.papermc.paper.util.MCUtil.toLocation(level, pos), + org.bukkit.craftbukkit.entity.CraftEntityType.minecraftToBukkit(entityType), SpawnReason.NATURAL + ); + if (!event.callEvent()) { + if (event.shouldAbortSpawn()) { + return PreSpawnStatus.ABORT; + } + return PreSpawnStatus.CANCELLED; + } + // Paper end - PreCreatureSpawnEvent + return entityType.getCategory() == MobCategory.MISC ? PreSpawnStatus.FAIL : (!entityType.canSpawnFarFromPlayer() && distance > (double) (entityType.getCategory().getDespawnDistance() * entityType.getCategory().getDespawnDistance()) ? PreSpawnStatus.FAIL : (entityType.canSummon() && NaturalSpawner.canSpawnMobAt(level, structureManager, generator, category, data, pos) ? (!SpawnPlacements.isSpawnPositionOk(entityType, level, pos) ? PreSpawnStatus.FAIL : (!SpawnPlacements.checkSpawnRules(entityType, level, EntitySpawnReason.NATURAL, pos, level.random) ? PreSpawnStatus.FAIL : level.noCollision(entityType.getSpawnAABB((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)) ? PreSpawnStatus.SUCCESS : PreSpawnStatus.FAIL)) : PreSpawnStatus.FAIL)); // Paper - PreCreatureSpawnEvent } @Nullable @@ -258,6 +_,7 @@ LOGGER.warn("Can't spawn entity of type: {}", BuiltInRegistries.ENTITY_TYPE.getKey(entityType)); } catch (Exception var4) { LOGGER.warn("Failed to create mob", (Throwable)var4); + com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(var4); // Paper - ServerExceptionEvent } return null; @@ -364,6 +_,7 @@ entity = spawnerData.type.create(levelAccessor.getLevel(), EntitySpawnReason.NATURAL); } catch (Exception var27) { LOGGER.warn("Failed to create mob", (Throwable)var27); + com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(var27); // Paper - ServerExceptionEvent continue; } @@ -381,7 +_,7 @@ EntitySpawnReason.CHUNK_GENERATION, spawnGroupData ); - levelAccessor.addFreshEntityWithPassengers(mob); + levelAccessor.addFreshEntityWithPassengers(mob, SpawnReason.CHUNK_GEN); // CraftBukkit flag = true; } } @@ -501,8 +_,10 @@ return this.unmodifiableMobCategoryCounts; } - boolean canSpawnForCategoryGlobal(MobCategory category) { - int i = category.getMaxInstancesPerChunk() * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER; + // CraftBukkit start + boolean canSpawnForCategoryGlobal(MobCategory category, int limit) { + int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER; + // CraftBukkit end return this.mobCategoryCounts.getInt(category) < i; }