diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java index 9cff609d2..b6d981a7c 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/WorldGuardEntityListener.java @@ -20,6 +20,7 @@ package com.sk89q.worldguard.bukkit.listener; import com.destroystokyo.paper.event.entity.EntityZapEvent; +import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent; import com.sk89q.worldedit.bukkit.BukkitAdapter; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldguard.LocalPlayer; @@ -657,8 +658,11 @@ public void onCreatureSpawn(CreatureSpawnEvent event) { return; } - EntityType entityType = event.getEntityType(); + handleCreatureSpawn(event, event.getLocation(), event.getEntityType(), event.getSpawnReason(), cfg, wcfg); + } + private static void handleCreatureSpawn(Cancellable event, Location location, EntityType entityType, SpawnReason spawnReason, + ConfigurationManager cfg, WorldConfiguration wcfg) { com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); if (weEntityType != null && wcfg.blockCreatureSpawn.contains(weEntityType)) { @@ -666,11 +670,9 @@ public void onCreatureSpawn(CreatureSpawnEvent event) { return; } - Location eventLoc = event.getLocation(); - if (wcfg.useRegions && cfg.useRegionsCreatureSpawnEvent) { ApplicableRegionSet set = - WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(eventLoc)); + WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(location)); if (!set.testState(null, Flags.MOB_SPAWNING)) { event.setCancelled(true); @@ -685,8 +687,8 @@ public void onCreatureSpawn(CreatureSpawnEvent event) { } if (wcfg.blockGroundSlimes && entityType == EntityType.SLIME - && eventLoc.getY() >= 60 - && event.getSpawnReason() == SpawnReason.NATURAL) { + && location.getY() >= 60 + && spawnReason == SpawnReason.NATURAL) { event.setCancelled(true); return; } @@ -907,6 +909,53 @@ public void onEntityZap(EntityZapEvent event) { handlePigZap(event.getEntity(), event); } } + + /** + * Applies the natural spawn checks from {@link WorldGuardEntityListener#onCreatureSpawn} before the + * server constructs the entity. The CreatureSpawnEvent checks fire at the very + * end of the spawn pipeline, after the position was picked, the placement + * checks ran and the mob was constructed and finalized, so a region that denies + * mob spawning pays for a mob to be built and thrown away on every attempt. + * Cancelling here skips the placement checks, the construction and + * finalizeSpawn for a spawn that was going to be refused anyway. + * + * Cancelling does not end the chunk's remaining attempts. Only + * setShouldAbortSpawn(true) makes the spawner return early; a plain cancel + * falls through to the next candidate position exactly like a failed + * placement check. + * + * Note that Paper fires this event for every candidate position, before the + * light, block and collision checks, so the region query below runs + * considerably more often than the CreatureSpawnEvent one did. On Paper with + * per-player-mob-spawns enabled, which is the default, every cancelled pre + * spawn is also charged to a per player mob backoff counter that is added to + * the mob cap of every player within tick view distance and bleeds off one per + * spawn cycle, so a large denied region can suppress spawning in neighbouring + * chunks that allow mobs. + * + * Only NATURAL spawns are handled here; every other spawn reason keeps going + * through the CreatureSpawnEvent checks unchanged. + */ + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) + public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { + if (event.getReason() != SpawnReason.NATURAL) { + return; + } + + ConfigurationManager cfg = getConfig(); + + if (!cfg.useRegionsPreCreatureSpawnEvent) { + return; + } + + if (cfg.activityHaltToggle) { + event.setCancelled(true); + return; + } + + Location spawnLoc = event.getSpawnLocation(); + handleCreatureSpawn(event, spawnLoc, event.getType(), event.getReason(), cfg, getWorldConfig(spawnLoc.getWorld())); + } } private static class SpigotListener implements Listener { diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/config/ConfigurationManager.java b/worldguard-core/src/main/java/com/sk89q/worldguard/config/ConfigurationManager.java index 5f159df49..6fbbdd620 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/config/ConfigurationManager.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/config/ConfigurationManager.java @@ -65,6 +65,7 @@ public abstract class ConfigurationManager { "#\r\n"; public boolean useRegionsCreatureSpawnEvent; + public boolean useRegionsPreCreatureSpawnEvent; public boolean activityHaltToggle = false; public boolean useGodPermission; public boolean useGodGroup; diff --git a/worldguard-core/src/main/java/com/sk89q/worldguard/config/YamlConfigurationManager.java b/worldguard-core/src/main/java/com/sk89q/worldguard/config/YamlConfigurationManager.java index d9952cb5c..d2df6f87a 100644 --- a/worldguard-core/src/main/java/com/sk89q/worldguard/config/YamlConfigurationManager.java +++ b/worldguard-core/src/main/java/com/sk89q/worldguard/config/YamlConfigurationManager.java @@ -56,6 +56,7 @@ public void load() { migrateRegionsToUuid = config.getBoolean("regions.uuid-migration.perform-on-next-start", true); keepUnresolvedNames = config.getBoolean("regions.uuid-migration.keep-names-that-lack-uuids", true); useRegionsCreatureSpawnEvent = config.getBoolean("regions.use-creature-spawn-event", true); + useRegionsPreCreatureSpawnEvent = config.getBoolean("regions.use-pre-creature-spawn-event", true); disableDefaultBypass = config.getBoolean("regions.disable-bypass-by-default", false); announceBypassStatus = config.getBoolean("regions.announce-bypass-status", false);