Complete phase 4 hydrology expansion

This commit is contained in:
George
2026-06-11 18:11:42 +01:00
parent 5331d5b207
commit 38ee028651
7 changed files with 365 additions and 5 deletions
@@ -291,7 +291,7 @@ public class LivingWorldMod {
RegionCoordinate spawnCoord = RegionCoordinate.fromBlock(
dimId, (int) event.getX(), (int) event.getZ(),
LivingWorldConstants.DEFAULT_REGION_SIZE_CHUNKS);
if (bootstrap.isDeadZone(spawnCoord)
if (bootstrap.isAquaticSpawnSuppressed(spawnCoord)
&& (event.getEntity() instanceof AbstractFish
|| event.getEntity() instanceof Squid
|| event.getEntity() instanceof Dolphin)) {
@@ -563,7 +563,7 @@ public class LivingWorldMod {
// -- Pre-compute tidal current for this tick --
ServerLevel overworld = minecraftServer.overworld();
long dayTime = overworld.getDayTime();
int seaLevel = overworld.getSeaLevel();
int seaLevel = bootstrap.getDynamicSeaLevel();
int moonPhase = overworld.getMoonPhase();
double springNeap = 1.0 + 0.40 * Math.cos(moonPhase * Math.PI / 4.0);
// Tidal current magnitude = derivative of tide height (max at mid-tide, zero at high/low)
@@ -696,7 +696,7 @@ public class LivingWorldMod {
if (minecraftServer == null) return;
ServerLevel overworld = minecraftServer.overworld();
long dayTime = overworld.getDayTime();
int seaLevel = overworld.getSeaLevel();
int seaLevel = bootstrap.getDynamicSeaLevel();
// Moon phase (0=full, 4=new); full moon = maximum spring tides
int moonPhase = overworld.getMoonPhase();
@@ -140,6 +140,11 @@ public final class LivingWorldBootstrap {
private record GeyserSite(int cycleTick, boolean active) {}
private final Map<RegionCoordinate, GeyserSite> geyserSites = new HashMap<>();
private int geologicalActivityTick = 0;
private final Map<RegionCoordinate, Integer> dryCycles = new HashMap<>();
private final Map<RegionCoordinate, Double> previousEffectiveTemperature = new HashMap<>();
private final Map<RegionCoordinate, Integer> blizzardHistory = new HashMap<>();
private int hydrologyCycleTick = 0;
private int dynamicSeaLevel = 62;
private PlatformAdapter platformAdapter;
private Path worldSaveDirectory;
@@ -273,7 +278,10 @@ public final class LivingWorldBootstrap {
*/
/** Sets the ecosystem tuning loaded from the server config. Must be called before onServerStarting(). */
public void setEcosystemTuning(EcosystemTuning tuning) {
if (tuning != null) ecosystemTuning = tuning;
if (tuning != null) {
ecosystemTuning = tuning;
dynamicSeaLevel = tuning.getSeaLevel();
}
}
public void onServerStopping() {
@@ -309,6 +317,11 @@ public final class LivingWorldBootstrap {
volcanicActivityTick = 0;
geyserSites.clear();
geologicalActivityTick = 0;
dryCycles.clear();
previousEffectiveTemperature.clear();
blizzardHistory.clear();
hydrologyCycleTick = 0;
dynamicSeaLevel = ecosystemTuning.getSeaLevel();
simSpeedMultiplier = 1;
serverReady = false;
LivingWorldLogger.info(
@@ -474,6 +487,147 @@ public final class LivingWorldBootstrap {
if (++geologicalActivityTick % 4 == 0) {
applyGeologicalActivity();
}
applyHydrologyExpansion();
}
private void applyHydrologyExpansion() {
Collection<Region> active = regionManager.getActiveRegions();
if (active.isEmpty() || worldEffectsModule == null) return;
hydrologyCycleTick++;
double warming = climateTracker.getWarmingLevel();
int[][] offsets = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
Map<RegionCoordinate, Region> byCoord = new HashMap<>();
for (Region region : active) byCoord.put(region.getCoordinate(), region);
double totalRain = 0.0;
int rainSamples = 0;
for (Region region : active) {
RegionCoordinate coord = region.getCoordinate();
AtmosphereRegionData atmosphere = region.getModuleData()
.get(AtmosphereModule.MODULE_ID, AtmosphereRegionData.class).orElse(null);
WaterRegionData water = region.getModuleData()
.get(WaterModule.MODULE_ID, WaterRegionData.class).orElse(null);
RecoveryRegionData recovery = region.getModuleData()
.get(RecoveryModule.MODULE_ID, RecoveryRegionData.class).orElse(null);
if (atmosphere == null || water == null || recovery == null) continue;
totalRain += atmosphere.getRainLevel() * 100.0;
rainSamples++;
double elevation = regionElevations.getOrDefault(coord, 64.0);
double effectiveTemperature = regionTemperatures.getOrDefault(coord, 0.8f) + warming * 0.2;
double previousTemperature = previousEffectiveTemperature.getOrDefault(
coord, effectiveTemperature);
previousEffectiveTemperature.put(coord, effectiveTemperature);
if (previousTemperature < 0.15 && effectiveTemperature >= 0.15) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.ICE_MELTS, coord, 0.8));
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.SNOW_MELTS, coord, 0.8));
groundwaterLevel.merge(coord, 12.0, Double::sum);
for (int[] offset : offsets) {
RegionCoordinate neighbour = new RegionCoordinate(
coord.dimensionId(), coord.x() + offset[0], coord.z() + offset[1]);
Double neighbourElevation = regionElevations.get(neighbour);
if (neighbourElevation != null && neighbourElevation < elevation - 3.0) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.GROUND_SPRING_EMERGES, neighbour, 0.7));
}
}
}
int dry = atmosphere.getRainLevel() < 0.15
? dryCycles.merge(coord, 1, Integer::sum) : 0;
if (dry == 0) dryCycles.remove(coord);
if (dry >= 20) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.RIVERBED_DRIES, coord,
Math.min(1.0, (dry - 15) / 30.0)));
groundwaterLevel.compute(coord,
(ignored, level) -> Math.max(0, (level == null ? 0.0 : level) - 2.0));
}
boolean lowSuccession = recovery.getSuccessionStage() == SuccessionStage.BARREN
|| recovery.getSuccessionStage() == SuccessionStage.SPARSE_GRASS;
double maxSlope = 0.0;
RegionCoordinate lowestNeighbour = null;
for (int[] offset : offsets) {
RegionCoordinate neighbour = new RegionCoordinate(
coord.dimensionId(), coord.x() + offset[0], coord.z() + offset[1]);
Double neighbourElevation = regionElevations.get(neighbour);
if (neighbourElevation != null && elevation - neighbourElevation > maxSlope) {
maxSlope = elevation - neighbourElevation;
lowestNeighbour = neighbour;
}
}
if (atmosphere.getRainLevel() > 0.80 && lowSuccession && maxSlope > 10.0
&& windRandom.nextDouble() < 0.02
&& !isClimateEventActive(coord, ClimateEventType.FLASH_FLOOD)) {
ClimateEvent flood = new ClimateEvent(
ClimateEventType.FLASH_FLOOD, coord,
simulationManager.getSimulationTickCounter(), 0.8);
activeClimateEvents.add(flood);
pendingEventMessages.add("[LW] Flash flood at region ("
+ coord.x() + "," + coord.z() + ").");
}
double flow = riverFlowIntensity.getOrDefault(coord, 0.0);
if (flow > 20.0 && lowestNeighbour != null) {
if (oceanicRegions.contains(lowestNeighbour)) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.SEDIMENT_DEPOSIT, lowestNeighbour,
Math.min(1.0, flow / 100.0)));
pendingElevationResample.add(lowestNeighbour);
}
if (maxSlope > 10.0) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.PLUNGE_POOL_DEEPENS, lowestNeighbour,
Math.min(1.0, flow / 100.0)));
}
}
if (isClimateEventActive(coord, ClimateEventType.BLIZZARD)) {
blizzardHistory.merge(coord, 1, Integer::sum);
}
int snowHistory = blizzardHistory.getOrDefault(coord, 0);
if (effectiveTemperature < 0.0 && elevation > 90.0 && snowHistory >= 10
&& hydrologyCycleTick % 50 == 0) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.GLACIER_ADVANCE, coord, 0.7));
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.GLACIER_POLISH, coord, 0.7));
}
if (isClimateEventActive(coord, ClimateEventType.ACID_RAIN) && flow > 10.0
&& lowestNeighbour != null && oceanicRegions.contains(lowestNeighbour)) {
Region downstream = byCoord.get(lowestNeighbour);
if (downstream != null) {
PollutionRegionData downstreamPollution = downstream.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class).orElse(null);
if (downstreamPollution != null) {
double spike = geothermalRegions.contains(coord) ? 0.2 : 1.0;
downstreamPollution.addPollution(0, 0, spike);
downstream.getModuleData().put(PollutionModule.MODULE_ID, downstreamPollution);
regionManager.markDirty(downstream);
}
}
}
}
if (hydrologyCycleTick % 500 == 0 && rainSamples > 0) {
double averageRain = totalRain / rainSamples;
int baseline = ecosystemTuning.getSeaLevel();
int oldSeaLevel = dynamicSeaLevel;
if (averageRain > 65.0 && dynamicSeaLevel < baseline + 3) dynamicSeaLevel++;
if (averageRain < 15.0 && dynamicSeaLevel > baseline - 3) dynamicSeaLevel--;
if (oldSeaLevel != dynamicSeaLevel) {
pendingEventMessages.add("[LW] Sea level changed to Y=" + dynamicSeaLevel + ".");
}
}
}
public int getDynamicSeaLevel() {
return dynamicSeaLevel;
}
private void applyGeologicalActivity() {
@@ -983,6 +1137,15 @@ public final class LivingWorldBootstrap {
return coord != null && deadZoneRegions.contains(coord);
}
public boolean isAquaticSpawnSuppressed(RegionCoordinate coord) {
if (coord == null || deadZoneRegions.contains(coord)) return coord != null;
return regionManager != null && regionManager.resolve(coord)
.flatMap(region -> region.getModuleData()
.get(PollutionModule.MODULE_ID, PollutionRegionData.class))
.map(pollution -> pollution.getWaterPollution() > 50.0)
.orElse(false);
}
/** Returns whether a region is known to be oceanic. */
public boolean isOceanicRegion(RegionCoordinate coord) {
return coord != null && oceanicRegions.contains(coord);
@@ -1795,6 +1958,13 @@ public final class LivingWorldBootstrap {
}
if (ev.getTicksActive() >= 2) shouldResolve = true;
}
case FLASH_FLOOD -> {
if (worldEffectsModule != null) {
worldEffectsModule.queueEffect(new WorldEffectRequest(
WorldEffectType.FLASH_FLOOD, coord, ev.getSeverity()));
}
if (ev.getTicksActive() >= 5) shouldResolve = true;
}
}
}
@@ -12,7 +12,8 @@ public enum ClimateEventType {
SANDSTORM("Sandstorm", "Dry high winds stripping plants and depositing sand"),
LIGHTNING_STORM("Lightning Storm", "Dry thunderstorm producing isolated ignition strikes"),
EARTHQUAKE("Earthquake", "Seismic rupture opening cracks and unstable slopes"),
SINKHOLE("Sinkhole", "Groundwater-driven collapse into a shallow cave");
SINKHOLE("Sinkhole", "Groundwater-driven collapse into a shallow cave"),
FLASH_FLOOD("Flash Flood", "Rapid runoff surging across barren steep terrain");
private final String displayName;
private final String description;
@@ -43,6 +43,7 @@ public final class EcosystemTuning {
private double corridorBoostMultiplier = 3.5;
/** Seeds blocked per unit of pollution score in the target region. */
private double seedPollutionBlock = 0.015;
private int seaLevel = 62;
public EcosystemTuning() {}
@@ -91,4 +92,7 @@ public final class EcosystemTuning {
public void setSeedEmissionRate(double v) { seedEmissionRate = v; }
public void setCorridorBoostMultiplier(double v) { corridorBoostMultiplier = v; }
public void setSeedPollutionBlock(double v) { seedPollutionBlock = v; }
public int getSeaLevel() { return seaLevel; }
public void setSeaLevel(int seaLevel) { this.seaLevel = Math.max(1, Math.min(320, seaLevel)); }
}
@@ -166,4 +166,25 @@ public enum WorldEffectType {
/** A cooling volcanic roof collapses to reveal a lava-adjacent cavity. */
LAVA_TUBE_COLLAPSE,
/** A temporary surge places water along a low terrain path. */
FLASH_FLOOD,
/** Seasonal thaw converts exposed ice back into water. */
ICE_MELTS,
/** Sustained drought drains exposed river sources and hardens the bed. */
RIVERBED_DRIES,
/** River sediment accumulates below sea level at an ocean mouth. */
SEDIMENT_DEPOSIT,
/** A glacier toe pushes loose rock downhill. */
GLACIER_ADVANCE,
/** Moving ice leaves polished andesite behind. */
GLACIER_POLISH,
/** Falling river water deepens its impact basin. */
PLUNGE_POOL_DEEPENS,
}
@@ -46,6 +46,7 @@ public final class NeoForgeModConfig {
private static final ModConfigSpec.DoubleValue SEED_EMISSION_RATE;
private static final ModConfigSpec.DoubleValue CORRIDOR_BOOST_MULTIPLIER;
private static final ModConfigSpec.DoubleValue SEED_POLLUTION_BLOCK;
private static final ModConfigSpec.IntValue SEA_LEVEL;
static {
ModConfigSpec.Builder b = new ModConfigSpec.Builder();
@@ -65,6 +66,12 @@ public final class NeoForgeModConfig {
.defineInRange("wind_boost", 0.5, 0.0, 2.0);
b.pop();
b.comment("Long-timescale hydrology").push("hydrology");
SEA_LEVEL = b
.comment("Baseline simulated sea level. Long climate cycles may drift this by +/-3 blocks.")
.defineInRange("sea_level", 62, 1, 320);
b.pop();
b.comment("Vegetation growth rates (per soil-quality unit above threshold per tick)").push("vegetation");
GRASS_GROWTH_RATE = b.comment("Grass growth rate. Default: 0.06")
.defineInRange("grass_growth", 0.06, 0.001, 2.0);
@@ -134,6 +141,7 @@ public final class NeoForgeModConfig {
t.setSeedEmissionRate(SEED_EMISSION_RATE.get());
t.setCorridorBoostMultiplier(CORRIDOR_BOOST_MULTIPLIER.get());
t.setSeedPollutionBlock(SEED_POLLUTION_BLOCK.get());
t.setSeaLevel(SEA_LEVEL.get());
return t;
}
}
@@ -44,6 +44,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
private final Random random = new Random();
private record TimedWater(BlockPos pos, long expiresAt) {}
private final Map<ResourceKey<Level>, List<TimedWater>> geyserWater = new HashMap<>();
private final Map<ResourceKey<Level>, List<TimedWater>> floodWater = new HashMap<>();
public NeoForgeWorldEffectExecutor(Supplier<MinecraftServer> serverSupplier) {
this.serverSupplier = serverSupplier;
@@ -65,6 +66,7 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
int baseX = request.region().x() * REGION_BLOCKS;
int baseZ = request.region().z() * REGION_BLOCKS;
cleanExpiredGeyserWater(level, dimensionKey);
cleanExpiredFloodWater(level, dimensionKey);
switch (request.type()) {
case GRASS_DEGRADES_TO_DIRT ->
@@ -134,6 +136,20 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
landslide(level, baseX, baseZ, request.intensity());
case LAVA_TUBE_COLLAPSE ->
collapseLavaTube(level, baseX, baseZ, request.intensity());
case FLASH_FLOOD ->
flashFlood(level, dimensionKey, baseX, baseZ, request.intensity());
case ICE_MELTS ->
meltIce(level, baseX, baseZ, request.intensity());
case RIVERBED_DRIES ->
dryRiverbed(level, baseX, baseZ, request.intensity());
case SEDIMENT_DEPOSIT ->
depositSediment(level, baseX, baseZ, request.intensity());
case GLACIER_ADVANCE ->
advanceGlacier(level, baseX, baseZ, request.intensity());
case GLACIER_POLISH ->
polishGlacierRock(level, baseX, baseZ, request.intensity());
case PLUNGE_POOL_DEEPENS ->
deepenPlungePool(level, baseX, baseZ, request.intensity());
}
}
@@ -991,6 +1007,146 @@ public final class NeoForgeWorldEffectExecutor implements WorldEffectConsumer {
|| state.is(Blocks.BASALT);
}
private void cleanExpiredFloodWater(ServerLevel level, ResourceKey<Level> dimensionKey) {
List<TimedWater> entries = floodWater.get(dimensionKey);
if (entries == null) return;
long now = level.getGameTime();
entries.removeIf(entry -> {
if (entry.expiresAt() > now) return false;
if (level.isLoaded(entry.pos()) && level.getBlockState(entry.pos()).is(Blocks.WATER)) {
level.setBlock(entry.pos(), Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
}
return true;
});
if (entries.isEmpty()) floodWater.remove(dimensionKey);
}
private void flashFlood(ServerLevel level, ResourceKey<Level> dimensionKey,
int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.35) return;
List<TimedWater> entries = floodWater.computeIfAbsent(dimensionKey, ignored -> new ArrayList<>());
BlockPos lowest = null;
for (int i = 0; i < 10; i++) {
BlockPos sample = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS));
if (sample != null && (lowest == null || sample.getY() < lowest.getY())) lowest = sample;
}
if (lowest == null) return;
int writes = 0;
for (Direction direction : Direction.Plane.HORIZONTAL) {
BlockPos pos = lowest.relative(direction).above();
if (writes >= 5) break;
if (level.isLoaded(pos) && level.getBlockState(pos).isAir()) {
level.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
entries.add(new TimedWater(pos, level.getGameTime() + 6000));
writes++;
}
}
}
private void meltIce(ServerLevel level, int baseX, int baseZ, double intensity) {
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
BlockPos pos = new BlockPos(x, y, z);
if (level.isLoaded(pos) && level.getBlockState(pos).is(Blocks.ICE)) {
level.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void dryRiverbed(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.25) return;
for (int i = 0; i < Math.max(2, (int) (intensity * 8)); i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int y = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
BlockPos pos = new BlockPos(x, y, z);
if (!level.isLoaded(pos) || !level.getBlockState(pos).is(Blocks.WATER)
|| !level.getFluidState(pos).isSource()) continue;
BlockPos below = pos.below();
var belowState = level.getBlockState(below);
if (belowState.is(Blocks.SAND) || belowState.is(Blocks.DIRT)
|| belowState.is(Blocks.GRAVEL) || belowState.is(Blocks.STONE)) {
level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
level.setBlock(below, random.nextBoolean() ? Blocks.COARSE_DIRT.defaultBlockState()
: Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void depositSediment(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.25) return;
for (int i = 0; i < Math.max(1, (int) (intensity * 5)); i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int y = level.getHeight(Heightmap.Types.OCEAN_FLOOR, x, z);
if (y >= level.getSeaLevel()) continue;
BlockPos pos = new BlockPos(x, y, z);
if (level.isLoaded(pos) && level.getFluidState(pos).is(FluidTags.WATER)) {
level.setBlock(pos, random.nextBoolean() ? Blocks.SAND.defaultBlockState()
: Blocks.GRAVEL.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void advanceGlacier(ServerLevel level, int baseX, int baseZ, double intensity) {
BlockPos snow = null;
BlockPos low = null;
for (int i = 0; i < 10; i++) {
BlockPos sample = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS));
if (sample == null) continue;
if ((level.getBlockState(sample).is(Blocks.SNOW_BLOCK)
|| level.getBlockState(sample).is(Blocks.ICE))
&& (snow == null || sample.getY() < snow.getY())) snow = sample;
if (low == null || sample.getY() < low.getY()) low = sample;
}
if (snow == null || low == null) return;
int dx = Integer.compare(low.getX(), snow.getX());
int dz = Integer.compare(low.getZ(), snow.getZ());
for (Direction direction : Direction.Plane.HORIZONTAL) {
BlockPos source = snow.relative(direction);
var sourceState = level.getBlockState(source);
if (!sourceState.is(Blocks.GRAVEL) && !sourceState.is(Blocks.STONE)) continue;
BlockPos target = source.offset(dx, 0, dz);
if (level.isLoaded(target) && level.getBlockState(target).isAir()) {
level.setBlock(source, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL);
level.setBlock(target, sourceState, Block.UPDATE_ALL);
return;
}
}
}
private void polishGlacierRock(ServerLevel level, int baseX, int baseZ, double intensity) {
for (int i = 0; i < 5; i++) {
BlockPos pos = surfaceAt(level, baseX + random.nextInt(REGION_BLOCKS),
baseZ + random.nextInt(REGION_BLOCKS));
if (pos != null && (level.getBlockState(pos).is(Blocks.STONE)
|| level.getBlockState(pos).is(Blocks.ANDESITE))) {
level.setBlock(pos, Blocks.POLISHED_ANDESITE.defaultBlockState(), Block.UPDATE_ALL);
}
}
}
private void deepenPlungePool(ServerLevel level, int baseX, int baseZ, double intensity) {
if (random.nextDouble() > 0.25) return;
for (int i = 0; i < 8; i++) {
int x = baseX + random.nextInt(REGION_BLOCKS);
int z = baseZ + random.nextInt(REGION_BLOCKS);
int surfaceY = level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z) - 1;
BlockPos water = new BlockPos(x, surfaceY, z);
if (!level.isLoaded(water) || !level.getFluidState(water).is(FluidTags.WATER)) continue;
BlockPos base = water.below();
var state = level.getBlockState(base);
if (state.is(Blocks.STONE) || state.is(Blocks.GRAVEL) || state.is(Blocks.SAND)) {
level.setBlock(base, Blocks.WATER.defaultBlockState(), Block.UPDATE_ALL);
return;
}
}
}
private BlockPos surfaceAt(ServerLevel level, int x, int z) {
int y = level.getHeight(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, x, z) - 1;
if (y < level.getMinBuildHeight()) {