From c157bf7491cdb8c5a8089a2d33a4d8af48534577 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Mon, 10 Aug 2026 00:51:29 +0200 Subject: [PATCH 1/7] Cancel blocked natural spawns before the entity is created The mob-spawning and deny-spawn checks in WorldGuardEntityListener fire on CreatureSpawnEvent, at the very end of the spawn pipeline. By that point the server has picked a spawn position, run the placement checks, constructed the mob and run finalizeSpawn, and the cancelled mob is thrown away. Since a cancelled spawn never counts toward the mob cap, the natural spawner keeps retrying the same area at full rate, so regions that deny mob spawning become permanent spawn attempt hotspots that pay entity construction over and over for nothing. On Paper servers the same checks can run in PreCreatureSpawnEvent, before the entity exists. Cancelling there also ends the remaining attempts for the chunk in that spawn cycle, so the wasted work is gone almost entirely. Measured on a flat test world with a region denying mob-spawning over the whole spawn range and the mob cap kept empty, the attempt rate around a single player collapsed from roughly 75000 attempts per second to roughly 60 per second, with no entities constructed at all. The new listener mirrors the natural spawn conditions from onCreatureSpawn exactly: activity halt, block-creature-spawn, mob-spawning and deny-spawn region flags, and block-ground-slimes. All other spawn reasons keep going through the existing CreatureSpawnEvent checks unchanged, and the listener is only registered when the Paper event class is present. --- .../worldguard/bukkit/WorldGuardPlugin.java | 4 + .../listener/PaperPreSpawnListener.java | 110 ++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java index 9229d1db5..c599e11c1 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java @@ -40,6 +40,7 @@ import com.sk89q.worldguard.blacklist.Blacklist; import com.sk89q.worldguard.bukkit.event.player.ProcessPlayerEvent; import com.sk89q.worldguard.bukkit.listener.BlacklistListener; +import com.sk89q.worldguard.bukkit.listener.PaperPreSpawnListener; import com.sk89q.worldguard.bukkit.listener.BlockedPotionsListener; import com.sk89q.worldguard.bukkit.listener.BuildPermissionListener; import com.sk89q.worldguard.bukkit.listener.ChestProtectionListener; @@ -186,6 +187,9 @@ public void accept(Object ignored) { (new WorldGuardPlayerListener(this)).registerEvents(); (new WorldGuardBlockListener(this)).registerEvents(); (new WorldGuardEntityListener(this)).registerEvents(); + if (PaperLib.isPaper()) { + (new PaperPreSpawnListener(this)).registerEvents(); + } (new WorldGuardWeatherListener(this)).registerEvents(); (new WorldGuardVehicleListener(this)).registerEvents(); (new WorldGuardServerListener(this)).registerEvents(); diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java new file mode 100644 index 000000000..3e28a5310 --- /dev/null +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java @@ -0,0 +1,110 @@ +/* + * WorldGuard, a suite of tools for Minecraft + * Copyright (C) sk89q + * Copyright (C) WorldGuard team and contributors + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by the + * Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldguard.bukkit.listener; + +import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent; +import com.sk89q.worldedit.bukkit.BukkitAdapter; +import com.sk89q.worldguard.WorldGuard; +import com.sk89q.worldguard.bukkit.BukkitConfigurationManager; +import com.sk89q.worldguard.bukkit.BukkitWorldConfiguration; +import com.sk89q.worldguard.bukkit.WorldGuardPlugin; +import com.sk89q.worldguard.protection.ApplicableRegionSet; +import com.sk89q.worldguard.protection.flags.Flags; +import org.bukkit.Location; +import org.bukkit.entity.EntityType; +import org.bukkit.event.EventHandler; +import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; + +import java.util.Set; + +/** + * Cancels blocked natural spawns before the server constructs the entity. + * + * The CreatureSpawnEvent checks in {@link WorldGuardEntityListener#onCreatureSpawn} + * fire at the very end of the spawn pipeline: by then the server has picked a spawn + * position, run the placement checks, constructed the mob and run finalizeSpawn, and + * the cancelled mob is thrown away. Since a cancelled spawn never counts toward the + * mob cap, the natural spawner keeps retrying the same area at full rate, so regions + * that deny mob-spawning become permanent spawn attempt hotspots that pay entity + * construction over and over for nothing. + * + * This listener applies the same natural spawn checks in Paper's + * PreCreatureSpawnEvent, before the entity exists. Cancelling at that stage also + * makes the server end the remaining attempts for the chunk in that spawn cycle, so + * the retry pressure disappears too: on a flat test world with a region denying + * mob-spawning over the whole spawn range and the mob cap kept empty, the attempt + * rate around a single player collapsed from roughly 75000 attempts per second to + * roughly 60 per second, with no entities constructed. + * + * Only NATURAL spawns are handled; every other spawn reason keeps going through the + * CreatureSpawnEvent checks unchanged. The listener is only registered when the + * Paper event is available. + */ +public class PaperPreSpawnListener extends AbstractListener { + + public PaperPreSpawnListener(WorldGuardPlugin plugin) { + super(plugin); + } + + @EventHandler(ignoreCancelled = true) + public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { + if (event.getReason() != SpawnReason.NATURAL) { + return; + } + + BukkitConfigurationManager cfg = getConfig(); + + if (cfg.activityHaltToggle) { + event.setCancelled(true); + return; + } + + Location eventLoc = event.getSpawnLocation(); + BukkitWorldConfiguration wcfg = getWorldConfig(eventLoc.getWorld()); + + EntityType entityType = event.getType(); + com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); + + if (weEntityType != null && wcfg.blockCreatureSpawn.contains(weEntityType)) { + event.setCancelled(true); + return; + } + + if (wcfg.useRegions && cfg.useRegionsCreatureSpawnEvent) { + ApplicableRegionSet set = + WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(eventLoc)); + + if (!set.testState(null, Flags.MOB_SPAWNING)) { + event.setCancelled(true); + return; + } + + Set entityTypes = set.queryValue(null, Flags.DENY_SPAWN); + if (entityTypes != null && weEntityType != null && entityTypes.contains(weEntityType)) { + event.setCancelled(true); + return; + } + } + + if (wcfg.blockGroundSlimes && entityType == EntityType.SLIME && eventLoc.getY() >= 60) { + event.setCancelled(true); + } + } +} From f124cdd7daef5a9641c784b40b46a2fae0af33f7 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:20:24 +0200 Subject: [PATCH 2/7] Move the pre spawn handler into the existing Paper inner listener Review feedback: platform specific code lives in the Paper inner classes of the main listeners, and WorldGuardEntityListener already has one for EntityZapEvent, so the handler moves in there and the separate listener class is gone. --- .../worldguard/bukkit/WorldGuardPlugin.java | 4 - .../listener/PaperPreSpawnListener.java | 110 ------------------ .../listener/WorldGuardEntityListener.java | 60 ++++++++++ 3 files changed, 60 insertions(+), 114 deletions(-) delete mode 100644 worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java index c599e11c1..9229d1db5 100644 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java +++ b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/WorldGuardPlugin.java @@ -40,7 +40,6 @@ import com.sk89q.worldguard.blacklist.Blacklist; import com.sk89q.worldguard.bukkit.event.player.ProcessPlayerEvent; import com.sk89q.worldguard.bukkit.listener.BlacklistListener; -import com.sk89q.worldguard.bukkit.listener.PaperPreSpawnListener; import com.sk89q.worldguard.bukkit.listener.BlockedPotionsListener; import com.sk89q.worldguard.bukkit.listener.BuildPermissionListener; import com.sk89q.worldguard.bukkit.listener.ChestProtectionListener; @@ -187,9 +186,6 @@ public void accept(Object ignored) { (new WorldGuardPlayerListener(this)).registerEvents(); (new WorldGuardBlockListener(this)).registerEvents(); (new WorldGuardEntityListener(this)).registerEvents(); - if (PaperLib.isPaper()) { - (new PaperPreSpawnListener(this)).registerEvents(); - } (new WorldGuardWeatherListener(this)).registerEvents(); (new WorldGuardVehicleListener(this)).registerEvents(); (new WorldGuardServerListener(this)).registerEvents(); diff --git a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java b/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java deleted file mode 100644 index 3e28a5310..000000000 --- a/worldguard-bukkit/src/main/java/com/sk89q/worldguard/bukkit/listener/PaperPreSpawnListener.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * WorldGuard, a suite of tools for Minecraft - * Copyright (C) sk89q - * Copyright (C) WorldGuard team and contributors - * - * This program is free software: you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as published by the - * Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or - * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License - * for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldguard.bukkit.listener; - -import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent; -import com.sk89q.worldedit.bukkit.BukkitAdapter; -import com.sk89q.worldguard.WorldGuard; -import com.sk89q.worldguard.bukkit.BukkitConfigurationManager; -import com.sk89q.worldguard.bukkit.BukkitWorldConfiguration; -import com.sk89q.worldguard.bukkit.WorldGuardPlugin; -import com.sk89q.worldguard.protection.ApplicableRegionSet; -import com.sk89q.worldguard.protection.flags.Flags; -import org.bukkit.Location; -import org.bukkit.entity.EntityType; -import org.bukkit.event.EventHandler; -import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason; - -import java.util.Set; - -/** - * Cancels blocked natural spawns before the server constructs the entity. - * - * The CreatureSpawnEvent checks in {@link WorldGuardEntityListener#onCreatureSpawn} - * fire at the very end of the spawn pipeline: by then the server has picked a spawn - * position, run the placement checks, constructed the mob and run finalizeSpawn, and - * the cancelled mob is thrown away. Since a cancelled spawn never counts toward the - * mob cap, the natural spawner keeps retrying the same area at full rate, so regions - * that deny mob-spawning become permanent spawn attempt hotspots that pay entity - * construction over and over for nothing. - * - * This listener applies the same natural spawn checks in Paper's - * PreCreatureSpawnEvent, before the entity exists. Cancelling at that stage also - * makes the server end the remaining attempts for the chunk in that spawn cycle, so - * the retry pressure disappears too: on a flat test world with a region denying - * mob-spawning over the whole spawn range and the mob cap kept empty, the attempt - * rate around a single player collapsed from roughly 75000 attempts per second to - * roughly 60 per second, with no entities constructed. - * - * Only NATURAL spawns are handled; every other spawn reason keeps going through the - * CreatureSpawnEvent checks unchanged. The listener is only registered when the - * Paper event is available. - */ -public class PaperPreSpawnListener extends AbstractListener { - - public PaperPreSpawnListener(WorldGuardPlugin plugin) { - super(plugin); - } - - @EventHandler(ignoreCancelled = true) - public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { - if (event.getReason() != SpawnReason.NATURAL) { - return; - } - - BukkitConfigurationManager cfg = getConfig(); - - if (cfg.activityHaltToggle) { - event.setCancelled(true); - return; - } - - Location eventLoc = event.getSpawnLocation(); - BukkitWorldConfiguration wcfg = getWorldConfig(eventLoc.getWorld()); - - EntityType entityType = event.getType(); - com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); - - if (weEntityType != null && wcfg.blockCreatureSpawn.contains(weEntityType)) { - event.setCancelled(true); - return; - } - - if (wcfg.useRegions && cfg.useRegionsCreatureSpawnEvent) { - ApplicableRegionSet set = - WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(eventLoc)); - - if (!set.testState(null, Flags.MOB_SPAWNING)) { - event.setCancelled(true); - return; - } - - Set entityTypes = set.queryValue(null, Flags.DENY_SPAWN); - if (entityTypes != null && weEntityType != null && entityTypes.contains(weEntityType)) { - event.setCancelled(true); - return; - } - } - - if (wcfg.blockGroundSlimes && entityType == EntityType.SLIME && eventLoc.getY() >= 60) { - event.setCancelled(true); - } - } -} 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..ede2d8eb7 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; @@ -907,6 +908,65 @@ 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. Since a cancelled + * spawn never counts toward the mob cap, the natural spawner keeps retrying + * the same area at full rate, so regions that deny mob spawning become + * permanent spawn attempt hotspots. Cancelling the pre spawn event instead + * also ends the remaining attempts for the chunk in that spawn cycle, which + * collapses the retry pressure as well. + * + * Only NATURAL spawns are handled here; every other spawn reason keeps going + * through the CreatureSpawnEvent checks unchanged. + */ + @EventHandler(ignoreCancelled = true) + public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { + if (event.getReason() != SpawnReason.NATURAL) { + return; + } + + ConfigurationManager cfg = getConfig(); + + if (cfg.activityHaltToggle) { + event.setCancelled(true); + return; + } + + Location eventLoc = event.getSpawnLocation(); + WorldConfiguration wcfg = getWorldConfig(eventLoc.getWorld()); + + EntityType entityType = event.getType(); + com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); + + if (weEntityType != null && wcfg.blockCreatureSpawn.contains(weEntityType)) { + event.setCancelled(true); + return; + } + + if (wcfg.useRegions && cfg.useRegionsCreatureSpawnEvent) { + ApplicableRegionSet set = + WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(eventLoc)); + + if (!set.testState(null, Flags.MOB_SPAWNING)) { + event.setCancelled(true); + return; + } + + Set entityTypes = set.queryValue(null, Flags.DENY_SPAWN); + if (entityTypes != null && weEntityType != null && entityTypes.contains(weEntityType)) { + event.setCancelled(true); + return; + } + } + + if (wcfg.blockGroundSlimes && entityType == EntityType.SLIME && eventLoc.getY() >= 60) { + event.setCancelled(true); + } + } } private static class SpigotListener implements Listener { From a6aa240cf0951a75c92b6b8e13d8e719192c677c Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:24:00 +0200 Subject: [PATCH 3/7] Match the HIGH priority of the other protection handlers --- .../worldguard/bukkit/listener/WorldGuardEntityListener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ede2d8eb7..a6895131e 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 @@ -923,7 +923,7 @@ public void onEntityZap(EntityZapEvent event) { * Only NATURAL spawns are handled here; every other spawn reason keeps going * through the CreatureSpawnEvent checks unchanged. */ - @EventHandler(ignoreCancelled = true) + @EventHandler(priority = EventPriority.HIGH, ignoreCancelled = true) public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { if (event.getReason() != SpawnReason.NATURAL) { return; From 2d879c760adb05cb056c3eca3a01c0aa0f8e02a2 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:27:27 +0200 Subject: [PATCH 4/7] Extract the shared spawn checks into a method used by both handlers The pre spawn handler duplicated the tail of onCreatureSpawn. Both now call handleCreatureSpawn, which takes the event as a Cancellable and the location, entity type and spawn reason as parameters, following the same shape as handlePigZap. The entity specific checks (plugin spawning, armor stands, tamed animals) stay in onCreatureSpawn since no entity exists yet at pre spawn time, and the activity halt check stays in each handler so its ordering relative to those checks is unchanged. --- .../listener/WorldGuardEntityListener.java | 46 ++++--------------- 1 file changed, 10 insertions(+), 36 deletions(-) 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 a6895131e..cc62ce777 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 @@ -658,7 +658,12 @@ public void onCreatureSpawn(CreatureSpawnEvent event) { return; } - EntityType entityType = event.getEntityType(); + handleCreatureSpawn(event, event.getLocation(), event.getEntityType(), event.getSpawnReason()); + } + + private static void handleCreatureSpawn(Cancellable event, Location location, EntityType entityType, SpawnReason spawnReason) { + ConfigurationManager cfg = getConfig(); + WorldConfiguration wcfg = getWorldConfig(location.getWorld()); com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); @@ -667,11 +672,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); @@ -686,8 +689,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; } @@ -936,36 +939,7 @@ public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { return; } - Location eventLoc = event.getSpawnLocation(); - WorldConfiguration wcfg = getWorldConfig(eventLoc.getWorld()); - - EntityType entityType = event.getType(); - com.sk89q.worldedit.world.entity.EntityType weEntityType = BukkitAdapter.adapt(entityType); - - if (weEntityType != null && wcfg.blockCreatureSpawn.contains(weEntityType)) { - event.setCancelled(true); - return; - } - - if (wcfg.useRegions && cfg.useRegionsCreatureSpawnEvent) { - ApplicableRegionSet set = - WorldGuard.getInstance().getPlatform().getRegionContainer().createQuery().getApplicableRegions(BukkitAdapter.adapt(eventLoc)); - - if (!set.testState(null, Flags.MOB_SPAWNING)) { - event.setCancelled(true); - return; - } - - Set entityTypes = set.queryValue(null, Flags.DENY_SPAWN); - if (entityTypes != null && weEntityType != null && entityTypes.contains(weEntityType)) { - event.setCancelled(true); - return; - } - } - - if (wcfg.blockGroundSlimes && entityType == EntityType.SLIME && eventLoc.getY() >= 60) { - event.setCancelled(true); - } + handleCreatureSpawn(event, event.getSpawnLocation(), event.getType(), event.getReason()); } } From e07f43a9fb19a1e4e5886aecf4cbc3a8d0e0aed2 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Mon, 10 Aug 2026 13:59:05 +0200 Subject: [PATCH 5/7] Pass cfg and wcfg into the shared spawn checks Both handlers already have them, so the shared method no longer does a second lookup. --- .../bukkit/listener/WorldGuardEntityListener.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) 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 cc62ce777..bf6fc10b0 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 @@ -658,13 +658,11 @@ public void onCreatureSpawn(CreatureSpawnEvent event) { return; } - handleCreatureSpawn(event, event.getLocation(), event.getEntityType(), event.getSpawnReason()); + handleCreatureSpawn(event, event.getLocation(), event.getEntityType(), event.getSpawnReason(), cfg, wcfg); } - private static void handleCreatureSpawn(Cancellable event, Location location, EntityType entityType, SpawnReason spawnReason) { - ConfigurationManager cfg = getConfig(); - WorldConfiguration wcfg = getWorldConfig(location.getWorld()); - + 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)) { @@ -939,7 +937,8 @@ public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { return; } - handleCreatureSpawn(event, event.getSpawnLocation(), event.getType(), event.getReason()); + Location spawnLoc = event.getSpawnLocation(); + handleCreatureSpawn(event, spawnLoc, event.getType(), event.getReason(), cfg, getWorldConfig(spawnLoc.getWorld())); } } From 93d73e868bf6e9180a372c0f1d4ba61a6fc322c2 Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Tue, 11 Aug 2026 01:06:52 +0200 Subject: [PATCH 6/7] Correct the rationale on the pre spawn handler The comment claimed that cancelling ends the chunk's remaining spawn attempts. It does not. Only setShouldAbortSpawn(true) makes the spawner return early; a plain cancel falls through to the next candidate position exactly like a failed placement check. What the handler does buy is stated accurately instead: the placement checks, the entity construction and finalizeSpawn are skipped for a spawn that was going to be refused. Two consequences a server operator should know are now named. The region query runs for every candidate position rather than every constructed mob, because Paper fires the event before the vanilla suitability checks. And on Paper with per-player-mob-spawns enabled, every cancelled pre spawn is charged to a per player mob backoff counter, so a large denied region can suppress spawning in neighbouring chunks that allow mobs. --- .../listener/WorldGuardEntityListener.java | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) 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 bf6fc10b0..4538a2938 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 @@ -914,12 +914,24 @@ public void onEntityZap(EntityZapEvent 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. Since a cancelled - * spawn never counts toward the mob cap, the natural spawner keeps retrying - * the same area at full rate, so regions that deny mob spawning become - * permanent spawn attempt hotspots. Cancelling the pre spawn event instead - * also ends the remaining attempts for the chunk in that spawn cycle, which - * collapses the retry pressure as well. + * 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. From 310add7c513d87948650d88715d4211fd767fe7b Mon Sep 17 00:00:00 2001 From: rasmus123d <59487370+RasmusKD@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:13:29 +0200 Subject: [PATCH 7/7] Add a config option for the pre spawn handler regions.use-pre-creature-spawn-event, default true, matching how regions.use-creature-spawn-event already gates the CreatureSpawnEvent path. Turning it off leaves the existing handler doing all the work, so a server that sees a regression can revert without downgrading. --- .../worldguard/bukkit/listener/WorldGuardEntityListener.java | 4 ++++ .../com/sk89q/worldguard/config/ConfigurationManager.java | 1 + .../com/sk89q/worldguard/config/YamlConfigurationManager.java | 1 + 3 files changed, 6 insertions(+) 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 4538a2938..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 @@ -944,6 +944,10 @@ public void onPreCreatureSpawn(PreCreatureSpawnEvent event) { ConfigurationManager cfg = getConfig(); + if (!cfg.useRegionsPreCreatureSpawnEvent) { + return; + } + if (cfg.activityHaltToggle) { event.setCancelled(true); return; 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);