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
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
<!-- Revision variable removes warning about dynamic version -->
<revision>${build.version}-SNAPSHOT</revision>
<!-- This allows to change between versions and snapshots. -->
<build.version>1.22.0</build.version>
<build.version>1.23.0</build.version>
<build.number>-LOCAL</build.number>
<sonar.organization>bentobox-world</sonar.organization>
<sonar.host.url>https://sonarcloud.io</sonar.host.url>
Expand Down
4 changes: 3 additions & 1 deletion src/main/java/world/bentobox/caveblock/CaveBlock.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import world.bentobox.caveblock.commands.IslandAboutCommand;
import world.bentobox.caveblock.generators.ChunkGeneratorWorld;
import world.bentobox.caveblock.listeners.CustomHeightLimitations;
import world.bentobox.caveblock.listeners.StructureGenerationListener;


public class CaveBlock extends GameModeAddon
Expand Down Expand Up @@ -67,8 +68,9 @@ public void onEnable()
CaveBlock.SKY_WALKER_FLAG.addGameModeAddon(this);
this.getPlugin().getFlagsManager().registerFlag(CaveBlock.SKY_WALKER_FLAG);

// Register listener
// Register listeners
this.registerListener(new CustomHeightLimitations(this));
this.registerListener(new StructureGenerationListener(this));
}


Expand Down
53 changes: 53 additions & 0 deletions src/main/java/world/bentobox/caveblock/Settings.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -159,6 +160,18 @@ public class Settings implements WorldSettings
@ConfigEntry(path = "world.world-depth", needsReset = true)
private int worldDepth = 319;

@ConfigComment("")
@ConfigComment("Vanilla structures that may generate in the overworld cave world.")
@ConfigComment("Set a structure to false to stop it generating; structures not listed here")
@ConfigComment("generate as normal. Use the vanilla structure key, for example:")
@ConfigComment(" ancient_city, trial_chambers, mineshaft, mineshaft_mesa, stronghold,")
@ConfigComment(" mansion, monument, pillager_outpost, ruined_portal, trail_ruins,")
@ConfigComment(" village_plains, desert_pyramid, jungle_pyramid, igloo, swamp_hut")
@ConfigComment("Large structures like Ancient Cities and Trial Chambers can fill or unbalance")
@ConfigComment("a cave world, so they are disabled by default. Only affects the overworld.")
@ConfigEntry(path = "world.structures", since = "1.23.0")
private Map<String, Boolean> generateStructures = defaultStructures();

@ConfigComment("")
@ConfigComment("Make over world roof of bedrock, if false, it will be made from stone.")
@ConfigEntry(path = "world.normal.roof", needsReset = true)
Expand Down Expand Up @@ -1197,6 +1210,46 @@ public int getWorldDepth()
}


/**
* Map of vanilla structure key to whether it may generate in the overworld.
* A structure explicitly mapped to {@code false} is prevented from generating;
* any structure not present in the map generates normally.
* @return the structure generation map.
*/
public Map<String, Boolean> getGenerateStructures()
{
return generateStructures;
}


/**
* Sets the structure generation map.
* @param generateStructures the structure generation map.
*/
public void setGenerateStructures(Map<String, Boolean> generateStructures)
{
this.generateStructures = generateStructures;
}


/**
* Default structure toggles. Large, disruptive structures that tend to fill or
* unbalance a cave world are disabled; the common smaller ones are left on so
* the entry is self-documenting.
* @return an ordered map of structure key to whether it generates.
*/
private static Map<String, Boolean> defaultStructures()
{
Map<String, Boolean> 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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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}.</p>
*
* <p>{@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.</p>
*
* @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();

Check warning on line 54 in src/main/java/world/bentobox/caveblock/listeners/StructureGenerationListener.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_CaveBlock&issues=AZ85DLEAyPZCj_lbKWoo&open=AZ85DLEAyPZCj_lbKWoo&pullRequest=113
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<String, Boolean> structures = addon.getSettings().getGenerateStructures();
if (structures == null || structures.isEmpty()) {
return false;
}
for (Map.Entry<String, Boolean> 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('-', '_');
}
}
16 changes: 16 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {

Check warning on line 30 in src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove redundant visibility modifiers from this test class and its methods.

See more on https://sonarcloud.io/project/issues?id=BentoBoxWorld_CaveBlock&issues=AZ85DLCEyPZCj_lbKWom&open=AZ85DLCEyPZCj_lbKWom&pullRequest=113

@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));

Check warning on line 59 in src/test/java/world/bentobox/caveblock/listeners/StructureGenerationListenerTest.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_CaveBlock&issues=AZ85DLCEyPZCj_lbKWon&open=AZ85DLCEyPZCj_lbKWon&pullRequest=113
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);
}
}
Loading