From 79ad86ff29b005594e1be161954dcde1c0afbbfa Mon Sep 17 00:00:00 2001 From: nossr50 Date: Sat, 16 Nov 2024 14:33:51 -0800 Subject: [PATCH] Fix infinite recursion caused by inadvertent chunk loading/unloading Fixes #5112 --- Changelog.txt | 3 ++ pom.xml | 2 +- .../nossr50/listeners/ChunkListener.java | 12 +++-- .../nossr50/util/TransientEntityTracker.java | 47 ++++++++++++------- 4 files changed, 42 insertions(+), 22 deletions(-) diff --git a/Changelog.txt b/Changelog.txt index c7c898baf..ece84ec19 100644 --- a/Changelog.txt +++ b/Changelog.txt @@ -1,3 +1,6 @@ +Version 2.2.028 + Fix stack overflow during ChunkUnloadEvent + Version 2.2.027 Added Tridents / Crossbows to salvage.vanilla.yml config (see notes) Fixed an issue where Folia could have all of its threads lock up effectively killing the server diff --git a/pom.xml b/pom.xml index b06f0ce9b..284d337fa 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 com.gmail.nossr50.mcMMO mcMMO - 2.2.027 + 2.2.028-SNAPSHOT mcMMO https://github.com/mcMMO-Dev/mcMMO diff --git a/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java b/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java index f972af3c0..1a6a146d7 100644 --- a/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java +++ b/src/main/java/com/gmail/nossr50/listeners/ChunkListener.java @@ -1,21 +1,23 @@ package com.gmail.nossr50.listeners; import com.gmail.nossr50.mcMMO; +import org.bukkit.Chunk; import org.bukkit.entity.LivingEntity; import org.bukkit.event.EventHandler; import org.bukkit.event.Listener; import org.bukkit.event.world.ChunkUnloadEvent; +import java.util.Arrays; import java.util.List; public class ChunkListener implements Listener { @EventHandler(ignoreCancelled = true) public void onChunkUnload(ChunkUnloadEvent event) { - List matchingEntities - = mcMMO.getTransientEntityTracker().getAllTransientEntitiesInChunk(event.getChunk()); - for(LivingEntity livingEntity : matchingEntities) { - mcMMO.getTransientEntityTracker().killSummonAndCleanMobFlags(livingEntity, null, false); - } + final Chunk unloadingChunk = event.getChunk(); + Arrays.stream(unloadingChunk.getEntities()) + .filter(entity -> entity instanceof LivingEntity) + .map(entity -> (LivingEntity) entity) + .forEach(livingEntity -> mcMMO.getTransientEntityTracker().removeTrackedEntity(livingEntity)); } } diff --git a/src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java b/src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java index f45ca6140..ed3b1c16b 100644 --- a/src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java +++ b/src/main/java/com/gmail/nossr50/util/TransientEntityTracker.java @@ -5,7 +5,6 @@ import com.gmail.nossr50.skills.taming.TrackedTamingEntity; import com.gmail.nossr50.util.player.NotificationManager; import com.gmail.nossr50.util.skills.ParticleEffectUtils; import com.gmail.nossr50.util.text.StringUtils; -import org.bukkit.Chunk; import org.bukkit.Location; import org.bukkit.Sound; import org.bukkit.entity.LivingEntity; @@ -19,10 +18,13 @@ import java.util.concurrent.ConcurrentHashMap; import static java.util.stream.Collectors.toSet; public class TransientEntityTracker { - private final @NotNull Map> playerSummonedEntityTracker; + final @NotNull Map> playerSummonedEntityTracker; + // used for fast lookups during chunk unload events + final @NotNull Set entityLookupCache; public TransientEntityTracker() { - playerSummonedEntityTracker = new ConcurrentHashMap<>(); + this.playerSummonedEntityTracker = new ConcurrentHashMap<>(); + this.entityLookupCache = ConcurrentHashMap.newKeySet(); } public void initPlayer(@NotNull Player player) { @@ -33,14 +35,6 @@ public class TransientEntityTracker { cleanPlayer(player, player.getUniqueId()); } - public @NotNull List getAllTransientEntitiesInChunk(@NotNull Chunk chunk) { - return playerSummonedEntityTracker.values().stream() - .flatMap(Collection::stream) - .map(TrackedTamingEntity::getLivingEntity) - .filter(livingEntity -> livingEntity.getLocation().getChunk() == chunk) - .toList(); - } - public int summonCountForPlayerOfType(@NotNull UUID playerUUID, @NotNull CallOfTheWildType callOfTheWildType) { return getTrackedEntities(playerUUID, callOfTheWildType).size(); } @@ -48,6 +42,7 @@ public class TransientEntityTracker { public void addSummon(@NotNull UUID playerUUID, @NotNull TrackedTamingEntity trackedTamingEntity) { playerSummonedEntityTracker.computeIfAbsent(playerUUID, __ -> ConcurrentHashMap.newKeySet()) .add(trackedTamingEntity); + entityLookupCache.add(trackedTamingEntity.getLivingEntity()); } public void killSummonAndCleanMobFlags(@NotNull LivingEntity livingEntity, @Nullable Player player, @@ -77,9 +72,7 @@ public class TransientEntityTracker { } public boolean isTransient(@NotNull LivingEntity livingEntity) { - return playerSummonedEntityTracker.values().stream().anyMatch( - trackedEntities -> trackedEntities.stream() - .anyMatch(trackedTamingEntity -> trackedTamingEntity.getLivingEntity().equals(livingEntity))); + return entityLookupCache.contains(livingEntity); } private @NotNull Set getTrackedEntities(@NotNull UUID playerUUID, @@ -117,7 +110,29 @@ public class TransientEntityTracker { } public void removeSummonFromTracker(@NotNull UUID playerUUID, @NotNull TrackedTamingEntity trackedTamingEntity) { - playerSummonedEntityTracker.computeIfAbsent(playerUUID, __ -> ConcurrentHashMap.newKeySet()) - .remove(trackedTamingEntity); + if (playerSummonedEntityTracker.containsKey(playerUUID)) { + playerSummonedEntityTracker.get(playerUUID).remove(trackedTamingEntity); + entityLookupCache.remove(trackedTamingEntity.getLivingEntity()); + } } + + public void removeTrackedEntity(@NotNull LivingEntity livingEntity) { + // Fail fast if the entity isn't being tracked + if (!entityLookupCache.contains(livingEntity)) { + return; + } + + final List matchingEntities = new ArrayList<>(); + + // Collect matching entities without copying each set + playerSummonedEntityTracker.values().forEach(trackedEntitiesPerPlayer -> + trackedEntitiesPerPlayer.stream() + .filter(trackedTamingEntity -> trackedTamingEntity.getLivingEntity() == livingEntity) + .forEach(matchingEntities::add) + ); + + // Iterate over the collected list to handle removal and cleanup + matchingEntities.forEach(TrackedTamingEntity::run); + } + }