From 25ca8eae488aa570e09f1c7b17875bcc3c2173e6 Mon Sep 17 00:00:00 2001 From: tastybento Date: Thu, 9 Jul 2026 07:30:30 -0700 Subject: [PATCH] Suppress structures in the spawn area too (#116) The structure listener was registered in onEnable(), but createWorlds() runs before onEnable() and generating a world also generates its spawn-area chunks. Those first structures were therefore placed before the listener existed, so a disabled structure could still appear near spawn. Register the listener at the start of createWorlds(), before any world is created. Because createWorlds() assigns islandWorld from the return value of createWorld(), the island-world reference is still null while the spawn chunks generate, so inWorld() would compare against a null world and throw. Match on the configured world name instead via a new isCaveOverworld() helper, used by both handlers. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_015GBGsNFUshypm5Sj8KQsEa --- .../world/bentobox/caveblock/CaveBlock.java | 8 ++++++- .../StructureGenerationListener.java | 23 ++++++++++++++++--- .../StructureGenerationListenerTest.java | 7 +++--- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/main/java/world/bentobox/caveblock/CaveBlock.java b/src/main/java/world/bentobox/caveblock/CaveBlock.java index 7fa825f..3f813b7 100644 --- a/src/main/java/world/bentobox/caveblock/CaveBlock.java +++ b/src/main/java/world/bentobox/caveblock/CaveBlock.java @@ -70,7 +70,8 @@ public void onEnable() // Register listeners this.registerListener(new CustomHeightLimitations(this)); - this.registerListener(new StructureGenerationListener(this)); + // StructureGenerationListener is registered earlier, in createWorlds(), so it is + // active before the first chunks (including the spawn area) are generated. } @@ -138,6 +139,11 @@ public void allLoaded() { @Override public void createWorlds() { + // Register the structure listener before any world is created. createWorlds() runs + // before onEnable(), and generating a world also generates its spawn-area chunks, so a + // listener registered in onEnable() would miss those first structures (issue #116). + this.registerListener(new StructureGenerationListener(this)); + String worldName = this.settings.getWorldName().toLowerCase(); if (this.getServer().getWorld(worldName) == null) diff --git a/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java b/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java index 4d7adc3..0a51aba 100644 --- a/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java +++ b/src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.java @@ -57,9 +57,8 @@ public StructureGenerationListener(CaveBlock addon) { */ @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)) { + if (!isCaveOverworld(event.getWorld())) { return; } String structureKey = event.getStructure().getKey().getKey(); @@ -81,7 +80,7 @@ public void onStructureSpawn(AsyncStructureSpawnEvent event) { */ @EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true) public void onStructuresLocate(StructuresLocateEvent event) { - if (!addon.inWorld(event.getWorld())) { + if (!isCaveOverworld(event.getWorld())) { return; } List targets = event.getStructures(); @@ -100,6 +99,24 @@ public void onStructuresLocate(StructuresLocateEvent event) { } } + /** + * @param world a world + * @return {@code true} if {@code world} is the CaveBlock overworld. + * + *

Matches on the configured world name rather than {@link CaveBlock#inWorld(World)} + * because structures in the spawn area are generated during {@code createWorlds()}, + * before the addon's island-world reference is assigned. At that point + * {@code inWorld()} would compare against a null world and fail, so the first + * structures would slip through (issue #116).

+ */ + private boolean isCaveOverworld(World world) { + if (world.getEnvironment() != World.Environment.NORMAL) { + return false; + } + String configuredName = addon.getSettings().getWorldName(); + return configuredName != null && world.getName().equalsIgnoreCase(configuredName); + } + /** * @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 417070c..1642dbe 100644 --- a/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java +++ b/src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java @@ -50,7 +50,8 @@ public void setUp() { 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(settings.getWorldName()).thenReturn("caveblock_world"); + lenient().when(world.getName()).thenReturn("caveblock_world"); lenient().when(world.getEnvironment()).thenReturn(World.Environment.NORMAL); listener = new StructureGenerationListener(addon); } @@ -92,7 +93,7 @@ void testUnlistedStructureGeneratesNormally() { @Test void testStructureOutsideCaveBlockWorldIsIgnored() { - when(addon.inWorld(world)).thenReturn(false); + when(world.getName()).thenReturn("some_other_world"); AsyncStructureSpawnEvent e = event("ancient_city"); listener.onStructureSpawn(e); verify(e, never()).setCancelled(true); @@ -156,7 +157,7 @@ void testLocateAllEnabledIsUntouched() { @Test void testLocateOutsideCaveBlockWorldIsIgnored() { - when(addon.inWorld(world)).thenReturn(false); + when(world.getName()).thenReturn("some_other_world"); StructuresLocateEvent e = locateEvent("trial_chambers"); listener.onStructuresLocate(e); verify(e, never()).setCancelled(true);