--- a/net/minecraft/world/level/NaturalSpawner.java +++ b/net/minecraft/world/level/NaturalSpawner.java @@ -47,8 +47,13 @@ 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 { @@ -82,6 +87,13 @@ 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) -> { @@ -107,15 +119,31 @@ return (Biome) chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value(); } - public static List getFilteredSpawningCategories(NaturalSpawner.SpawnState info, boolean spawnAnimals, boolean spawnMonsters, boolean rare) { + // CraftBukkit start - add server + public static List getFilteredSpawningCategories(NaturalSpawner.SpawnState spawnercreature_d, boolean flag, boolean flag1, boolean flag2, ServerLevel worldserver) { + LevelData worlddata = worldserver.getLevelData(); // CraftBukkit - Other mob type spawn tick rate + // CraftBukkit end List list = new ArrayList(NaturalSpawner.SPAWNING_CATEGORIES.length); MobCategory[] aenumcreaturetype = NaturalSpawner.SPAWNING_CATEGORIES; int i = aenumcreaturetype.length; 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 = worldserver.ticksPerSpawnCategory.getLong(spawnCategory) != 0 && worlddata.getGameTime() % worldserver.ticksPerSpawnCategory.getLong(spawnCategory) == 0; + limit = worldserver.getWorld().getSpawnLimit(spawnCategory); + } - if ((spawnAnimals || !enumcreaturetype.isFriendly()) && (spawnMonsters || enumcreaturetype.isFriendly()) && (rare || !enumcreaturetype.isPersistent()) && info.canSpawnForCategoryGlobal(enumcreaturetype)) { + if (!spawnThisTick || limit == 0) { + continue; + } + + if ((flag || !enumcreaturetype.isFriendly()) && (flag1 || enumcreaturetype.isFriendly()) && (flag2 || !enumcreaturetype.isPersistent()) && spawnercreature_d.canSpawnForCategoryGlobal(enumcreaturetype, limit)) { + // CraftBukkit end list.add(enumcreaturetype); } } @@ -144,6 +172,16 @@ gameprofilerfiller.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 group, ServerLevel world, LevelChunk chunk, NaturalSpawner.SpawnPredicate checker, NaturalSpawner.AfterSpawnCallback runner) { BlockPos blockposition = NaturalSpawner.getRandomPosWithin(world, chunk); @@ -164,9 +202,9 @@ StructureManager structuremanager = world.structureManager(); ChunkGenerator chunkgenerator = world.getChunkSource().getGenerator(); int i = pos.getY(); - BlockState iblockdata = chunk.getBlockState(pos); + BlockState iblockdata = world.getBlockStateIfLoadedAndInBounds(pos); // Paper - don't load chunks for mob spawn - if (!iblockdata.isRedstoneConductor(chunk, pos)) { + 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 k = 0; @@ -195,7 +233,7 @@ if (entityhuman != null) { double d2 = entityhuman.distanceToSqr(d0, (double) i, d1); - if (NaturalSpawner.isRightDistanceToPlayerAndSpawnPoint(world, chunk, blockposition_mutableblockposition, d2)) { + 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 optional = NaturalSpawner.getRandomSpawnMobAt(world, structuremanager, chunkgenerator, group, world.random, blockposition_mutableblockposition); @@ -207,7 +245,13 @@ j1 = biomesettingsmobs_c.minCount + world.random.nextInt(1 + biomesettingsmobs_c.maxCount - biomesettingsmobs_c.minCount); } - if (NaturalSpawner.isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2) && checker.test(biomesettingsmobs_c.type, blockposition_mutableblockposition, chunk)) { + // Paper start - PreCreatureSpawnEvent + PreSpawnStatus doSpawning = isValidSpawnPostitionForType(world, group, structuremanager, chunkgenerator, biomesettingsmobs_c, blockposition_mutableblockposition, d2); + if (doSpawning == PreSpawnStatus.ABORT) { + return; + } + 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) { @@ -217,10 +261,15 @@ 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()), EntitySpawnReason.NATURAL, groupdataentity); - ++j; - ++k1; - world.addFreshEntityWithPassengers(entityinsentient); - runner.run(entityinsentient, 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. + 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); + } + // CraftBukkit end if (j >= entityinsentient.getMaxSpawnClusterSize()) { return; } @@ -250,10 +299,31 @@ 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) { + // 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; - return entitytypes.getCategory() == MobCategory.MISC ? false : (!entitytypes.canSpawnFarFromPlayer() && squaredDistance > (double) (entitytypes.getCategory().getDespawnDistance() * entitytypes.getCategory().getDespawnDistance()) ? false : (entitytypes.canSummon() && NaturalSpawner.canSpawnMobAt(world, structureAccessor, chunkGenerator, group, spawnEntry, pos) ? (!SpawnPlacements.isSpawnPositionOk(entitytypes, world, pos) ? false : (!SpawnPlacements.checkSpawnRules(entitytypes, world, EntitySpawnReason.NATURAL, pos, world.random) ? false : world.noCollision(entitytypes.getSpawnAABB((double) pos.getX() + 0.5D, (double) pos.getY(), (double) pos.getZ() + 0.5D)))) : false)); + // 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, EntitySpawnReason.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 @@ -268,6 +338,7 @@ 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; @@ -356,6 +427,7 @@ entity = biomesettingsmobs_c.type.create(world.getLevel(), EntitySpawnReason.NATURAL); } catch (Exception exception) { NaturalSpawner.LOGGER.warn("Failed to create mob", exception); + com.destroystokyo.paper.exception.ServerInternalException.reportInternalException(exception); // Paper - ServerExceptionEvent continue; } @@ -369,7 +441,7 @@ if (entityinsentient.checkSpawnRules(world, EntitySpawnReason.CHUNK_GENERATION) && entityinsentient.checkSpawnObstruction(world)) { groupdataentity = entityinsentient.finalizeSpawn(world, world.getCurrentDifficultyAt(entityinsentient.blockPosition()), EntitySpawnReason.CHUNK_GENERATION, groupdataentity); - world.addFreshEntityWithPassengers(entityinsentient); + world.addFreshEntityWithPassengers(entityinsentient, SpawnReason.CHUNK_GEN); // CraftBukkit flag = true; } } @@ -482,10 +554,12 @@ return this.unmodifiableMobCategoryCounts; } - boolean canSpawnForCategoryGlobal(MobCategory group) { - int i = group.getMaxInstancesPerChunk() * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER; + // CraftBukkit start + boolean canSpawnForCategoryGlobal(MobCategory enumcreaturetype, int limit) { + int i = limit * this.spawnableChunkCount / NaturalSpawner.MAGIC_NUMBER; + // CraftBukkit end - return this.mobCategoryCounts.getInt(group) < i; + return this.mobCategoryCounts.getInt(enumcreaturetype) < i; } boolean canSpawnForCategoryLocal(MobCategory group, ChunkPos chunkPos) {