Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions src/main/java/world/bentobox/bentobox/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -535,14 +535,22 @@ public class Settings implements ConfigObject {
@ConfigComment("skip in structure searches (/locate, Eyes of Ender, explorer/treasure maps,")
@ConfigComment("dolphins and villager cartographer trades). Suppressing a structure this way also")
@ConfigComment("prevents those searches scanning to the world border and freezing the server.")
@ConfigComment("Keys may use '-' or '_' and any case, e.g. trial_chambers, ancient-city.")
@ConfigComment("Keys are the vanilla structure ids: tab-complete '/locate structure ' in-game for")
@ConfigComment("the full list ('minecraft:mansion' is listed here as 'mansion'). Keys may use '-'")
@ConfigComment("or '_' and any case, e.g. trial_chambers, ancient-city.")
@ConfigComment("To stop the map searches that most commonly freeze servers with void worlds")
@ConfigComment("(villager cartographer explorer-map trades, dolphins, chest treasure maps),")
@ConfigComment("disable the structures those maps look for:")
@ConfigComment(" disabled-structures:")
@ConfigComment(" - monument")
@ConfigComment(" - mansion")
@ConfigComment(" - trial_chambers")
@ConfigComment(" - buried_treasure")
@ConfigComment("Worlds whose generator places no structures at all (e.g. classic skyblock void")
@ConfigComment("worlds) skip these searches automatically — no entries are needed for them.")
@ConfigComment("A game mode can override this per structure in its own config, both to disable")
@ConfigComment("more structures and to force-enable one that this list disables.")
@ConfigComment("Empty (the default) disables nothing, so behaviour is unchanged.")
@ConfigComment("Example:")
@ConfigComment(" disabled-structures:")
@ConfigComment(" - trial_chambers")
@ConfigComment(" - ancient_city")
@ConfigEntry(path = "world.disabled-structures", since = "3.19.1")
private List<String> disabledStructures = new ArrayList<>();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Random;

import org.bukkit.Location;
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 org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.structure.Structure;

import io.papermc.paper.event.world.StructuresLocateEvent;
Expand Down Expand Up @@ -41,6 +44,14 @@
* nothing enabled remains — skips that scan entirely.</li>
* </ul>
*
* <p>Searches are additionally suppressed, with no configuration required, in worlds whose
* {@link ChunkGenerator} reports {@link ChunkGenerator#shouldGenerateStructures(org.bukkit.generator.WorldInfo, Random, int, int)
* shouldGenerateStructures} {@code false} (e.g. void skyblock worlds): a world that never
* places structures can never satisfy a search, so every search there is the pathological
* scan-to-the-cap case. A per-world {@link WorldSettings#getStructureSettings()} entry of
* {@code true} still force-enables searching for that structure — the escape hatch for
* converted worlds that contain structures generated before the game mode took over.</p>
*
* <p>One instance is registered per {@link GameModeAddon}, immediately before its worlds are
* created, so it is active for the initial spawn-area generation. Worlds are matched by
* configured name rather than {@link world.bentobox.bentobox.managers.IslandWorldManager#inWorld(World)}
Expand Down Expand Up @@ -90,9 +101,10 @@
if (!isGameModeWorld(event.getWorld())) {
return;
}
boolean structurelessWorld = !generatesStructures(event.getWorld(), event.getOrigin());
List<Structure> targets = event.getStructures();
List<Structure> allowed = targets.stream()
.filter(structure -> !isDisabled(structure.getKey().getKey()))
.filter(structure -> !isSuppressed(structure.getKey().getKey(), structurelessWorld))

Check warning on line 107 in src/main/java/world/bentobox/bentobox/listeners/StructureListener.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated method, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_BentoBox&issues=AZ-UtQ3GGwp10Fn5k-Q3&open=AZ-UtQ3GGwp10Fn5k-Q3&pullRequest=3035
.toList();
if (allowed.size() == targets.size()) {
// Nothing disabled in this search — let it run normally.
Expand Down Expand Up @@ -135,6 +147,18 @@
* @return {@code true} if this structure should be suppressed
*/
private boolean isDisabled(String structureKey) {
return isSuppressed(structureKey, false);
}

/**
* As {@link #isDisabled(String)}, with one extra final layer: in a world whose generator
* never places structures, every structure not explicitly force-enabled is suppressed.
*
* @param structureKey the vanilla structure key path, e.g. {@code ancient_city}
* @param structurelessWorld whether the world's generator reports it never places structures
* @return {@code true} if this structure should be suppressed
*/
private boolean isSuppressed(String structureKey, boolean structurelessWorld) {
String normalizedKey = normalize(structureKey);
// Per-world override wins: value is whether the structure should generate.
Map<String, Boolean> overrides = gameMode.getWorldSettings().getStructureSettings();
Expand All @@ -145,9 +169,28 @@
}
}
}
// Otherwise fall back to the global default list.
return plugin.getSettings().getDisabledStructures().stream()
.anyMatch(key -> normalize(key).equals(normalizedKey));
// Then the global default list.
if (plugin.getSettings().getDisabledStructures().stream()
.anyMatch(key -> normalize(key).equals(normalizedKey))) {
return true;
}
// Finally, a world that never places structures can never satisfy a search.
return structurelessWorld;
}

/**
* @param world a game mode world
* @param origin the origin of the structure search
* @return {@code true} if {@code world}'s generator places vanilla structures. A world
* with no custom generator is vanilla-generated, so structures are assumed.
*/
private boolean generatesStructures(World world, Location origin) {
ChunkGenerator generator = world.getGenerator();
if (generator == null) {
return true;
}
return generator.shouldGenerateStructures(world, new Random(world.getSeed()),
origin.getBlockX() >> 4, origin.getBlockZ() >> 4);
}

private String normalize(String key) {
Expand Down
18 changes: 13 additions & 5 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,14 +320,22 @@ world:
# skip in structure searches (/locate, Eyes of Ender, explorer/treasure maps,
# dolphins and villager cartographer trades). Suppressing a structure this way also
# prevents those searches scanning to the world border and freezing the server.
# Keys may use '-' or '_' and any case, e.g. trial_chambers, ancient-city.
# Keys are the vanilla structure ids: tab-complete '/locate structure ' in-game for
# the full list ('minecraft:mansion' is listed here as 'mansion'). Keys may use '-'
# or '_' and any case, e.g. trial_chambers, ancient-city.
# To stop the map searches that most commonly freeze servers with void worlds
# (villager cartographer explorer-map trades, dolphins, chest treasure maps),
# disable the structures those maps look for:
# disabled-structures:
# - monument
# - mansion
# - trial_chambers
# - buried_treasure
# Worlds whose generator places no structures at all (e.g. classic skyblock void
# worlds) skip these searches automatically — no entries are needed for them.
# A game mode can override this per structure in its own config, both to disable
# more structures and to force-enable one that this list disables.
# Empty (the default) disables nothing, so behaviour is unchanged.
# Example:
# disabled-structures:
# - trial_chambers
# - ancient_city
# Added since 3.19.1.
disabled-structures: []
web:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,20 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Random;

import org.bukkit.Location;
import org.bukkit.NamespacedKey;
import org.bukkit.World;
import org.bukkit.event.world.AsyncStructureSpawnEvent;
import org.bukkit.generator.ChunkGenerator;
import org.bukkit.generator.WorldInfo;
import org.bukkit.generator.structure.Structure;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -89,9 +96,21 @@
StructuresLocateEvent e = mock(StructuresLocateEvent.class);
lenient().when(e.getWorld()).thenReturn(world);
lenient().when(e.getStructures()).thenReturn(structures);
lenient().when(e.getOrigin()).thenReturn(mock(Location.class));
return e;
}

/**
* Gives the test world a custom generator whose {@code shouldGenerateStructures}
* reports {@code generates}.
*/
private void withGenerator(boolean generates) {
ChunkGenerator generator = mock(ChunkGenerator.class);
lenient().when(generator.shouldGenerateStructures(any(WorldInfo.class), any(Random.class), anyInt(), anyInt()))
.thenReturn(generates);
lenient().when(world.getGenerator()).thenReturn(generator);
}

// --- Spawn suppression -------------------------------------------------

@Test
Expand Down Expand Up @@ -180,4 +199,53 @@
verify(e, never()).setCancelled(true);
verify(e, never()).setStructures(anyList());
}

// --- Structureless-world locate suppression ----------------------------

@Test
void testLocateInStructurelessWorldIsCancelledWithoutConfig() {
// Generator never places structures — even structures on no disabled list
// (e.g. a cartographer map's monument search) must not be scanned for.
withGenerator(false);
StructuresLocateEvent e = locateEvent("monument", "mansion");
listener.onStructuresLocate(e);
verify(e).setCancelled(true);
verify(e, never()).setStructures(anyList());
}

@Test
void testLocateInStructurelessWorldKeepsForceEnabledStructure() {
// ancient_city is force-enabled by the per-world override, so it survives
// even though the world generates no structures.
withGenerator(false);
StructuresLocateEvent e = locateEvent("monument", "ancient_city");
listener.onStructuresLocate(e);
verify(e, never()).setCancelled(true);
@SuppressWarnings("unchecked")
ArgumentCaptor<List<Structure>> captor = ArgumentCaptor.forClass(List.class);
verify(e).setStructures(captor.capture());
assertEquals(1, captor.getValue().size());
assertEquals("ancient_city", captor.getValue().get(0).getKey().getKey());

Check warning on line 228 in src/test/java/world/bentobox/bentobox/listeners/StructureListenerTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove this call to a deprecated method, it has been marked for removal.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_BentoBox&issues=AZ-UtQ1PGwp10Fn5k-Q2&open=AZ-UtQ1PGwp10Fn5k-Q2&pullRequest=3035
}

@Test
void testLocateWithStructureGeneratingGeneratorIsUntouched() {
// Generator places structures (e.g. SkyGrid, Boxed with allow-structures) —
// unlisted structures search normally.
withGenerator(true);
StructuresLocateEvent e = locateEvent("monument", "village_plains");
listener.onStructuresLocate(e);
verify(e, never()).setCancelled(true);
verify(e, never()).setStructures(anyList());
}

@Test
void testStructurelessWorldDoesNotAffectSpawnSuppression() {
// The generator layer applies to searches only; spawn events for enabled
// structures pass through (they cannot fire in a structureless world anyway).
withGenerator(false);
AsyncStructureSpawnEvent e = spawnEvent("village_plains");
listener.onStructureSpawn(e);
verify(e, never()).setCancelled(true);
}
}
Loading