Skip to content

Commit ccdf020

Browse files
authored
Merge pull request #268 from BentoBoxWorld/fix/134-spawn-egg-consumed
Deny spawn-egg use before consumption when at the entity limit (#134)
2 parents 80705a3 + f78f97f commit ccdf020

2 files changed

Lines changed: 164 additions & 2 deletions

File tree

src/main/java/world/bentobox/limits/listeners/EntityLimitListener.java

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,12 @@
1414
import org.bukkit.event.entity.EntityBreedEvent;
1515
import org.bukkit.event.entity.EntityPortalEvent;
1616
import org.bukkit.event.entity.EntityRemoveEvent;
17+
import org.bukkit.event.block.Action;
1718
import org.bukkit.event.hanging.HangingPlaceEvent;
19+
import org.bukkit.event.player.PlayerInteractEntityEvent;
20+
import org.bukkit.event.player.PlayerInteractEvent;
1821
import org.bukkit.event.vehicle.VehicleCreateEvent;
22+
import org.bukkit.inventory.ItemStack;
1923
import org.eclipse.jdt.annotation.Nullable;
2024
import world.bentobox.bentobox.BentoBox;
2125
import world.bentobox.bentobox.api.localization.TextVariables;
@@ -153,6 +157,87 @@ public void onBlock(HangingPlaceEvent hangingPlaceEvent) {
153157
});
154158
}
155159

160+
/**
161+
* Deny spawn-egg use the moment a player would exceed an entity limit, before the egg is
162+
* consumed. The {@link CreatureSpawnEvent} path already cancels the over-limit spawn, but by
163+
* then the egg item has been used up; cancelling the interaction here keeps the egg (#134).
164+
*/
165+
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
166+
public void onSpawnEggUseOnBlock(PlayerInteractEvent e) {
167+
if (e.getAction() != Action.RIGHT_CLICK_BLOCK || e.getClickedBlock() == null) {
168+
return;
169+
}
170+
EntityType type = spawnEggType(e.getItem());
171+
if (type != null) {
172+
checkSpawnEggLimit(e, e.getPlayer(), e.getClickedBlock().getLocation(), type);
173+
}
174+
}
175+
176+
@EventHandler(priority = EventPriority.LOW, ignoreCancelled = true)
177+
public void onSpawnEggUseOnEntity(PlayerInteractEntityEvent e) {
178+
if (e.getHand() == null) {
179+
return;
180+
}
181+
EntityType type = spawnEggType(e.getPlayer().getInventory().getItem(e.getHand()));
182+
if (type != null) {
183+
checkSpawnEggLimit(e, e.getPlayer(), e.getRightClicked().getLocation(), type);
184+
}
185+
}
186+
187+
private void checkSpawnEggLimit(Cancellable event, Player player, Location location, EntityType type) {
188+
if (!addon.inGameModeWorld(location.getWorld())) {
189+
return;
190+
}
191+
addon.getIslands().getIslandAt(location).ifPresent(island -> {
192+
boolean bypass = player.isOp() || player.hasPermission(
193+
addon.getPlugin().getIWM().getPermissionPrefix(location.getWorld()) + MOD_BYPASS);
194+
if (bypass || island.isSpawn()) {
195+
return;
196+
}
197+
AtLimitResult res = atLimit(island, type, envOf(location.getWorld()));
198+
if (res.hit()) {
199+
event.setCancelled(true);
200+
notifyEntityLimit(player, type, res);
201+
}
202+
});
203+
}
204+
205+
/**
206+
* Resolve the entity type a spawn-egg item would create, e.g. {@code PIG_SPAWN_EGG -> PIG}.
207+
*
208+
* @return the entity type, or {@code null} if the item is not a recognised spawn egg
209+
*/
210+
private EntityType spawnEggType(ItemStack item) {
211+
if (item == null) {
212+
return null;
213+
}
214+
String name = item.getType().name();
215+
if (!name.endsWith("_SPAWN_EGG")) {
216+
return null;
217+
}
218+
try {
219+
return EntityType.valueOf(name.substring(0, name.length() - "_SPAWN_EGG".length()));
220+
} catch (IllegalArgumentException ex) {
221+
return null;
222+
}
223+
}
224+
225+
private void notifyEntityLimit(Player player, EntityType type, AtLimitResult res) {
226+
if (res.getTypelimit() != null) {
227+
User.getInstance(player).notify("entity-limits.hit-limit", "[entity]",
228+
Util.prettifyText(type.toString()), TextVariables.NUMBER,
229+
String.valueOf(res.getTypelimit().getValue()));
230+
} else {
231+
User.getInstance(player).notify("entity-limits.hit-limit", "[entity]",
232+
res.getGrouplimit().getKey().getName() + " ("
233+
+ res.getGrouplimit().getKey().getTypes().stream()
234+
.map(x -> Util.prettifyText(x.toString()))
235+
.collect(Collectors.joining(", "))
236+
+ ")",
237+
TextVariables.NUMBER, String.valueOf(res.getGrouplimit().getValue()));
238+
}
239+
}
240+
156241
/* =========================================================================
157242
* Increment handlers (MONITOR priority — count entities that actually spawned)
158243
* ========================================================================= */
@@ -473,8 +558,10 @@ private void tellPlayers(Location location, Entity entity, SpawnReason spawnReas
473558
* for the entity's current environment.
474559
*/
475560
AtLimitResult atLimit(Island island, Entity entity) {
476-
Environment env = envOf(entity.getWorld());
477-
EntityType type = entity.getType();
561+
return atLimit(island, entity.getType(), envOf(entity.getWorld()));
562+
}
563+
564+
AtLimitResult atLimit(Island island, EntityType type, Environment env) {
478565
@Nullable
479566
IslandBlockCount ibc = addon.getBlockLimitListener().getIsland(island.getUniqueId());
480567

src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,18 @@
3535
import org.bukkit.event.entity.CreatureSpawnEvent;
3636
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
3737
import org.bukkit.event.entity.EntityBreedEvent;
38+
import org.bukkit.event.block.Action;
3839
import org.bukkit.event.hanging.HangingPlaceEvent;
40+
import org.bukkit.event.player.PlayerInteractEntityEvent;
41+
import org.bukkit.event.player.PlayerInteractEvent;
3942
import org.bukkit.entity.Hanging;
4043
import org.bukkit.entity.Minecart;
4144
import org.bukkit.entity.Painting;
4245
import org.bukkit.entity.Villager;
4346
import org.bukkit.event.vehicle.VehicleCreateEvent;
4447
import org.bukkit.inventory.EquipmentSlot;
48+
import org.bukkit.inventory.ItemStack;
49+
import org.bukkit.inventory.PlayerInventory;
4550
import org.bukkit.Material;
4651
import org.junit.jupiter.api.AfterEach;
4752
import org.junit.jupiter.api.BeforeEach;
@@ -680,4 +685,74 @@ private void invokeDetect(String method, Location loc) throws Exception {
680685
m.invoke(ell, loc);
681686
}
682687

688+
// --- Spawn-egg interact guard tests (#134) ---
689+
690+
@Test
691+
public void testSpawnEggOnEntityAtLimitIsCancelled() {
692+
ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4); // seeded count is 4 -> at limit
693+
Player p = eggPlayer(Material.ENDERMAN_SPAWN_EGG, EquipmentSlot.HAND);
694+
Entity clicked = mock(Entity.class);
695+
when(clicked.getLocation()).thenReturn(location);
696+
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(p, clicked, EquipmentSlot.HAND);
697+
698+
ell.onSpawnEggUseOnEntity(e);
699+
700+
// Cancelled before the egg is consumed, rather than only blocking the later spawn.
701+
assertTrue(e.isCancelled());
702+
}
703+
704+
@Test
705+
public void testSpawnEggOnEntityUnderLimitNotCancelled() {
706+
ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 10); // count 4 < 10
707+
Player p = eggPlayer(Material.ENDERMAN_SPAWN_EGG, EquipmentSlot.HAND);
708+
Entity clicked = mock(Entity.class);
709+
when(clicked.getLocation()).thenReturn(location);
710+
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(p, clicked, EquipmentSlot.HAND);
711+
712+
ell.onSpawnEggUseOnEntity(e);
713+
714+
assertFalse(e.isCancelled());
715+
}
716+
717+
@Test
718+
public void testNonSpawnEggOnEntityIgnored() {
719+
ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4);
720+
Player p = eggPlayer(Material.STICK, EquipmentSlot.HAND);
721+
Entity clicked = mock(Entity.class);
722+
when(clicked.getLocation()).thenReturn(location);
723+
PlayerInteractEntityEvent e = new PlayerInteractEntityEvent(p, clicked, EquipmentSlot.HAND);
724+
725+
ell.onSpawnEggUseOnEntity(e);
726+
727+
assertFalse(e.isCancelled());
728+
}
729+
730+
@Test
731+
public void testSpawnEggOnBlockAtLimitIsCancelled() {
732+
ibc.setEntityLimit(Environment.NORMAL, EntityType.ENDERMAN, 4);
733+
Player p = mock(Player.class);
734+
when(p.isOp()).thenReturn(false);
735+
when(p.hasPermission(anyString())).thenReturn(false);
736+
when(p.getUniqueId()).thenReturn(UUID.randomUUID());
737+
Block clicked = mock(Block.class);
738+
when(clicked.getLocation()).thenReturn(location);
739+
PlayerInteractEvent e = new PlayerInteractEvent(p, Action.RIGHT_CLICK_BLOCK,
740+
new ItemStack(Material.ENDERMAN_SPAWN_EGG), clicked, BlockFace.UP);
741+
742+
ell.onSpawnEggUseOnBlock(e);
743+
744+
assertTrue(e.isCancelled());
745+
}
746+
747+
private Player eggPlayer(Material item, EquipmentSlot hand) {
748+
Player p = mock(Player.class);
749+
when(p.isOp()).thenReturn(false);
750+
when(p.hasPermission(anyString())).thenReturn(false);
751+
when(p.getUniqueId()).thenReturn(UUID.randomUUID());
752+
PlayerInventory inv = mock(PlayerInventory.class);
753+
when(p.getInventory()).thenReturn(inv);
754+
when(inv.getItem(hand)).thenReturn(new ItemStack(item));
755+
return p;
756+
}
757+
683758
}

0 commit comments

Comments
 (0)