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
34 changes: 34 additions & 0 deletions src/main/java/world/bentobox/bentobox/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,23 @@ public class Settings implements ConfigObject {
@ConfigEntry(path = "island.obsidian-scooping-lava-tip-duration", since = "3.14.0")
private int obsidianScoopingLavaTipDuration = 5;

/* WORLD */
@ConfigComment("Vanilla structures disabled by default in EVERY BentoBox game mode world")
@ConfigComment("(overworld, nether and end). List the structure keys to stop generating and to")
@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("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<>();

/* WEB */
@ConfigComment("Toggle whether BentoBox can connect to GitHub to get data about updates and addons.")
@ConfigComment("Disabling this will result in the deactivation of the update checker and of some other")
Expand Down Expand Up @@ -1753,4 +1770,21 @@ public void setIslandPurgeLevel(int islandPurgeLevel) {
this.islandPurgeLevel = islandPurgeLevel;
}

/**
* Vanilla structures disabled by default in every BentoBox game mode world.
* @return the list of disabled structure keys, never {@code null}
* @since 3.19.1
*/
public List<String> getDisabledStructures() {
return disabledStructures == null ? new ArrayList<>() : disabledStructures;
}

/**
* @param disabledStructures the disabledStructures to set
* @since 3.19.1
*/
public void setDisabledStructures(List<String> disabledStructures) {
this.disabledStructures = disabledStructures;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -697,4 +697,29 @@ default boolean isDisallowTeamMemberIslands() {
default boolean isTeamsDisabled() {
return false;
}

/**
* Per-world override for vanilla structure generation and searching.
* <p>
* The key is a structure name, e.g. {@code "trial_chambers"} or {@code "ancient_city"}
* (with {@code '-'} or {@code '_'} separators and any case). The value is whether that
* structure should generate:
* <ul>
* <li>{@code false} — disable the structure in this world: it will not generate and is
* removed from structure searches ({@code /locate}, Eyes of Ender, explorer/treasure
* maps, dolphins, villager cartographer trades), which also stops those searches
* scanning to the world border and freezing the server.</li>
* <li>{@code true} — force the structure on in this world even if BentoBox's global
* {@code world.disabled-structures} list disables it.</li>
* </ul>
* A structure that is absent from this map inherits the global
* {@code world.disabled-structures} setting. The default is an empty map, so a game mode
* that does not override this method inherits BentoBox's global configuration entirely.
*
* @return a map of structure key to whether it should generate, never {@code null}
* @since 3.19.1
*/
default Map<String, Boolean> getStructureSettings() {
return Collections.emptyMap();
}
}
156 changes: 156 additions & 0 deletions src/main/java/world/bentobox/bentobox/listeners/StructureListener.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package world.bentobox.bentobox.listeners;

import java.util.List;
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 org.bukkit.generator.structure.Structure;

import io.papermc.paper.event.world.StructuresLocateEvent;
import world.bentobox.bentobox.BentoBox;
import world.bentobox.bentobox.api.addons.GameModeAddon;
import world.bentobox.bentobox.api.configuration.WorldSettings;

/**
* Suppresses vanilla structures in a game mode's worlds, both when they would be
* <em>placed</em> and when they are <em>searched</em> for.
*
* <p>Game modes whose worlds delegate to vanilla generation (e.g. Boxed, SkyGrid,
* CaveBlock) can generate structures that fill or unbalance an island world. The
* {@link org.bukkit.generator.ChunkGenerator} flag can only turn all structures on or
* off, so this listener provides per-structure control driven by BentoBox's global
* {@code world.disabled-structures} list and each world's
* {@link WorldSettings#getStructureSettings()} override.</p>
*
* <p>Suppression has two halves:</p>
* <ul>
* <li>{@link AsyncStructureSpawnEvent} — stops a disabled structure being placed during
* chunk generation. It fires off the main thread, so this handler only reads config
* and inspects the event; it performs no world or block mutation, which is what makes
* it safe to run async.</li>
* <li>{@link StructuresLocateEvent} — fires before any structure search ({@code /locate},
* Eyes of Ender, explorer/treasure maps, dolphins, villager cartographer trades).
* Cancelling the spawn alone leaves the world's placement rules intact, so a search
* for a suppressed structure never succeeds and scans out to the radius cap, freezing
* the main thread. Removing disabled structures from the search — and cancelling when
* nothing enabled remains — skips that scan entirely.</li>
* </ul>
*
* <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)}
* because the spawn chunks generate during {@code createWorlds()}, before the game mode's
* worlds are registered with the world manager.</p>
*
* @author tastybento
* @since 3.19.1
*/
public class StructureListener implements Listener {

private final BentoBox plugin;
private final GameModeAddon gameMode;

/**
* @param plugin BentoBox instance
* @param gameMode the game mode whose worlds this listener guards
*/
public StructureListener(BentoBox plugin, GameModeAddon gameMode) {
this.plugin = plugin;
this.gameMode = gameMode;
}

/**
* Cancels the spawn of a disabled structure in one of this game mode's worlds.
*
* @param event the structure spawn event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onStructureSpawn(AsyncStructureSpawnEvent event) {
if (!isGameModeWorld(event.getWorld())) {
return;
}
if (isDisabled(event.getStructure().getKey().getKey())) {

Check warning on line 77 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=AZ9HWeCO0i6s0SOCReLN&open=AZ9HWeCO0i6s0SOCReLN&pullRequest=3019
event.setCancelled(true);
}
}

/**
* Removes disabled structures from a structure search in one of this game mode's worlds,
* cancelling outright when nothing enabled remains so the expensive scan is skipped.
*
* @param event the structure locate event
*/
@EventHandler(priority = EventPriority.NORMAL, ignoreCancelled = true)
public void onStructuresLocate(StructuresLocateEvent event) {
if (!isGameModeWorld(event.getWorld())) {
return;
}
List<Structure> targets = event.getStructures();
List<Structure> allowed = targets.stream()
.filter(structure -> !isDisabled(structure.getKey().getKey()))

Check warning on line 95 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=AZ9HWeCO0i6s0SOCReLO&open=AZ9HWeCO0i6s0SOCReLO&pullRequest=3019
.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 world a world
* @return {@code true} if {@code world} is this game mode's overworld, nether or end.
*
* <p>Matches on the configured world name rather than the world manager because the
* spawn-area chunks generate during {@code createWorlds()}, before the game mode's worlds
* are registered. At that point a world-manager lookup would not yet know the world and
* the first structures would slip through.</p>
*/
private boolean isGameModeWorld(World world) {
String base = gameMode.getWorldSettings().getWorldName();
if (base == null) {
return false;
}
String name = world.getName();
return name.equalsIgnoreCase(base) || name.equalsIgnoreCase(base + "_nether")
|| name.equalsIgnoreCase(base + "_the_end");
}

/**
* Decides whether a structure is disabled in this game mode. A per-world entry in
* {@link WorldSettings#getStructureSettings()} always wins ({@code false} disables,
* {@code true} force-enables); otherwise the global {@code world.disabled-structures}
* list applies.
*
* @param structureKey the vanilla structure key path, e.g. {@code ancient_city}
* @return {@code true} if this structure should be suppressed
*/
private boolean isDisabled(String structureKey) {
String normalizedKey = normalize(structureKey);
// Per-world override wins: value is whether the structure should generate.
Map<String, Boolean> overrides = gameMode.getWorldSettings().getStructureSettings();
if (overrides != null) {
for (Map.Entry<String, Boolean> entry : overrides.entrySet()) {
if (normalize(entry.getKey()).equals(normalizedKey)) {
return Boolean.FALSE.equals(entry.getValue());
}
}
}
// Otherwise fall back to the global default list.
return plugin.getSettings().getDisabledStructures().stream()
.anyMatch(key -> normalize(key).equals(normalizedKey));
}

private String normalize(String key) {
return key.toLowerCase(Locale.ROOT).replace('-', '_');
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import world.bentobox.bentobox.api.events.addon.AddonEvent;
import world.bentobox.bentobox.commands.BentoBoxCommand;
import world.bentobox.bentobox.database.objects.DataObject;
import world.bentobox.bentobox.listeners.StructureListener;
import world.bentobox.bentobox.util.Util;

/**
Expand Down Expand Up @@ -329,6 +330,11 @@ private void enableAddon(Addon addon) {
try {
// If this is a GameModeAddon create the worlds, register it and load the blueprints
if (addon instanceof GameModeAddon gameMode) {
// Suppress disabled structures before the worlds (and their spawn-area chunks)
// are generated. createWorlds() generates the spawn chunks, so a listener
// registered any later would miss those first structures. Registered through
// registerListener so it is tracked and unregistered when the addon is disabled.
registerListener(gameMode, new StructureListener(plugin, gameMode));
// Create the gameWorlds
gameMode.createWorlds();
plugin.getIWM().addGameMode(gameMode);
Expand Down
16 changes: 16 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,22 @@ island:
# This value is also used for valid nether portal linking between dimension.
# Added since 1.21.0.
safe-spot-search-range: 16
world:
# Vanilla structures disabled by default in EVERY BentoBox game mode world
# (overworld, nether and end). List the structure keys to stop generating and to
# 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.
# 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:
github:
# Toggle whether BentoBox can connect to GitHub to get data about updates and addons.
Expand Down
Loading
Loading