From d68ed5112b05a5869db30b2c2cf010154567eb8c Mon Sep 17 00:00:00 2001 From: tastybento Date: Fri, 14 Aug 2026 07:50:45 -0700 Subject: [PATCH] Let bees exit hives even when the island is at its bee limit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An admin report: at 12/12 bees, bees stored in hives could never come out — every release attempt was cancelled by the limit check, the server kept the bee as a hive occupant and retried every few ticks, spamming "Bee spawning limited to 12!" at nearby players and permanently trapping the bees. A bee leaving its hive is not a new bee: its count was decremented when it entered (EntityRemoveEvent ENTER_BLOCK) and the MONITOR tracker re-increments on exit, so the enter/exit cycle is net-zero. Exempt SpawnReason.BEEHIVE from the limit check while keeping the exit counted. If a placed hive item carries stored (never-counted) bees, the island can end slightly over its limit, which simply blocks further spawns and breeding until the population drops. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015uo8XWUxGr6VEn4TRRQ6YB --- .../limits/listeners/EntityLimitListener.java | 8 +++ .../listeners/EntityLimitListenerTest.java | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java index 07f997f..f61233c 100644 --- a/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java +++ b/src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java @@ -133,7 +133,15 @@ public void onCreatureSpawn(final CreatureSpawnEvent creatureSpawnEvent) { justSpawned.remove(creatureSpawnEvent.getEntity().getUniqueId()); return; } + // BEEHIVE: a bee leaving its hive is not a new bee — its count was decremented when it + // entered (EntityRemoveEvent ENTER_BLOCK), and the MONITOR tracker re-increments on exit, + // so the enter/exit cycle is net-zero. Cancelling the exit would strand the bee: the + // server keeps it as a hive occupant and retries every few ticks, spamming the hit-limit + // message and permanently trapping bees whenever the island is at its limit for any other + // reason. Over-limit bees (e.g. from placing a hive item with stored bees) simply block + // further spawns and breeding until the population drops. if (creatureSpawnEvent.getSpawnReason().equals(SpawnReason.SHOULDER_ENTITY) + || creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BEEHIVE) || (!(creatureSpawnEvent.getEntity() instanceof Villager) && creatureSpawnEvent.getSpawnReason().equals(SpawnReason.BREEDING))) { return; diff --git a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java index 3786df5..dec7466 100644 --- a/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java +++ b/src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java @@ -497,6 +497,57 @@ void testCreatureSpawnDebounceSkipsSecond() throws Exception { assertFalse(event.isCancelled()); } + // --- Bee hive tests --- + + @Test + void testBeehiveExitAtLimitNotCancelled() { + // A bee leaving its hive was decremented when it entered (ENTER_BLOCK), so the exit + // must never be limit-checked — cancelling it strands the bee in the hive and the + // server retries forever, spamming the hit-limit message. + ibc.setEntityLimit(Environment.NORMAL, EntityType.BEE, 1); + ibc.incrementEntity(Environment.NORMAL, EntityType.BEE); + LivingEntity bee = mockEntity(EntityType.BEE, location); + + CreatureSpawnEvent event = new CreatureSpawnEvent(bee, SpawnReason.BEEHIVE); + + ell.onCreatureSpawn(event); + + assertFalse(event.isCancelled()); + verify(islandsManager, never()).getIslandAt(any(Location.class)); + } + + @Test + void testBeehiveExitStillCounted() throws Exception { + // Exempting the exit from the limit check must not exempt it from counting. + LivingEntity bee = mockEntity(EntityType.BEE, location); + CreatureSpawnEvent event = new CreatureSpawnEvent(bee, SpawnReason.BEEHIVE); + + ell.onCreatureSpawnTrack(event); + + assertEquals(1, ibc.getEntityCount(Environment.NORMAL, EntityType.BEE)); + assertEquals("test-island-id", entityIslandMap().get(bee.getUniqueId())); + } + + @Test + void testBeeHiveEnterExitCycleNetZero() throws Exception { + // Full cycle: a counted bee enters a hive (ENTER_BLOCK removal decrements) and is + // later released (BEEHIVE spawn re-increments) — the count must end where it started. + LivingEntity bee = mockEntity(EntityType.BEE, location); + ibc.incrementEntity(Environment.NORMAL, EntityType.BEE); + entityIslandMap().put(bee.getUniqueId(), "test-island-id"); + + ell.onEntityRemove(new EntityRemoveEvent(bee, EntityRemoveEvent.Cause.ENTER_BLOCK)); + assertEquals(0, ibc.getEntityCount(Environment.NORMAL, EntityType.BEE)); + + LivingEntity released = mockEntity(EntityType.BEE, location); + CreatureSpawnEvent exit = new CreatureSpawnEvent(released, SpawnReason.BEEHIVE); + ell.onCreatureSpawn(exit); + assertFalse(exit.isCancelled()); + ell.onCreatureSpawnTrack(exit); + + assertEquals(1, ibc.getEntityCount(Environment.NORMAL, EntityType.BEE)); + } + // --- Copper golem / copper chest limit tests (#276) --- @Test