/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.layers.exporters;
import org.pepsoft.util.PerlinNoise;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.MixedMaterial;
import org.pepsoft.worldpainter.MixedMaterial.Row;
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.FloodWithLava;
import org.pepsoft.worldpainter.layers.River;
import org.pepsoft.worldpainter.util.GeometryUtil;
import java.awt.*;
import java.util.List;
import static org.pepsoft.minecraft.Block.BLOCKS;
import static org.pepsoft.minecraft.Constants.*;
import static org.pepsoft.minecraft.Material.*;
import static org.pepsoft.worldpainter.Constants.TINY_BLOBS;
import static org.pepsoft.worldpainter.biomeschemes.Minecraft1_7Biomes.BIOME_RIVER;
/**
*
* @author pepijn
*/
public class RiverExporter extends AbstractLayerExporter<River> implements SecondPassLayerExporter {
public RiverExporter() {
super(River.INSTANCE, new RiverSettings());
}
@Override
public List<Fixup> render(final Dimension dimension, final Rectangle area, final Rectangle exportedArea, final MinecraftWorld minecraftWorld) {
final RiverSettings settings = new RiverSettings();
final int shoreHeight = settings.getShoreHeight();
final boolean shore = shoreHeight > 0;
final MixedMaterial riverBedMaterial = settings.getRiverBedMaterial();
final int riverConnectionRadius = settings.getRiverConnectionRadius();
final int maxDepth = shoreHeight + settings.getMaxDepth();
final int depthVariation = settings.getMaxDepth() - settings.getMinDepth();
final long seed = dimension.getSeed();
if (floorNoise.getSeed() != seed) {
floorNoise.setSeed(seed);
}
// First pass. Dig out the riverbed and flood the river
for (int x = area.x; x < area.x + area.width; x++) {
for (int y = area.y; y < area.y + area.height; y++) {
if (dimension.getBitLayerValueAt(River.INSTANCE, x, y)) {
final float distanceToShore = dimension.getDistanceToEdge(River.INSTANCE, x, y, maxDepth);
final int depth = (int) Math.min(distanceToShore, maxDepth - (int) ((floorNoise.getPerlinNoise(x / TINY_BLOBS, y / TINY_BLOBS) + 0.5f) * depthVariation + 0.5f));
final int terrainHeight = dimension.getIntHeightAt(x, y);
final int waterLevel = dimension.getWaterLevelAt(x, y);
if ((waterLevel > terrainHeight) && (! dimension.getBitLayerValueAt(FloodWithLava.INSTANCE, x, y))) {
// Already flooded. Special processing: just dig out the
// river bed
for (int dz = 0; dz >= (-depth - 1); dz--) {
final int z = waterLevel + dz;
if (dz > -depth) {
minecraftWorld.setMaterialAt(x, y, z, STATIONARY_WATER);
} else if (z <= terrainHeight) {
minecraftWorld.setMaterialAt(x, y, z, riverBedMaterial.getMaterial(seed, x, y, z));
}
}
} else {
// Dry land (although the river connection flooding
// algorithm may have placed water, so we still have to
// check for it
for (int dz = 0; dz >= (-depth - 1); dz--) {
final int z = terrainHeight + dz;
if (z <= 0) {
break;
}
if (dz <= -depth) {
// River bed
int existingBlockType = minecraftWorld.getBlockTypeAt(x, y, z);
if (! BLOCKS[existingBlockType].veryInsubstantial) {
minecraftWorld.setMaterialAt(x, y, z, riverBedMaterial.getMaterial(seed, x, y, z));
}
} else if (dz > -shoreHeight) {
// The air above the river
int existingBlockType = minecraftWorld.getBlockTypeAt(x, y, z);
if ((existingBlockType != BLK_WATER) && (existingBlockType != BLK_STATIONARY_WATER)) {
minecraftWorld.setMaterialAt(x, y, z, AIR);
}
} else if (dz == -shoreHeight) {
// The surface of the river
minecraftWorld.setMaterialAt(x, y, z, STATIONARY_WATER);
} else {
// The river itself below the surface
minecraftWorld.setMaterialAt(x, y, z, STATIONARY_WATER);
}
}
}
}
}
}
// Second pass. Look for bodies of water that connect to rivers and
// flood the connecting areas
if (shore) {
for (int x = area.x; x < area.x + area.width; x++) {
for (int y = area.y; y < area.y + area.height; y++) {
final int waterLevel = dimension.getWaterLevelAt(x, y);
if (dimension.getBitLayerValueAt(River.INSTANCE, x, y) // River
&& (waterLevel > dimension.getIntHeightAt(x, y)) // Flooded
&& (dimension.getFloodedCount(x, y, 1, false) < 9)) { // Not every surrounding block flooded
// We're at the edge of a flooded area. Check
// whether it needs to be expanded to flood the
// connecting riverbed, where the water will be
// lower (if the shore height is higher than zero)
final int finalX = x, finalY = y;
// First see if there is any river around with a
// water level that's higher
final boolean[] higherRiverAround = new boolean[1];
GeometryUtil.visitFilledCircle(riverConnectionRadius, (dx, dy, d) -> {
final int x1 = finalX + dx, y1 = finalY + dy;
if ((dimension.getBitLayerValueAt(River.INSTANCE, x1, y1))
&& ((dimension.getIntHeightAt(x1, y1) - shoreHeight) > waterLevel)) {
higherRiverAround[0] = true;
return false;
} else {
return true;
}
});
if (higherRiverAround[0]) {
// There is a river near with a water level
// that's higher, so flood the surroundings to
// the height of the body of water
GeometryUtil.visitFilledCircle(riverConnectionRadius, (dx, dy, d) -> {
final int x1 = finalX + dx, y1 = finalY + dy;
if ((dimension.getBitLayerValueAt(River.INSTANCE, x1, y1))
&& (dimension.getWaterLevelAt(x1, y1) < waterLevel)) {
int z = waterLevel;
int existingBlockType = minecraftWorld.getBlockTypeAt(x1, y1, z);
while ((z > 0)
&& (existingBlockType != BLK_WATER)
&& (existingBlockType != BLK_STATIONARY_WATER)
&& (existingBlockType != BLK_ICE)
&& BLOCKS[existingBlockType].veryInsubstantial) {
minecraftWorld.setMaterialAt(x1, y1, z, STATIONARY_WATER);
z--;
existingBlockType = minecraftWorld.getBlockTypeAt(x1, y1, z);
}
}
return true;
});
} else {
// There is no river near with a water level
// that's higher, so flood the surroundings
// sloping down just in case there's *lower*
// river water which we want to connect to a bit
// less abruptly than with a sheer wall of water
GeometryUtil.visitFilledCircle(riverConnectionRadius, (dx, dy, d) -> {
final int x1 = finalX + dx, y1 = finalY + dy;
if ((dimension.getBitLayerValueAt(River.INSTANCE, x1, y1))
&& (dimension.getWaterLevelAt(x1, y1) < waterLevel)) {
int z = waterLevel - ((int) d);
int existingBlockType = minecraftWorld.getBlockTypeAt(x1, y1, z);
while ((z > 0)
&& (existingBlockType != BLK_WATER)
&& (existingBlockType != BLK_STATIONARY_WATER)
&& (existingBlockType != BLK_ICE)
&& BLOCKS[existingBlockType].veryInsubstantial) {
minecraftWorld.setMaterialAt(x1, y1, z, STATIONARY_WATER);
z--;
existingBlockType = minecraftWorld.getBlockTypeAt(x1, y1, z);
}
}
return true;
});
}
}
}
}
}
// Third pass. Make sure all the water connects to at least one
// surrounding water column, to make it connect visually, and mitigate
// overflowing problems
for (int x = area.x; x < area.x + area.width; x++) {
for (int y = area.y; y < area.y + area.height; y++) {
if (dimension.getBitLayerValueAt(River.INSTANCE, x, y)) {
int lowestWaterHeight = -1;
for (int z = dimension.getIntHeightAt(x, y); z > 0; z--) {
if (((minecraftWorld.getBlockTypeAt(x, y, z) == BLK_WATER) || (minecraftWorld.getBlockTypeAt(x, y, z) == BLK_STATIONARY_WATER)) && (minecraftWorld.getBlockTypeAt(x, y, z - 1) != BLK_WATER) && (minecraftWorld.getBlockTypeAt(x, y, z - 1) != BLK_STATIONARY_WATER)) {
lowestWaterHeight = z;
break;
}
}
if (lowestWaterHeight != -1) {
// There is water in this column. Raise the water in
// surrounding columns to at least the height of the
// lowest water block in this column.
raiseWaterTo(dimension, minecraftWorld, x + 1, y, lowestWaterHeight - 1);
raiseWaterTo(dimension, minecraftWorld, x, y + 1, lowestWaterHeight - 1);
raiseWaterTo(dimension, minecraftWorld, x - 1, y, lowestWaterHeight - 1);
raiseWaterTo(dimension, minecraftWorld, x, y - 1, lowestWaterHeight - 1);
}
}
}
}
return null;
}
private void raiseWaterTo(Dimension dimension, MinecraftWorld world, int x, int y, int raiseTo) {
if (! dimension.getBitLayerValueAt(River.INSTANCE, x, y)) {
return;
}
int waterLevel = -1;
for (int z = dimension.getIntHeightAt(x, y); z > 0; z--) {
int existingBlockType = world.getBlockTypeAt(x, y, z);
if (existingBlockType == BLK_AIR) {
continue;
} else if ((existingBlockType == BLK_WATER) || (existingBlockType == BLK_STATIONARY_WATER)) {
waterLevel = z;
break;
} else {
// No water in this column (at least not on the surface)
return;
}
}
if (waterLevel != -1) {
for (int z = waterLevel; z <= raiseTo; z++) {
world.setMaterialAt(x, y, z, STATIONARY_WATER);
}
}
}
private final PerlinNoise floorNoise = new PerlinNoise(0);
public static class RiverSettings implements ExporterSettings {
public RiverSettings() {
riverBedMaterial = new MixedMaterial(
"Riverbed",
new Row[] {new Row(GRASS, 650, 1.0f),
new Row(GRAVEL, 200, 1.0f),
new Row(CLAY, 50, 1.0f),
new Row(STONE, 100, 1.0f)},
BIOME_RIVER,
0x0000ff,
3.0f);
}
@Override
public boolean isApplyEverywhere() {
return false;
}
public int getShoreHeight() {
return shoreHeight;
}
public void setShoreHeight(int shoreHeight) {
this.shoreHeight = shoreHeight;
}
public int getMinDepth() {
return minDepth;
}
public void setMinDepth(int minDepth) {
this.minDepth = minDepth;
}
public int getMaxDepth() {
return maxDepth;
}
public void setMaxDepth(int maxDepth) {
this.maxDepth = maxDepth;
}
public MixedMaterial getRiverBedMaterial() {
return riverBedMaterial;
}
public void setRiverBedMaterial(MixedMaterial riverBedMaterial) {
this.riverBedMaterial = riverBedMaterial;
}
public int getRiverConnectionRadius() {
return riverConnectionRadius;
}
public void setRiverConnectionRadius(int riverConnectionRadius) {
this.riverConnectionRadius = riverConnectionRadius;
}
@Override
public River getLayer() {
return River.INSTANCE;
}
@Override
public RiverSettings clone() {
try {
return (RiverSettings) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
private int shoreHeight = 2, minDepth = 4, maxDepth = 7, riverConnectionRadius = 10;
private MixedMaterial riverBedMaterial;
private static final long serialVersionUID = 1L;
}
}