diff --git a/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java b/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java
index 83bc8ca..4d7adc3 100644
--- a/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java
+++ b/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java
@@ -1,5 +1,6 @@
package world.bentobox.caveblock.listeners;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -8,7 +9,9 @@
import org.bukkit.event.EventPriority;
import org.bukkit.event.Listener;
import org.bukkit.event.world.AsyncStructureSpawnEvent;
+import org.bukkit.generator.structure.Structure;
+import io.papermc.paper.event.world.StructuresLocateEvent;
import world.bentobox.caveblock.CaveBlock;
/**
@@ -23,10 +26,17 @@
* disabled in {@code world.structures}.
*
* {@link AsyncStructureSpawnEvent} fires off the main thread during chunk
- * generation, so this handler only reads config and inspects the event. It
+ * generation, so that handler only reads config and inspects the event. It
* queries the event's world for environment and ownership checks but performs
* no world or block mutation, which is what makes it safe to run async.
*
+ * Cancelling the spawn stops a structure being placed but leaves its
+ * placement rules intact, so structure searches ({@code /locate}, Eyes of
+ * Ender, treasure/explorer maps, villager map trades) keep proposing candidate
+ * positions that are then all cancelled, scanning to the radius cap and freezing
+ * the server (issue #116). {@link #onStructuresLocate} closes that hole by removing
+ * disabled structures from the search before it runs.
+ *
* @author tastybento
*/
public class StructureGenerationListener implements Listener {
@@ -58,6 +68,38 @@ public void onStructureSpawn(AsyncStructureSpawnEvent event) {
}
}
+ /**
+ * Prevents disabled structures from being located. {@link StructuresLocateEvent}
+ * fires before any structure search — the {@code /locate} command, Eyes of Ender,
+ * explorer/treasure maps, dolphins and villager map trades. On a cave world where the
+ * target structure is suppressed, that search never succeeds and scans out to the radius
+ * cap, freezing the main thread (issue #116). Removing the disabled structures from the
+ * search list — and cancelling outright when nothing remains — skips the scan entirely so
+ * the search returns "not found" instantly.
+ *
+ * @param event the structure locate event
+ */
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ public void onStructuresLocate(StructuresLocateEvent event) {
+ if (!addon.inWorld(event.getWorld())) {
+ return;
+ }
+ List targets = event.getStructures();
+ List allowed = targets.stream()
+ .filter(structure -> !isDisabled(structure.getKey().getKey()))
+ .toList();
+ if (allowed.size() == targets.size()) {
+ // Nothing disabled in this search — let it run normally.
+ return;
+ }
+ if (allowed.isEmpty()) {
+ // Every requested structure is disabled here: skip the expensive scan entirely.
+ event.setCancelled(true);
+ } else {
+ event.setStructures(allowed);
+ }
+ }
+
/**
* @param structureKey the vanilla structure key path, e.g. {@code ancient_city}
* @return {@code true} if the config explicitly disables this structure
diff --git a/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java b/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java
index e61c3f4..417070c 100644
--- a/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java
+++ b/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java
@@ -1,11 +1,15 @@
package world.bentobox.caveblock.listeners;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.util.Arrays;
+import java.util.List;
import java.util.Map;
import org.bukkit.NamespacedKey;
@@ -17,9 +21,11 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockbukkit.mockbukkit.MockBukkit;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
+import io.papermc.paper.event.world.StructuresLocateEvent;
import world.bentobox.caveblock.CaveBlock;
import world.bentobox.caveblock.Settings;
@@ -107,4 +113,53 @@ void testConfigKeyWithHyphensAndCaseMatches() {
listener.onStructureSpawn(e);
verify(e).setCancelled(true);
}
+
+ private StructuresLocateEvent locateEvent(String... structureKeys) {
+ List structures = Arrays.stream(structureKeys).map(key -> {
+ Structure structure = mock(Structure.class);
+ lenient().when(structure.getKey()).thenReturn(NamespacedKey.minecraft(key));
+ return structure;
+ }).map(Structure.class::cast).toList();
+ StructuresLocateEvent e = mock(StructuresLocateEvent.class);
+ lenient().when(e.getWorld()).thenReturn(world);
+ lenient().when(e.getStructures()).thenReturn(structures);
+ return e;
+ }
+
+ @Test
+ void testLocateAllDisabledIsCancelled() {
+ StructuresLocateEvent e = locateEvent("trial_chambers");
+ listener.onStructuresLocate(e);
+ verify(e).setCancelled(true);
+ verify(e, never()).setStructures(anyList());
+ }
+
+ @Test
+ void testLocateMixedNarrowsToEnabledOnly() {
+ StructuresLocateEvent e = locateEvent("trial_chambers", "mineshaft");
+ listener.onStructuresLocate(e);
+ verify(e, never()).setCancelled(true);
+ @SuppressWarnings("unchecked")
+ ArgumentCaptor> captor = ArgumentCaptor.forClass(List.class);
+ verify(e).setStructures(captor.capture());
+ assertEquals(1, captor.getValue().size());
+ assertEquals("mineshaft", captor.getValue().get(0).getKey().getKey());
+ }
+
+ @Test
+ void testLocateAllEnabledIsUntouched() {
+ StructuresLocateEvent e = locateEvent("mineshaft", "village_plains");
+ listener.onStructuresLocate(e);
+ verify(e, never()).setCancelled(true);
+ verify(e, never()).setStructures(anyList());
+ }
+
+ @Test
+ void testLocateOutsideCaveBlockWorldIsIgnored() {
+ when(addon.inWorld(world)).thenReturn(false);
+ StructuresLocateEvent e = locateEvent("trial_chambers");
+ listener.onStructuresLocate(e);
+ verify(e, never()).setCancelled(true);
+ verify(e, never()).setStructures(anyList());
+ }
}