Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ public class EntityLimitListener implements Listener {
private final Limits addon;
/** Entity UUIDs that have just spawned to prevent double-processing. */
private final List<UUID> justSpawned = new ArrayList<>();
/** Entity UUIDs that are currently portaling to prevent double-decrement on cross-world removal. */
private final List<UUID> justPortaled = new ArrayList<>();
/** Maps entity UUID to island ID so decrement works even when the entity dies off-island. */
private final Map<UUID, String> entityIslandMap = new HashMap<>();
/** Cardinal directions used for block structure detection. */
Expand Down Expand Up @@ -344,9 +342,6 @@ public void onEntityRemove(EntityRemoveEvent e) {
World w = entity.getWorld();
if (!addon.inGameModeWorld(w)) return;

// Entity is being portaled — count transfer was already handled by onEntityPortal.
if (justPortaled.remove(entity.getUniqueId())) return;

String islandId = entityIslandMap.remove(entity.getUniqueId());
if (islandId != null) {
addon.getBlockLimitListener().decrementEntity(islandId, envOf(w), entity.getType());
Expand Down Expand Up @@ -374,8 +369,9 @@ public void onEntityPortal(EntityPortalEvent e) {
if (fromEnv == toEnv) return;
if (!addon.inGameModeWorld(fromWorld) && !addon.inGameModeWorld(toWorld)) return;

// Prevent onEntityRemove from double-decrementing for the source-world removal.
justPortaled.add(entity.getUniqueId());
// No EntityRemoveEvent guard is needed here: Paper suppresses the event for
// dimension changes (RemovalReason.CHANGED_DIMENSION carries a null Bukkit cause),
// so the source-world removal never reaches onEntityRemove.

// Decrement at source if on a tracked island
if (addon.inGameModeWorld(fromWorld)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.bukkit.event.entity.CreatureSpawnEvent;
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
import org.bukkit.event.entity.EntityBreedEvent;
import org.bukkit.event.entity.EntityPortalEvent;
import org.bukkit.event.entity.EntityRemoveEvent;
import org.bukkit.event.Event;
import org.bukkit.event.block.Action;
Expand Down Expand Up @@ -803,6 +804,74 @@
assertFalse(entityIslandMap().containsKey(enderman.getUniqueId()));
}

// --- EntityPortalEvent / phantom-count tests ---

@Test
void testEntityPortalTransfersCountBetweenEnvironments() throws Exception {
Location netherLoc = mockNetherLocation();
LivingEntity chicken = mockEntity(EntityType.CHICKEN, location);
ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN);

ell.onEntityPortal(new EntityPortalEvent(chicken, location, netherLoc));

assertEquals(0, ibc.getEntityCount(Environment.NORMAL, EntityType.CHICKEN));
assertEquals(1, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));
assertEquals("test-island-id", entityIslandMap().get(chicken.getUniqueId()));
}

@Test
void testPortaledEntityDeathStillDecrements() throws Exception {
// Regression: Paper never fires an EntityRemoveEvent for the dimension change itself
// (RemovalReason.CHANGED_DIMENSION has a null Bukkit cause), so the old justPortaled
// guard was never consumed and instead swallowed the entity's real death decrement,
// leaving a phantom count in the destination environment.
Location netherLoc = mockNetherLocation();
World nether = netherLoc.getWorld();
LivingEntity chicken = mockEntity(EntityType.CHICKEN, location);
ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN);
ell.onEntityPortal(new EntityPortalEvent(chicken, location, netherLoc));
assertEquals(1, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));

// The entity now lives in the nether and dies there.
when(chicken.getWorld()).thenReturn(nether);
when(chicken.getLocation()).thenReturn(netherLoc);
ell.onEntityRemove(new EntityRemoveEvent(chicken, EntityRemoveEvent.Cause.DEATH));

assertEquals(0, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));
assertFalse(entityIslandMap().containsKey(chicken.getUniqueId()));
}

@Test
void testRoundTripPortalThenDeathLeavesNoPhantomCount() throws Exception {

Check warning on line 845 in src/test/java/world/bentobox/limits/listeners/EntityLimitListenerTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the declaration of thrown exception 'java.lang.Exception', as it cannot be thrown from method's body.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_Limits&issues=AZ9OMSCE5_sui7STwv_U&open=AZ9OMSCE5_sui7STwv_U&pullRequest=289
// The reported symptom: a mob portals to the nether, comes back, and dies in the
// overworld — the overworld count must return to zero, not stick at a phantom 1.
Location netherLoc = mockNetherLocation();
World nether = netherLoc.getWorld();
LivingEntity chicken = mockEntity(EntityType.CHICKEN, location);
ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN);

ell.onEntityPortal(new EntityPortalEvent(chicken, location, netherLoc));
when(chicken.getWorld()).thenReturn(nether);
when(chicken.getLocation()).thenReturn(netherLoc);
ell.onEntityPortal(new EntityPortalEvent(chicken, netherLoc, location));
when(chicken.getWorld()).thenReturn(world);
when(chicken.getLocation()).thenReturn(location);

ell.onEntityRemove(new EntityRemoveEvent(chicken, EntityRemoveEvent.Cause.DEATH));

assertEquals(0, ibc.getEntityCount(Environment.NORMAL, EntityType.CHICKEN));
assertEquals(0, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));
}

private Location mockNetherLocation() {
World nether = mock(World.class);
when(nether.getEnvironment()).thenReturn(Environment.NETHER);
when(addon.inGameModeWorld(nether)).thenReturn(true);
Location netherLoc = mock(Location.class);
when(netherLoc.getWorld()).thenReturn(nether);
return netherLoc;
}

@SuppressWarnings("unchecked")
private Map<UUID, String> entityIslandMap() throws Exception {
Field f = EntityLimitListener.class.getDeclaredField("entityIslandMap");
Expand Down
Loading