Skip to content

Commit 0232df3

Browse files
authored
Merge pull request #289 from BentoBoxWorld/fix/portal-phantom-entity-counts
Fix phantom entity counts left by portaled mobs
2 parents 9e3f0e5 + 4157970 commit 0232df3

2 files changed

Lines changed: 72 additions & 7 deletions

File tree

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,6 @@ public class EntityLimitListener implements Listener {
5959
private final Limits addon;
6060
/** Entity UUIDs that have just spawned to prevent double-processing. */
6161
private final List<UUID> justSpawned = new ArrayList<>();
62-
/** Entity UUIDs that are currently portaling to prevent double-decrement on cross-world removal. */
63-
private final List<UUID> justPortaled = new ArrayList<>();
6462
/** Maps entity UUID to island ID so decrement works even when the entity dies off-island. */
6563
private final Map<UUID, String> entityIslandMap = new HashMap<>();
6664
/** Cardinal directions used for block structure detection. */
@@ -344,9 +342,6 @@ public void onEntityRemove(EntityRemoveEvent e) {
344342
World w = entity.getWorld();
345343
if (!addon.inGameModeWorld(w)) return;
346344

347-
// Entity is being portaled — count transfer was already handled by onEntityPortal.
348-
if (justPortaled.remove(entity.getUniqueId())) return;
349-
350345
String islandId = entityIslandMap.remove(entity.getUniqueId());
351346
if (islandId != null) {
352347
addon.getBlockLimitListener().decrementEntity(islandId, envOf(w), entity.getType());
@@ -374,8 +369,9 @@ public void onEntityPortal(EntityPortalEvent e) {
374369
if (fromEnv == toEnv) return;
375370
if (!addon.inGameModeWorld(fromWorld) && !addon.inGameModeWorld(toWorld)) return;
376371

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

380376
// Decrement at source if on a tracked island
381377
if (addon.inGameModeWorld(fromWorld)) {

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

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.bukkit.event.entity.CreatureSpawnEvent;
3838
import org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason;
3939
import org.bukkit.event.entity.EntityBreedEvent;
40+
import org.bukkit.event.entity.EntityPortalEvent;
4041
import org.bukkit.event.entity.EntityRemoveEvent;
4142
import org.bukkit.event.Event;
4243
import org.bukkit.event.block.Action;
@@ -803,6 +804,74 @@ void testReloadedEntityDecrementsWhenItDiesOffIsland() throws Exception {
803804
assertFalse(entityIslandMap().containsKey(enderman.getUniqueId()));
804805
}
805806

807+
// --- EntityPortalEvent / phantom-count tests ---
808+
809+
@Test
810+
void testEntityPortalTransfersCountBetweenEnvironments() throws Exception {
811+
Location netherLoc = mockNetherLocation();
812+
LivingEntity chicken = mockEntity(EntityType.CHICKEN, location);
813+
ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN);
814+
815+
ell.onEntityPortal(new EntityPortalEvent(chicken, location, netherLoc));
816+
817+
assertEquals(0, ibc.getEntityCount(Environment.NORMAL, EntityType.CHICKEN));
818+
assertEquals(1, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));
819+
assertEquals("test-island-id", entityIslandMap().get(chicken.getUniqueId()));
820+
}
821+
822+
@Test
823+
void testPortaledEntityDeathStillDecrements() throws Exception {
824+
// Regression: Paper never fires an EntityRemoveEvent for the dimension change itself
825+
// (RemovalReason.CHANGED_DIMENSION has a null Bukkit cause), so the old justPortaled
826+
// guard was never consumed and instead swallowed the entity's real death decrement,
827+
// leaving a phantom count in the destination environment.
828+
Location netherLoc = mockNetherLocation();
829+
World nether = netherLoc.getWorld();
830+
LivingEntity chicken = mockEntity(EntityType.CHICKEN, location);
831+
ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN);
832+
ell.onEntityPortal(new EntityPortalEvent(chicken, location, netherLoc));
833+
assertEquals(1, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));
834+
835+
// The entity now lives in the nether and dies there.
836+
when(chicken.getWorld()).thenReturn(nether);
837+
when(chicken.getLocation()).thenReturn(netherLoc);
838+
ell.onEntityRemove(new EntityRemoveEvent(chicken, EntityRemoveEvent.Cause.DEATH));
839+
840+
assertEquals(0, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));
841+
assertFalse(entityIslandMap().containsKey(chicken.getUniqueId()));
842+
}
843+
844+
@Test
845+
void testRoundTripPortalThenDeathLeavesNoPhantomCount() throws Exception {
846+
// The reported symptom: a mob portals to the nether, comes back, and dies in the
847+
// overworld — the overworld count must return to zero, not stick at a phantom 1.
848+
Location netherLoc = mockNetherLocation();
849+
World nether = netherLoc.getWorld();
850+
LivingEntity chicken = mockEntity(EntityType.CHICKEN, location);
851+
ibc.incrementEntity(Environment.NORMAL, EntityType.CHICKEN);
852+
853+
ell.onEntityPortal(new EntityPortalEvent(chicken, location, netherLoc));
854+
when(chicken.getWorld()).thenReturn(nether);
855+
when(chicken.getLocation()).thenReturn(netherLoc);
856+
ell.onEntityPortal(new EntityPortalEvent(chicken, netherLoc, location));
857+
when(chicken.getWorld()).thenReturn(world);
858+
when(chicken.getLocation()).thenReturn(location);
859+
860+
ell.onEntityRemove(new EntityRemoveEvent(chicken, EntityRemoveEvent.Cause.DEATH));
861+
862+
assertEquals(0, ibc.getEntityCount(Environment.NORMAL, EntityType.CHICKEN));
863+
assertEquals(0, ibc.getEntityCount(Environment.NETHER, EntityType.CHICKEN));
864+
}
865+
866+
private Location mockNetherLocation() {
867+
World nether = mock(World.class);
868+
when(nether.getEnvironment()).thenReturn(Environment.NETHER);
869+
when(addon.inGameModeWorld(nether)).thenReturn(true);
870+
Location netherLoc = mock(Location.class);
871+
when(netherLoc.getWorld()).thenReturn(nether);
872+
return netherLoc;
873+
}
874+
806875
@SuppressWarnings("unchecked")
807876
private Map<UUID, String> entityIslandMap() throws Exception {
808877
Field f = EntityLimitListener.class.getDeclaredField("entityIslandMap");

0 commit comments

Comments
 (0)