map = new LinkedHashMap<>();
+ map.put("ancient_city", false);
+ map.put("trial_chambers", false);
+ map.put("mansion", false);
+ map.put("mineshaft", true);
+ map.put("stronghold", true);
+ return map;
+ }
+
+
/**
* This method returns the normalRoof value.
* @return the value of normalRoof.
diff --git a/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java b/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java
new file mode 100644
index 0000000..29b974c
--- /dev/null
+++ b/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java
@@ -0,0 +1,81 @@
+package world.bentobox.caveblock.listeners;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.bukkit.World;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+import org.bukkit.event.world.AsyncStructureSpawnEvent;
+
+import world.bentobox.caveblock.CaveBlock;
+
+/**
+ * Prevents configured vanilla structures from generating in the CaveBlock
+ * overworld.
+ *
+ * The overworld delegates to vanilla generation, which includes structures
+ * such as Ancient Cities, Trial Chambers and Strongholds. Some of these fill or
+ * unbalance a cave world (see issue #112). The {@link org.bukkit.generator.ChunkGenerator}
+ * flag can only turn all structures on or off, so this listener provides
+ * per-structure control by cancelling the spawn of any structure the admin has
+ * 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
+ * performs no Bukkit world access.
+ *
+ * @author tastybento
+ */
+public class StructureGenerationListener implements Listener {
+
+ private final CaveBlock addon;
+
+ /**
+ * @param addon CaveBlock addon
+ */
+ public StructureGenerationListener(CaveBlock addon) {
+ this.addon = addon;
+ }
+
+ /**
+ * Cancels the spawn of a disabled structure in the CaveBlock overworld.
+ *
+ * @param event the structure spawn event
+ */
+ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
+ public void onStructureSpawn(AsyncStructureSpawnEvent event) {
+ World world = event.getWorld();
+ // Only the CaveBlock overworld uses vanilla structures; nether/end do not.
+ if (world.getEnvironment() != World.Environment.NORMAL || !addon.inWorld(world)) {
+ return;
+ }
+ String structureKey = event.getStructure().getKey().getKey();
+ if (isDisabled(structureKey)) {
+ event.setCancelled(true);
+ }
+ }
+
+ /**
+ * @param structureKey the vanilla structure key path, e.g. {@code ancient_city}
+ * @return {@code true} if the config explicitly disables this structure
+ */
+ private boolean isDisabled(String structureKey) {
+ Map structures = addon.getSettings().getGenerateStructures();
+ if (structures == null || structures.isEmpty()) {
+ return false;
+ }
+ for (Map.Entry entry : structures.entrySet()) {
+ // Accept hyphen or underscore separators and any casing.
+ if (normalize(entry.getKey()).equals(structureKey) && Boolean.FALSE.equals(entry.getValue())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String normalize(String key) {
+ return key.toLowerCase(Locale.ROOT).replace('-', '_');
+ }
+}
diff --git a/src/main/resources/config.yml b/src/main/resources/config.yml
index 5c36969..150d0a6 100644
--- a/src/main/resources/config.yml
+++ b/src/main/resources/config.yml
@@ -91,6 +91,22 @@ world:
# Should not be less than cave height.
# /!\ BentoBox currently does not support changing this value mid-game. If you do need to change it, do a full reset of your databases and worlds.
world-depth: 319
+ #
+ # Vanilla structures that may generate in the overworld cave world.
+ # Set a structure to false to stop it generating; structures not listed here
+ # generate as normal. Use the vanilla structure key, for example:
+ # ancient_city, trial_chambers, mineshaft, mineshaft_mesa, stronghold,
+ # mansion, monument, pillager_outpost, ruined_portal, trail_ruins,
+ # village_plains, desert_pyramid, jungle_pyramid, igloo, swamp_hut
+ # Large structures like Ancient Cities and Trial Chambers can fill or unbalance
+ # a cave world, so they are disabled by default. Only affects the overworld.
+ # Added since 1.23.0.
+ structures:
+ ancient_city: false
+ trial_chambers: false
+ mansion: false
+ mineshaft: true
+ stronghold: true
normal:
# Make over world roof of bedrock. If false, it will be made from the main block (stone).
# /!\ BentoBox currently does not support changing this value mid-game. If you do need to change it, do a full reset of your databases and worlds.
diff --git a/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java b/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java
new file mode 100644
index 0000000..e61c3f4
--- /dev/null
+++ b/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java
@@ -0,0 +1,110 @@
+package world.bentobox.caveblock.listeners;
+
+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.Map;
+
+import org.bukkit.NamespacedKey;
+import org.bukkit.World;
+import org.bukkit.event.world.AsyncStructureSpawnEvent;
+import org.bukkit.generator.structure.Structure;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockbukkit.mockbukkit.MockBukkit;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import world.bentobox.caveblock.CaveBlock;
+import world.bentobox.caveblock.Settings;
+
+/**
+ * Tests for {@link StructureGenerationListener}.
+ */
+@ExtendWith(MockitoExtension.class)
+class StructureGenerationListenerTest {
+
+ @Mock
+ private CaveBlock addon;
+ @Mock
+ private Settings settings;
+ @Mock
+ private World world;
+
+ private StructureGenerationListener listener;
+
+ @BeforeEach
+ public void setUp() {
+ MockBukkit.mock();
+ lenient().when(addon.getSettings()).thenReturn(settings);
+ lenient().when(settings.getGenerateStructures())
+ .thenReturn(Map.of("ancient_city", false, "trial_chambers", false, "mineshaft", true));
+ lenient().when(addon.inWorld(world)).thenReturn(true);
+ lenient().when(world.getEnvironment()).thenReturn(World.Environment.NORMAL);
+ listener = new StructureGenerationListener(addon);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ MockBukkit.unmock();
+ }
+
+ private AsyncStructureSpawnEvent event(String structureKey) {
+ Structure structure = mock(Structure.class);
+ lenient().when(structure.getKey()).thenReturn(NamespacedKey.minecraft(structureKey));
+ AsyncStructureSpawnEvent e = mock(AsyncStructureSpawnEvent.class);
+ lenient().when(e.getWorld()).thenReturn(world);
+ lenient().when(e.getStructure()).thenReturn(structure);
+ return e;
+ }
+
+ @Test
+ void testDisabledStructureIsCancelled() {
+ AsyncStructureSpawnEvent e = event("ancient_city");
+ listener.onStructureSpawn(e);
+ verify(e).setCancelled(true);
+ }
+
+ @Test
+ void testEnabledStructureIsNotCancelled() {
+ AsyncStructureSpawnEvent e = event("mineshaft");
+ listener.onStructureSpawn(e);
+ verify(e, never()).setCancelled(true);
+ }
+
+ @Test
+ void testUnlistedStructureGeneratesNormally() {
+ AsyncStructureSpawnEvent e = event("village_plains");
+ listener.onStructureSpawn(e);
+ verify(e, never()).setCancelled(true);
+ }
+
+ @Test
+ void testStructureOutsideCaveBlockWorldIsIgnored() {
+ when(addon.inWorld(world)).thenReturn(false);
+ AsyncStructureSpawnEvent e = event("ancient_city");
+ listener.onStructureSpawn(e);
+ verify(e, never()).setCancelled(true);
+ }
+
+ @Test
+ void testNetherEnvironmentIsIgnored() {
+ when(world.getEnvironment()).thenReturn(World.Environment.NETHER);
+ AsyncStructureSpawnEvent e = event("ancient_city");
+ listener.onStructureSpawn(e);
+ verify(e, never()).setCancelled(true);
+ }
+
+ @Test
+ void testConfigKeyWithHyphensAndCaseMatches() {
+ when(settings.getGenerateStructures()).thenReturn(Map.of("Ancient-City", false));
+ AsyncStructureSpawnEvent e = event("ancient_city");
+ listener.onStructureSpawn(e);
+ verify(e).setCancelled(true);
+ }
+}