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
3 changes: 2 additions & 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 Expand Up @@ -226,6 +226,7 @@
<include>**/*Test??.java</include>
</includes>
<argLine>
@{argLine}
--add-opens java.base/java.lang=ALL-UNNAMED
--add-opens java.base/java.math=ALL-UNNAMED
--add-opens java.base/java.io=ALL-UNNAMED
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
116 changes: 116 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,38 @@ 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("Overworld cave density. Vanilla generates a dense 1.18+ cave network")
@ConfigComment("(cheese and spaghetti caves) which on a solid cave world can feel like")
@ConfigComment("'nothing but passageways'. This re-solidifies a fraction of that cave air")
@ConfigComment("after generation, using a low-frequency noise field so whole regions close")
@ConfigComment("up (leaving separate chambers) rather than punching random single holes.")
@ConfigComment("0.0 = keep every vanilla cave (densest, the original behaviour).")
@ConfigComment("1.0 = fill nearly all caves (almost solid). Try 0.4 - 0.6 to thin them out.")
@ConfigComment("Underground biomes, ores, decorations and structures are kept either way.")
@ConfigComment("Only affects newly generated chunks.")
@ConfigEntry(path = "world.overworld-cave-fill", since = "1.23.0")
private double overworldCaveFill = 0.0;

@ConfigComment("")
@ConfigComment("Generate vanilla carver caves (big ravines and long round tunnels) in the")
@ConfigComment("overworld. These stack on top of the noise caves above. Set to false to")
@ConfigComment("remove the ravines and wide tunnels while keeping the noise caves.")
@ConfigEntry(path = "world.overworld-carvers", needsReset = true, since = "1.23.0")
private boolean overworldCarvers = true;

@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 +1230,89 @@ 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;
}


/**
* Fraction of overworld vanilla cave air to re-solidify after generation to
* thin the cave network. {@code 0.0} keeps every cave, {@code 1.0} fills
* nearly all of them. Clamped to the [0, 1] range on read.
* @return the overworld cave fill ratio.
*/
public double getOverworldCaveFill()
{
return Math.max(0.0, Math.min(1.0, overworldCaveFill));
}


/**
* Sets the overworld cave fill ratio.
* @param overworldCaveFill the overworld cave fill ratio.
*/
public void setOverworldCaveFill(double overworldCaveFill)
{
this.overworldCaveFill = overworldCaveFill;
}


/**
* Whether vanilla carver caves (ravines and long round tunnels) generate in
* the overworld.
* @return {@code true} if carver caves generate.
*/
public boolean isOverworldCarvers()
{
return overworldCarvers;
}


/**
* Sets whether vanilla carver caves generate in the overworld.
* @param overworldCarvers whether carver caves generate.
*/
public void setOverworldCarvers(boolean overworldCarvers)
{
this.overworldCarvers = overworldCarvers;
}


/**
* 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
Expand Up @@ -126,7 +126,7 @@ public boolean shouldGenerateSurface() {
*/
@Override
public boolean shouldGenerateCaves() {
return this.environment == World.Environment.NORMAL;
return this.environment == World.Environment.NORMAL && this.settings.isOverworldCarvers();
}

/**
Expand Down Expand Up @@ -251,9 +251,18 @@ public void generateSurface(WorldInfo worldInfo, Random random, int chunkX, int
// Cap every column: replace sky air and surface water with stone.
// CAVE_AIR (vanilla cave pockets) marks the underground boundary and is left alone.
final Material fillMaterial = settings.getNormalMainBlock();
// Optional density control: re-solidify a fraction of the vanilla caves
// so the overworld is not "nothing but passageways" (issue #111).
final double caveFill = settings.getOverworldCaveFill();
final NoiseCaveGenerator fillField = caveFill > 0 ? getCaveGenerator(worldInfo.getSeed()) : null;
for (int x = 0; x < 16; x++) {
final int worldX = (chunkX << 4) + x;
for (int z = 0; z < 16; z++) {
capColumn(chunkData, x, z, minHeight, maxHeight, fillMaterial);
if (fillField != null) {
fillCaves(chunkData, x, z, worldX, (chunkZ << 4) + z, minHeight, maxHeight,
fillMaterial, fillField, caveFill);
}
}
}
// Roof at the very top of the world
Expand Down Expand Up @@ -345,6 +354,43 @@ private void capColumn(ChunkData chunkData, int x, int z, int minHeight, int max
}
}

/**
* Re-solidifies a fraction of the vanilla overworld cave air to thin the cave
* network. Runs <b>after</b> {@link #capColumn} has already sealed this column's
* sky, so any remaining {@code AIR} or {@code CAVE_AIR} below the cap is genuine
* underground cave.
*
* <p>Both air types must be matched: the vanilla 1.18+ <i>noise</i> caves (the
* dense "cheese and spaghetti" network) are placed as plain {@code AIR}, while
* the older carvers use {@code CAVE_AIR}. Matching only {@code CAVE_AIR} would
* miss the noise caves entirely — which are exactly the passageways this setting
* is meant to thin. A low-frequency noise field decides which blocks to fill, so
* caves close in broad connected patches rather than as a random speckle, and the
* decision is deterministic for the world seed.</p>
*
* @param chunkData chunk being generated
* @param x block X within chunk (0-15)
* @param z block Z within chunk (0-15)
* @param worldX absolute world X of this column
* @param worldZ absolute world Z of this column
* @param minHeight world min height
* @param maxHeight world max height (roof reserved at maxHeight-1)
* @param fillMaterial material used to fill caves (normally the main block)
* @param fillField noise field seeded from the world seed
* @param ratio fraction to fill; a block is filled when its field value is below this
*/
private void fillCaves(ChunkData chunkData, int x, int z, int worldX, int worldZ, int minHeight, int maxHeight,
Material fillMaterial, NoiseCaveGenerator fillField, double ratio) {
// Leave the floor (minHeight, set by generateBedrock) and roof (maxHeight-1) alone.
for (int y = maxHeight - 2; y > minHeight; y--) {
Material type = chunkData.getType(x, y, z);
if ((type == Material.AIR || type == Material.CAVE_AIR)
&& fillField.fillField(worldX, y, worldZ) < ratio) {
chunkData.setBlock(x, y, z, fillMaterial);
}
}
}

private Material getGroundRoofMaterial(World.Environment env) {
return switch (env) {
case NETHER -> settings.isNetherRoof() ? Material.BEDROCK : settings.getNetherMainBlock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ public class NoiseCaveGenerator {
private static final double CHEESE_THRESHOLD = 0.3;
/** How strongly the cheese field eats into the rock. */
private static final double CHEESE_STRENGTH = 0.3;
/** Low frequency for the overworld fill field: closes caves in broad patches. */
private static final double FILL_FREQUENCY = 0.02;

private final NoiseGenerator noiseGen1;
private final NoiseGenerator noiseGen2;
Expand Down Expand Up @@ -94,4 +96,21 @@ public double noise(double x, double y, double z) {
public boolean isCave(double x, double y, double z, double threshold) {
return noise(x, y, z) < threshold;
}

/**
* A coherent [0, 1] field used to thin an existing cave network. Because the
* frequency is low, nearby blocks share similar values, so filling every
* block whose value falls below a ratio closes caves in broad connected
* patches rather than as a random speckle. Deterministic for the seed.
*
* @param x world X
* @param y world Y
* @param z world Z
* @return a value in the range [0, 1]
*/
public double fillField(double x, double y, double z) {
// noise() returns roughly [-1, 1]; remap to [0, 1] and clamp for safety.
double v = (noiseGen2.noise(x * FILL_FREQUENCY, y * FILL_FREQUENCY, z * FILL_FREQUENCY) + 1.0) / 2.0;
return Math.max(0.0, Math.min(1.0, v));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
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
* queries the event's world for environment and ownership checks but performs
* no world or block mutation, which is what makes it safe to run async.</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();
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;
}
// Accept hyphen or underscore separators and any casing on both sides.
String normalizedKey = normalize(structureKey);
for (Map.Entry<String, Boolean> entry : structures.entrySet()) {
if (normalize(entry.getKey()).equals(normalizedKey) && Boolean.FALSE.equals(entry.getValue())) {
return true;
}
}
return false;
}

private String normalize(String key) {
return key.toLowerCase(Locale.ROOT).replace('-', '_');
}
}
33 changes: 33 additions & 0 deletions src/main/resources/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,39 @@ 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
#
# Overworld cave density. Vanilla generates a dense 1.18+ cave network (cheese and
# spaghetti caves) which on a solid cave world can feel like 'nothing but passageways'.
# This re-solidifies a fraction of that cave air after generation, using a low-frequency
# noise field so whole regions close up (leaving separate chambers) rather than punching
# random single holes.
# 0.0 = keep every vanilla cave (densest, the original behaviour)
# 1.0 = fill nearly all caves (almost solid)
# Try 0.4 - 0.6 to thin them out. Underground biomes, ores, decorations and structures
# are kept either way. Only affects newly generated chunks. Added since 1.23.0.
overworld-cave-fill: 0.0
#
# Generate vanilla carver caves (big ravines and long round tunnels) in the overworld.
# These stack on top of the noise caves above. Set to false to remove the ravines and
# wide tunnels while keeping the noise caves. Added since 1.23.0.
# /!\ 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.
overworld-carvers: 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
Loading
Loading