/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.layers.bo2;
import org.pepsoft.worldpainter.Dimension;
import org.pepsoft.worldpainter.exporting.Fixup;
import org.pepsoft.worldpainter.exporting.IncidentalLayerExporter;
import org.pepsoft.worldpainter.exporting.MinecraftWorld;
import org.pepsoft.worldpainter.exporting.SecondPassLayerExporter;
import org.pepsoft.worldpainter.layers.Bo2Layer;
import org.pepsoft.worldpainter.layers.FloodWithLava;
import org.pepsoft.worldpainter.layers.exporters.WPObjectExporter;
import org.pepsoft.worldpainter.objects.MirroredObject;
import org.pepsoft.worldpainter.objects.RotatedObject;
import org.pepsoft.worldpainter.objects.WPObject;
import javax.vecmath.Point3i;
import java.awt.*;
import java.util.ArrayList;
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.objects.WPObject.*;
/**
* An exporter of {@link Bo2Layer}s.
*
* @author pepijn
*/
public class Bo2LayerExporter extends WPObjectExporter<Bo2Layer> implements SecondPassLayerExporter, IncidentalLayerExporter {
public Bo2LayerExporter(Bo2Layer layer) {
super(layer);
}
@Override
public List<Fixup> render(final Dimension dimension, Rectangle area, Rectangle exportedArea, MinecraftWorld minecraftWorld) {
final Bo2ObjectProvider objectProvider = layer.getObjectProvider();
final int maxHeight = dimension.getMaxHeight();
final int maxZ = maxHeight - 1;
final List<Fixup> fixups = new ArrayList<>();
final int density = layer.getDensity() * 64;
for (int chunkX = area.x; chunkX < area.x + area.width; chunkX += 16) {
for (int chunkY = area.y; chunkY < area.y + area.height; chunkY += 16) {
// Set the seed and randomizer according to the chunk
// coordinates to make sure the chunk is always rendered the
// same, no matter how often it is rendered
final long seed = dimension.getSeed() + (chunkX >> 4) * 65537 + (chunkY >> 4) * 4099;
final Random random = new Random(seed);
objectProvider.setSeed(seed);
for (int x = chunkX; x < chunkX + 16; x++) {
for (int y = chunkY; y < chunkY + 16; y++) {
final int height = dimension.getIntHeightAt(x, y);
if ((height == -1) || (height >= maxZ)) {
// height == -1 means no tile present
continue;
}
final int strength = dimension.getLayerValueAt(layer, x, y);
if ((strength > 0) && (random.nextInt(density) <= strength * strength)) {
WPObject object = objectProvider.getObject();
final Placement placement = getPlacement(minecraftWorld, dimension, x, y, height + 1, object, random);
if (placement == Placement.NONE) {
continue;
}
if (object.getAttribute(ATTRIBUTE_RANDOM_ROTATION)) {
if (random.nextBoolean()) {
object = new MirroredObject(object, false);
}
int rotateSteps = random.nextInt(4);
if (rotateSteps > 0) {
object = new RotatedObject(object, rotateSteps);
}
}
final int z = (placement == Placement.ON_LAND) ? height + 1 : dimension.getWaterLevelAt(x, y) + 1;
if ((! isSane(object, x, y, z, maxHeight)) || (! isRoom(minecraftWorld, dimension, object, x, y, z, placement))) {
continue;
}
if (! fitsInExportedArea(exportedArea, object, x, y)) {
// There is room on our side of the border, but
// the object extends outside the exported area,
// so it might clash with an object from another
// area. Schedule a fixup to retest whether
// there is room after all the objects have been
// placed on both sides of the border
fixups.add(new WPObjectFixup(object, x, y, z, placement));
continue;
}
renderObject(minecraftWorld, dimension, object, x, y, z);
}
}
}
}
}
return fixups;
}
@Override
public Fixup apply(Dimension dimension, Point3i location, int intensity, Rectangle exportedArea, MinecraftWorld minecraftWorld) {
final Random random = incidentalRandomRef.get();
final long seed = dimension.getSeed() ^ ((long) location.x << 40) ^ ((long) location.y << 20) ^ (location.z);
random.setSeed(seed);
if ((intensity > 0) && (random.nextInt(layer.getDensity() * 20) <= intensity * intensity / 225)) {
final Bo2ObjectProvider objectProvider = layer.getObjectProvider();
objectProvider.setSeed(seed);
WPObject object = objectProvider.getObject();
int existingBlockType = minecraftWorld.getBlockTypeAt(location.x, location.y, location.z);
int blockBelow = minecraftWorld.getBlockTypeAt(location.x, location.y, location.z - 1);
if ((object.getAttribute(ATTRIBUTE_SPAWN_IN_LAVA) && ((existingBlockType == BLK_LAVA) || (existingBlockType == BLK_STATIONARY_LAVA)))
|| (object.getAttribute(ATTRIBUTE_SPAWN_IN_WATER) && ((existingBlockType == BLK_WATER) || (existingBlockType == BLK_STATIONARY_WATER)))
|| (object.getAttribute(ATTRIBUTE_SPAWN_ON_LAND) && (! BLOCKS[blockBelow].veryInsubstantial))
|| (! object.getAttribute(ATTRIBUTE_NEEDS_FOUNDATION) && BLOCKS[blockBelow].veryInsubstantial)) {
if (object.getAttribute(ATTRIBUTE_RANDOM_ROTATION)) {
if (random.nextBoolean()) {
object = new MirroredObject(object, false);
}
int rotateSteps = random.nextInt(4);
if (rotateSteps > 0) {
object = new RotatedObject(object, rotateSteps);
}
}
if ((!isSane(object, location.x, location.y, location.z, minecraftWorld.getMaxHeight())) || (!isRoom(minecraftWorld, dimension, object, location.x, location.y, location.z, Placement.ON_LAND))) {
return null;
}
if (!fitsInExportedArea(exportedArea, object, location.x, location.y)) {
// There is room on our side of the border, but the object
// extends outside the exported area, so it might clash with an
// object from another area. Schedule a fixup to retest whether
// there is room after all the objects have been placed on both
// sides of the border
return new WPObjectFixup(object, location.x, location.y, location.z, Placement.ON_LAND);
}
renderObject(minecraftWorld, dimension, object, location.x, location.y, location.z);
}
}
return null;
}
/**
* Determines whether an object fits completely into the area currently
* being rendere in the horizontal dimensions.
*
* @return <code>true</code> if the object fits.
*/
private boolean fitsInExportedArea(final Rectangle exportedArea, final WPObject object, final int x, final int y) {
final Point3i dimensions = object.getDimensions();
final Point3i offset = object.getOffset();
// Check whether the objects fits completely inside the exported area.
// This is to avoid objects getting cut off at area boundaries
return ! ((x + offset.x < exportedArea.x) || (x + offset.x + dimensions.x > exportedArea.x + exportedArea.width)
|| (y + offset.y < exportedArea.y) || (y + offset.y + dimensions.y > exportedArea.y + exportedArea.height));
}
/**
* Determines whether an object's attributes allow it to be placed at a
* certain location, and if so where along the vertical axis.
*
* @return An indication of where along the vertical axis the object may
* be placed, which may be {@link Placement#NONE} if it may not be
* placed at all.
*/
private Placement getPlacement(final MinecraftWorld minecraftWorld, final Dimension dimension, final int x, final int y, final int z, final WPObject object, final Random random) {
final boolean spawnUnderWater = object.getAttribute(ATTRIBUTE_SPAWN_IN_WATER), spawnUnderLava = object.getAttribute(ATTRIBUTE_SPAWN_IN_LAVA);
final boolean spawnOnWater = object.getAttribute(ATTRIBUTE_SPAWN_ON_WATER), spawnOnLava = object.getAttribute(ATTRIBUTE_SPAWN_ON_LAVA);
final int waterLevel = dimension.getWaterLevelAt(x, y);
final boolean flooded = waterLevel >= z;
if (flooded && (spawnUnderWater || spawnUnderLava || spawnOnWater || spawnOnLava)) {
boolean lava = dimension.getBitLayerValueAt(FloodWithLava.INSTANCE, x, y);
if (lava ? (spawnUnderLava && spawnOnLava) : (spawnUnderWater && spawnOnWater)) {
if (logger.isTraceEnabled()) {
logger.trace("Object " + object.getName() + " @ " + x + "," + y + "," + z + " potentially placeable under or on water or lava");
}
return random.nextBoolean() ? Placement.ON_LAND : Placement.FLOATING;
} else if (lava ? spawnUnderLava : spawnUnderWater) {
if (logger.isTraceEnabled()) {
logger.trace("Object " + object.getName() + " @ " + x + "," + y + "," + z + " potentially placeable under water or lava");
}
return Placement.ON_LAND;
} else if (lava ? spawnOnLava : spawnOnWater) {
if (logger.isTraceEnabled()) {
logger.trace("Object " + object.getName() + " @ " + x + "," + y + "," + z + " potentially placeable on water or lava");
}
return Placement.FLOATING;
}
} else if (! flooded) {
int blockTypeUnderCoords = (z > 0) ? minecraftWorld.getBlockTypeAt(x, y, z - 1) : BLK_AIR;
if (object.getAttribute(ATTRIBUTE_SPAWN_ON_LAND) && (! BLOCKS[blockTypeUnderCoords].veryInsubstantial)) {
if (logger.isTraceEnabled()) {
logger.trace("Object " + object.getName() + " @ " + x + "," + y + "," + z + " potentially placeable on land");
}
return Placement.ON_LAND;
} else if ((! object.getAttribute(ATTRIBUTE_NEEDS_FOUNDATION)) && BLOCKS[blockTypeUnderCoords].veryInsubstantial) {
if (logger.isTraceEnabled()) {
logger.trace("Object " + object.getName() + " @ " + x + "," + y + "," + z + " potentially placeable in the air");
}
return Placement.ON_LAND;
}
}
if (logger.isTraceEnabled()) {
logger.trace("Object " + object.getName() + " @ " + x + "," + y + "," + z + " not placeable");
}
return Placement.NONE;
}
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Bo2LayerExporter.class);
private final ThreadLocal<Random> incidentalRandomRef = new ThreadLocal<Random>() {
@Override
protected Random initialValue() {
return new Random();
}
};
}