From ad67d96420b6bd670680b81e755e8c8779e1f2ab Mon Sep 17 00:00:00 2001 From: bobhenl <18662086+bobhenl@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:53:38 +0100 Subject: [PATCH] Avoid allocations in the entity spawn listener hot path --- .../core/listeners/MVEntityListener.java | 48 ++++++++++--------- .../multiverse/core/world/WorldManager.java | 12 +++++ .../multiverse/core/world/WorldStore.java | 9 +++- .../core/world/entity/EntitySpawnConfig.java | 14 ++++-- .../world/entity/SpawnCategoryMapper.java | 13 ++++- 5 files changed, 68 insertions(+), 28 deletions(-) diff --git a/src/main/java/org/mvplugins/multiverse/core/listeners/MVEntityListener.java b/src/main/java/org/mvplugins/multiverse/core/listeners/MVEntityListener.java index 3497f53a0..5b551c687 100644 --- a/src/main/java/org/mvplugins/multiverse/core/listeners/MVEntityListener.java +++ b/src/main/java/org/mvplugins/multiverse/core/listeners/MVEntityListener.java @@ -10,7 +10,9 @@ import com.destroystokyo.paper.event.entity.PreCreatureSpawnEvent; import com.dumptruckman.minecraft.util.Logging; import jakarta.inject.Inject; +import org.bukkit.World; import org.bukkit.entity.Entity; +import org.bukkit.entity.EntityType; import org.bukkit.entity.LivingEntity; import org.bukkit.entity.Player; import org.bukkit.event.Listener; @@ -29,6 +31,7 @@ import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventClass; import org.mvplugins.multiverse.core.dynamiclistener.annotations.EventMethod; import org.mvplugins.multiverse.core.utils.compatibility.EntityCompatibility; +import org.mvplugins.multiverse.core.world.LoadedMultiverseWorld; import org.mvplugins.multiverse.core.world.WorldManager; import java.util.Arrays; @@ -102,14 +105,11 @@ public void onEvent(PreCreatureSpawnEvent event) { return; } - worldManager.getLoadedWorld(event.getSpawnLocation().getWorld()) - .peek(world -> { - if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getType())) { - Logging.finest("Cancelling Pre Creature Spawn Event for: " + event.getType()); - event.setCancelled(true); - event.setShouldAbortSpawn(true); - } - }); + if (!shouldAllowSpawn(event.getSpawnLocation().getWorld(), event.getType())) { + Logging.finest("Cancelling Pre Creature Spawn Event for: " + event.getType()); + event.setCancelled(true); + event.setShouldAbortSpawn(true); + } } }; } @@ -130,13 +130,10 @@ void creatureSpawn(CreatureSpawnEvent event) { return; } - worldManager.getLoadedWorld(event.getEntity().getWorld()) - .peek(world -> { - if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) { - Logging.finest("Cancelling Creature Spawn Event for: " + event.getEntity()); - event.setCancelled(true); - } - }); + if (!shouldAllowSpawn(event.getEntity().getWorld(), event.getEntityType())) { + Logging.finest("Cancelling Creature Spawn Event for: " + event.getEntity()); + event.setCancelled(true); + } } /** @@ -149,13 +146,20 @@ void entitySpawn(EntitySpawnEvent event) { return; } - worldManager.getLoadedWorld(event.getEntity().getWorld()) - .peek(world -> { - if (!world.getEntitySpawnConfig().shouldAllowSpawn(event.getEntityType())) { - Logging.finest("Cancelling Entity Spawn Event for: " + event.getEntity()); - event.setCancelled(true); - } - }); + if (!shouldAllowSpawn(event.getEntity().getWorld(), event.getEntityType())) { + Logging.finest("Cancelling Entity Spawn Event for: " + event.getEntity()); + event.setCancelled(true); + } + } + + /** + * Checks the world's spawn config. Spawn events fire thousands of times per second, so this avoids the + * Option and lambda allocations of {@link WorldManager#getLoadedWorld(World)}. + * Worlds not managed by Multiverse always allow the spawn. + */ + private boolean shouldAllowSpawn(@Nullable World bukkitWorld, @NotNull EntityType entityType) { + LoadedMultiverseWorld world = worldManager.getLoadedWorldOrNull(bukkitWorld); + return world == null || world.getEntitySpawnConfig().shouldAllowSpawn(entityType); } @EventMethod diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java index b574a54a4..a7773d2f7 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldManager.java @@ -1177,6 +1177,18 @@ public Option getLoadedWorld(@Nullable World world) { return Option.of(world).flatMap(notNullWorld -> getLoadedWorld(notNullWorld.getName())); } + /** + * Same as {@link #getLoadedWorld(World)}, but returns null instead of allocating an {@link Option}. + * Meant for hot paths such as entity spawn events. + * + * @param world The bukkit world that should be loaded. + * @return The multiverse world, or null if it is not a loaded multiverse world. + */ + @ApiStatus.Internal + public @Nullable LoadedMultiverseWorld getLoadedWorldOrNull(@Nullable World world) { + return world == null ? null : worldStore.getLoadedWorldOrNull(world.getName()); + } + /** * Get a multiverse world that is loaded. * diff --git a/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java b/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java index 57ccf6ed2..b17548756 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/WorldStore.java @@ -218,8 +218,13 @@ Option getLoadedWorld(@Nullable NamespacedKey worldKey) { @NotNull Option getLoadedWorld(@Nullable String worldKeyString) { - return Option.of(loadedMap.get(worldKeyString)) - .filter(MultiverseWorld::isLoaded); + return Option.of(getLoadedWorldOrNull(worldKeyString)); + } + + @Nullable + LoadedMultiverseWorld getLoadedWorldOrNull(@Nullable String worldKeyString) { + LoadedMultiverseWorld world = loadedMap.get(worldKeyString); + return world != null && world.isLoaded() ? world : null; } boolean isLoadedWorld(@Nullable WorldKeyOrName keyOrName) { diff --git a/src/main/java/org/mvplugins/multiverse/core/world/entity/EntitySpawnConfig.java b/src/main/java/org/mvplugins/multiverse/core/world/entity/EntitySpawnConfig.java index 441dd8a8a..06487faba 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/entity/EntitySpawnConfig.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/entity/EntitySpawnConfig.java @@ -28,6 +28,11 @@ public final class EntitySpawnConfig { } public @NotNull SpawnCategoryConfig getSpawnCategoryConfig(@NotNull SpawnCategory spawnCategory) { + // Plain get first, so the common case does not allocate the capturing lambda below. + SpawnCategoryConfig spawnCategoryConfig = spawnCategoriesConfig.get(spawnCategory); + if (spawnCategoryConfig != null) { + return spawnCategoryConfig; + } return spawnCategoriesConfig.computeIfAbsent(spawnCategory, computeSpawnCategory -> new SpawnCategoryConfig( config, @@ -40,9 +45,12 @@ public final class EntitySpawnConfig { public boolean shouldAllowSpawn(@NotNull EntityType entityType) { // Ensure it defaults to true if SpawnCategoryMapper fails to find a spawn category for the entity type, // to avoid accidentally breaking mob spawns. - return SpawnCategoryMapper.getSpawnCategory(entityType) - .map(spawnCategory -> getSpawnCategoryConfig(spawnCategory).shouldAllowSpawn(entityType)) - .getOrElse(true); + // This runs for every spawn event, so it avoids Option and lambda allocations on purpose. + SpawnCategory spawnCategory = SpawnCategoryMapper.getSpawnCategoryOrNull(entityType); + if (spawnCategory == null) { + return true; + } + return getSpawnCategoryConfig(spawnCategory).shouldAllowSpawn(entityType); } public boolean shouldAllowSpawn(@NotNull Entity entity) { diff --git a/src/main/java/org/mvplugins/multiverse/core/world/entity/SpawnCategoryMapper.java b/src/main/java/org/mvplugins/multiverse/core/world/entity/SpawnCategoryMapper.java index 8a3ca65bb..a2d04fe5f 100644 --- a/src/main/java/org/mvplugins/multiverse/core/world/entity/SpawnCategoryMapper.java +++ b/src/main/java/org/mvplugins/multiverse/core/world/entity/SpawnCategoryMapper.java @@ -100,7 +100,18 @@ public static void buildSpawnCategoryMap() { * @return The spawn category that the given entity type is in, or null if it cannot be determined */ static @NotNull Option getSpawnCategory(@Nullable EntityType entityType) { - return Option.of(entityTypeToSpawnCategoryMap.get(entityType)); + return Option.of(getSpawnCategoryOrNull(entityType)); + } + + /** + * Gets the spawn category that the given entity type is in, without wrapping it in an {@link Option}. + * Meant for hot paths such as entity spawn events. + * + * @param entityType The entity type + * @return The spawn category that the given entity type is in, or null if it cannot be determined + */ + static @Nullable SpawnCategory getSpawnCategoryOrNull(@Nullable EntityType entityType) { + return entityTypeToSpawnCategoryMap.get(entityType); } /**