package org.pepsoft.worldpainter.layers.exporters;
import org.pepsoft.minecraft.Material;
import org.pepsoft.util.MathUtils;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.Tile;
import org.pepsoft.worldpainter.exporting.AbstractLayerExporter;
import org.pepsoft.worldpainter.exporting.Fixup;
import org.pepsoft.worldpainter.exporting.MinecraftWorld;
import org.pepsoft.worldpainter.exporting.SecondPassLayerExporter;
import org.pepsoft.worldpainter.layers.Caves;
import org.pepsoft.worldpainter.layers.Layer;
import org.pepsoft.worldpainter.util.GeometryUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.vecmath.Point3d;
import javax.vecmath.Point3i;
import javax.vecmath.Vector3d;
import java.awt.*;
import java.util.List;
import java.util.Random;
import static org.pepsoft.minecraft.Block.BLOCKS;
import static org.pepsoft.minecraft.Constants.*;
import static org.pepsoft.worldpainter.Constants.TILE_SIZE;
import static org.pepsoft.worldpainter.Constants.TILE_SIZE_BITS;
/**
* Created by Pepijn on 15-1-2017.
*/
public class CavesExporter extends AbstractLayerExporter<Caves> implements SecondPassLayerExporter {
public CavesExporter() {
super(Caves.INSTANCE, new CavesSettings());
}
@Override
public List<Fixup> render(Dimension dimension, Rectangle area, Rectangle exportedArea, MinecraftWorld minecraftWorld) {
CavesSettings settings = (CavesSettings) getSettings();
int minZ = Math.max(settings.getMinimumLevel(), dimension.isBottomless() ? 0 : 1),
maxZForWorld = Math.min(settings.getMaximumLevel(), minecraftWorld.getMaxHeight() - 1);
boolean surfaceBreaking = settings.isSurfaceBreaking();
Random random = new Random();
CaveSettings caveSettings = new CaveSettings();
caveSettings.minZ = minZ;
// Grow the area we will check for spawning caves, such that parts of
// caves which start outside the exported area are still rendered inside
// the exported area
Rectangle spawnArea = (Rectangle) exportedArea.clone();
spawnArea.grow(MAX_CAVE_LENGTH, MAX_CAVE_LENGTH);
// Go tile by tile, so we can quickly check whether the tile even
// exists and contains the layer and if not skip it entirely
int tileX1 = spawnArea.x >> TILE_SIZE_BITS, tileX2 = (spawnArea.x + spawnArea.width - 1) >> TILE_SIZE_BITS;
int tileY1 = spawnArea.y >> TILE_SIZE_BITS, tileY2 = (spawnArea.y + spawnArea.height - 1) >> TILE_SIZE_BITS;
for (int tileX = tileX1; tileX <= tileX2; tileX++) {
for (int tileY = tileY1; tileY <= tileY2; tileY++) {
Tile tile = dimension.getTile(tileX, tileY);
if ((tile == null) || (! tile.hasLayer(Caves.INSTANCE))) {
continue;
}
for (int xInTile = 0; xInTile < TILE_SIZE; xInTile++) {
for (int yInTile = 0; yInTile < TILE_SIZE; yInTile++) {
int x = (tileX << TILE_SIZE_BITS) | xInTile, y = (tileY << TILE_SIZE_BITS) | yInTile;
int value = tile.getLayerValue(Caves.INSTANCE, xInTile, yInTile);
if (value > 0) {
int height = tile.getIntHeight(xInTile, yInTile);
int maxZ = Math.min(maxZForWorld, height - (surfaceBreaking ? 0 : dimension.getTopLayerDepth(x, y, height)));
random.setSeed(dimension.getSeed() + x * 65537 + y);
for (int z = minZ; z <= maxZ; z++) {
if (value > random.nextInt(CAVE_CHANCE)) {
caveSettings.start = new Point3i(x, y, z);
caveSettings.length = MathUtils.clamp(0, (int) ((random.nextGaussian() + 2.0) * (MAX_CAVE_LENGTH / 3.0) + 0.5), MAX_CAVE_LENGTH);
createTunnel(minecraftWorld, dimension, new Random(random.nextLong()), caveSettings);
}
}
}
}
}
}
}
return null;
}
private void createTunnel(MinecraftWorld world, Dimension dimension, Random random, CaveSettings settings) {
Point3d location = new Point3d(settings.start.x, settings.start.y, settings.start.z);
Vector3d direction = getRandomDirection(random);
double length = 0.0, minRadius = settings.minRadius, maxRadius = settings.maxRadius,
radius = (maxRadius + minRadius) / 2.0, radiusDelta = 0.0, radiusChangeSpeed = settings.radiusChangeSpeed;
int maxLength = settings.length, twistiness = settings.twistiness;
if (logger.isTraceEnabled()) {
logger.trace("Creating tunnel @ {},{},{} of length {}; radius: {} - {} (variability: {}); twistiness: {}",
settings.start.x, settings.start.y, settings.start.z, maxLength, settings.minRadius, settings.maxRadius,
radiusChangeSpeed, twistiness);
}
while (length < maxLength) {
if (dimension.getLayerValueAt(Caves.INSTANCE, (int) location.x, (int) location.y) < 1) {
// Don't stray into areas where the layer isn't present at all
return;
}
excavate(world, random, settings, location, radius);
length += direction.length();
location.add(direction);
Vector3d dirChange = getRandomDirection(random);
dirChange.scale(random.nextDouble() / (5 - twistiness));
direction.add(dirChange);
direction.normalize();
if (radiusChangeSpeed > 0.0) {
radius = MathUtils.clamp(minRadius, radius + radiusDelta, maxRadius);
radiusDelta += random.nextDouble() * 2 * radiusChangeSpeed - radiusChangeSpeed;
}
}
}
private void excavate(MinecraftWorld world, Random random, CaveSettings settings, Point3d location, double radius) {
boolean intrudingStone = settings.intrudingStone, roughWalls = settings.roughWalls, removeFloatingBlocks = settings.removeFloatingBlocks;
int minZ = settings.minZ, maxZ = world.getMaxHeight() - 1;
// TODO: change visitFilledSphere so the sphere doesn't have single-block spikes at the x, y, and z axes
GeometryUtil.visitFilledSphere((int) Math.ceil(radius), ((dx, dy, dz, d) -> {
if (d > radius) {
return true;
}
int z = (int) (location.z + dz);
// TODO: efficiently check maxZ per x,y:
if (z >= minZ) {
int x = (int) (location.x + dx);
int y = (int) (location.y + dy);
int existingBlock = world.getBlockTypeAt(x, y, z);
if (existingBlock == BLK_AIR) {
// Already excavated
return true;
}
boolean blockExcavated = false;
if ((roughWalls || intrudingStone) && (radius - d <= 1)) {
// Remember: this is not near the wall of the tunnel; it is
// near the edge of the sphere we're currently excavating,
// so only remove things, don't add them
if (intrudingStone) {
if (((existingBlock != BLK_STONE)
|| (world.getDataAt(x, y, z) == DATA_STONE_STONE))
&& ((! roughWalls)
|| random.nextBoolean())) {
// Treat andesite, etc. as "harder" than regular stone
// so it protrudes slightly into the cave
world.setMaterialAt(x, y, z, Material.AIR);
blockExcavated = true;
}
} else if (random.nextBoolean()) {
world.setMaterialAt(x, y, z, Material.AIR);
blockExcavated = true;
}
} else {
world.setMaterialAt(x, y, z, Material.AIR);
blockExcavated = true;
}
if (blockExcavated && removeFloatingBlocks && (radius - d <= 2)) {
checkForFloatingBlock(world, x - 1, y, z, maxZ);
checkForFloatingBlock(world, x, y - 1, z, maxZ);
checkForFloatingBlock(world, x + 1, y, z, maxZ);
checkForFloatingBlock(world, x, y + 1, z, maxZ);
if (z > 1) {
checkForFloatingBlock(world, x, y, z - 1, maxZ);
}
if (z < maxZ) {
checkForFloatingBlock(world, x, y, z + 1, maxZ);
}
}
}
return true;
}));
}
static void checkForFloatingBlock(MinecraftWorld world, int x, int y, int z, int maxZ) {
int blockType = world.getBlockTypeAt(x, y, z);
if ((blockType == BLK_DIRT) || (blockType == BLK_SAND) || (blockType == BLK_GRAVEL)) {
if (((z > 0) && (! BLOCKS[world.getBlockTypeAt(x, y, z - 1)].solid))
&& ((z < maxZ) && (! BLOCKS[world.getBlockTypeAt(x, y, z + 1)].solid))) {
// The block is only one layer thick
world.setMaterialAt(x, y, z, Material.AIR);
// TODO: this isn't removing nearly all one-block thick dirt. Why?
}
} else if ((blockType != BLK_AIR) && (blockType != BLK_WATER) && (blockType != BLK_STATIONARY_WATER) && (blockType != BLK_LAVA) && (blockType != BLK_STATIONARY_LAVA)) {
if ((! BLOCKS[world.getBlockTypeAt(x - 1, y, z)].solid)
&& (! BLOCKS[world.getBlockTypeAt(x, y - 1, z)].solid)
&& (! BLOCKS[world.getBlockTypeAt(x + 1, y, z)].solid)
&& (! BLOCKS[world.getBlockTypeAt(x, y + 1, z)].solid)
&& ((z > 0) && (! BLOCKS[world.getBlockTypeAt(x, y, z - 1)].solid))
&& ((z < maxZ) && (! BLOCKS[world.getBlockTypeAt(x, y, z + 1)].solid))) {
// The block is floating in the air
// TODO: this does not take leaves into account, which count as an insubstantial block but can be attached to other leaves!
world.setMaterialAt(x, y, z, Material.AIR);
}
}
}
private Vector3d getRandomDirection(Random random) {
double x1 = random.nextDouble() * 2 - 1, x2 = random.nextDouble() * 2 - 1;
while (x1 * x1 + x2 * x2 >= 1) {
x1 = random.nextDouble() * 2 - 1;
x2 = random.nextDouble() * 2 - 1;
}
return new Vector3d(2 * x1 * Math.sqrt(1 - x1 * x1 - x2 * x2),
2 * x2 * Math.sqrt(1 - x1 * x1 - x2 * x2),
1 - 2 * (x1 * x1 + x2 * x2));
}
private static final int MAX_CAVE_LENGTH = 128;
private static final int CAVE_CHANCE = 131072;
private static final Logger logger = LoggerFactory.getLogger(CavesExporter.class);
/**
* Settings for an individual cave.
*/
static class CaveSettings {
Point3i start;
int length, minZ, minRadius = 2, maxRadius = 4, twistiness = 2;
boolean intrudingStone = true, roughWalls, removeFloatingBlocks = true;
double radiusChangeSpeed = 0.2;
}
/**
* Settings for the Caves layer.
*/
public static class CavesSettings implements ExporterSettings {
@Override
public boolean isApplyEverywhere() {
return cavesEverywhereLevel > 0;
}
@Override
public Layer getLayer() {
return Caves.INSTANCE;
}
@Override
public ExporterSettings clone() {
try {
return (ExporterSettings) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
public int getWaterLevel() {
return waterLevel;
}
public void setWaterLevel(int waterLevel) {
this.waterLevel = waterLevel;
}
public int getCavesEverywhereLevel() {
return cavesEverywhereLevel;
}
public void setCavesEverywhereLevel(int cavesEverywhereLevel) {
this.cavesEverywhereLevel = cavesEverywhereLevel;
}
public boolean isFloodWithLava() {
return floodWithLava;
}
public void setFloodWithLava(boolean floodWithLava) {
this.floodWithLava = floodWithLava;
}
public boolean isSurfaceBreaking() {
return surfaceBreaking;
}
public void setSurfaceBreaking(boolean surfaceBreaking) {
this.surfaceBreaking = surfaceBreaking;
}
public boolean isLeaveWater() {
return leaveWater;
}
public void setLeaveWater(boolean leaveWater) {
this.leaveWater = leaveWater;
}
public int getMinimumLevel() {
return minimumLevel;
}
public void setMinimumLevel(int minimumLevel) {
this.minimumLevel = minimumLevel;
}
public int getMaximumLevel() {
return maximumLevel;
}
public void setMaximumLevel(int maximumLevel) {
this.maximumLevel = maximumLevel;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CavesSettings that = (CavesSettings) o;
if (waterLevel != that.waterLevel) return false;
if (cavesEverywhereLevel != that.cavesEverywhereLevel) return false;
if (floodWithLava != that.floodWithLava) return false;
if (surfaceBreaking != that.surfaceBreaking) return false;
if (leaveWater != that.leaveWater) return false;
if (minimumLevel != that.minimumLevel) return false;
return maximumLevel == that.maximumLevel;
}
@Override
public int hashCode() {
int result = waterLevel;
result = 31 * result + cavesEverywhereLevel;
result = 31 * result + (floodWithLava ? 1 : 0);
result = 31 * result + (surfaceBreaking ? 1 : 0);
result = 31 * result + (leaveWater ? 1 : 0);
result = 31 * result + minimumLevel;
result = 31 * result + maximumLevel;
return result;
}
private int waterLevel, cavesEverywhereLevel;
private boolean floodWithLava, surfaceBreaking = true, leaveWater = true;
private int minimumLevel = 8, maximumLevel = Integer.MAX_VALUE;
private static final long serialVersionUID = 1L;
}
}