package org.pepsoft.worldpainter.exporting;
import org.pepsoft.minecraft.Material;
import org.pepsoft.util.Box;
import org.pepsoft.util.ProgressReceiver;
import org.pepsoft.worldpainter.objects.MinecraftWorldObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import static org.pepsoft.minecraft.Block.*;
import static org.pepsoft.minecraft.Constants.*;
/**
* Helper class which can post process a fully rendered Minecraft map to make
* sure it doesn't violate any Minecraft rules.
*
* Created by Pepijn Schmitz on 15-06-15.
*/
public class PostProcessor {
/**
* Post process (part of) a {@link MinecraftWorld} to make sure it conforms
* to Minecraft's rules. For instance:
*
* <ul><li>Remove plants that are on the wrong underground or floating
* in air.
* <li>Change the lowest block of a column of Sand to Sandstone.
* <li>Remove snow on top of blocks which don't support snow, or floating in
* air.
* <li>Change covered grass and mycelium blocks to dirt.
* </ul>
*
* @param minecraftWorld The <code>MinecraftWorld</code> to post process.
* @param area The area of the world to post process from top to bottom.
* @param progressReceiver The optional progress receiver to which to report
* progress. May be <code>null</code>.
* @throws ProgressReceiver.OperationCancelled If the progress receiver
* threw an <code>OperationCancelled</code> exception.
*/
public static void postProcess(MinecraftWorld minecraftWorld, Rectangle area, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled {
postProcess(minecraftWorld, new Box(area.x, area.x + area.width, area.y, area.y + area.height, 0, minecraftWorld.getMaxHeight()), progressReceiver);
}
/**
* Post process (part of) a {@link MinecraftWorld} to make sure it conforms
* to Minecraft's rules. For instance:
*
* <ul><li>Remove plants that are on the wrong underground or floating
* in air.
* <li>Change the lowest block of a column of Sand to Sandstone.
* <li>Remove snow on top of blocks which don't support snow, or floating in
* air.
* <li>Change covered grass and mycelium blocks to dirt.
* </ul>
*
* @param minecraftWorld The <code>MinecraftWorld</code> to post process.
* @param volume The three dimensional area of the world to post process.
* @param progressReceiver The optional progress receiver to which to report
* progress. May be <code>null</code>.
* @throws ProgressReceiver.OperationCancelled If the progress receiver
* threw an <code>OperationCancelled</code> exception.
*/
public static void postProcess(MinecraftWorld minecraftWorld, Box volume, ProgressReceiver progressReceiver) throws ProgressReceiver.OperationCancelled {
if (! enabled) {
return;
}
if (progressReceiver != null) {
progressReceiver.setMessage("Enforcing Minecraft rules on exported blocks");
}
final int worldMaxZ = minecraftWorld.getMaxHeight() - 1;
final int x1, y1, x2, y2, minZ, maxZ;
// TODO: make these configurable:
final FloatMode sandMode = "false".equalsIgnoreCase(System.getProperty("org.pepsoft.worldpainter.supportSand")) ? FloatMode.LEAVE_FLOATING : FloatMode.SUPPORT;
final FloatMode gravelMode = FloatMode.LEAVE_FLOATING;
if (minecraftWorld instanceof MinecraftWorldObject) {
// Special support for MinecraftWorldObjects to constrain the area
// further
Box objectVolume = ((MinecraftWorldObject) minecraftWorld).getVolume();
objectVolume.intersect(volume);
if (objectVolume.isEmpty()) {
// The specified area does not intersect the volume encompassed
// by the minecraftWorld. Weird, but it means we have nothing to
// do
return;
} else {
x1 = objectVolume.getX1();
x2 = objectVolume.getX2() - 1;
y1 = objectVolume.getY1();
y2 = objectVolume.getY2() - 1;
minZ = objectVolume.getZ1();
maxZ = objectVolume.getZ2() - 1;
}
} else {
x1 = volume.getX1();
y1 = volume.getY1();
x2 = volume.getX2() - 1;
y2 = volume.getY2() - 1;
minZ = volume.getZ1();
maxZ = volume.getZ2() - 1;
}
final boolean traceEnabled = logger.isTraceEnabled();
for (int x = x1; x <= x2; x ++) {
for (int y = y1; y <= y2; y++) {
int blockTypeBelow = BLK_AIR;
int blockTypeAbove = minecraftWorld.getBlockTypeAt(x, y, minZ);
if ((minZ == 0) && (blockTypeAbove != BLK_BEDROCK) && (blockTypeAbove != BLK_AIR) && (blockTypeAbove != BLK_STATIONARY_WATER) && (blockTypeAbove != BLK_STATIONARY_LAVA)) {
logger.warn("Non-bedrock block @ " + x + "," + y + ",0: " + BLOCKS[blockTypeAbove].name);
}
for (int z = minZ; z <= maxZ; z++) {
int blockType = blockTypeAbove;
blockTypeAbove = (z < worldMaxZ) ? minecraftWorld.getBlockTypeAt(x, y, z + 1) : BLK_AIR;
if (((blockTypeBelow == BLK_GRASS) || (blockTypeBelow == BLK_MYCELIUM) || (blockTypeBelow == BLK_TILLED_DIRT)) && ((blockType == BLK_WATER) || (blockType == BLK_STATIONARY_WATER) || (blockType == BLK_ICE) || ((blockType <= HIGHEST_KNOWN_BLOCK_ID) && (BLOCK_TRANSPARENCY[blockType] == 15)))) {
// Covered grass, mycelium or tilled earth block, should
// be dirt. Note that unknown blocks are treated as
// transparent for this check so that grass underneath
// custom plants doesn't turn to dirt, for instance
minecraftWorld.setMaterialAt(x, y, z - 1, Material.DIRT);
blockTypeBelow = BLK_DIRT;
}
switch (blockType) {
case BLK_SAND:
if (BLOCKS[blockTypeBelow].veryInsubstantial) {
switch (sandMode) {
case DROP:
dropBlock(minecraftWorld, x, y, z);
blockType = BLK_AIR;
break;
case SUPPORT:
// All unsupported sand should be supported by sandstone
minecraftWorld.setMaterialAt(x, y, z, (minecraftWorld.getDataAt(x, y, z) == 1) ? Material.RED_SANDSTONE : Material.SANDSTONE);
blockType = minecraftWorld.getBlockTypeAt(x, y, z);
break;
default:
// Do nothing
break;
}
}
break;
case BLK_GRAVEL:
if (BLOCKS[blockTypeBelow].veryInsubstantial) {
switch (gravelMode) {
case DROP:
dropBlock(minecraftWorld, x, y, z);
blockType = BLK_AIR;
break;
case SUPPORT:
// All unsupported gravel should be supported by stone
minecraftWorld.setMaterialAt(x, y, z, Material.STONE);
blockType = BLK_STONE;
break;
default:
// Do nothing
break;
}
}
break;
case BLK_DEAD_SHRUBS:
if ((blockTypeBelow != BLK_SAND) && (blockTypeBelow != BLK_DIRT) && (blockTypeBelow != BLK_STAINED_CLAY) && (blockTypeBelow != BLK_HARDENED_CLAY)) {
// Dead shrubs can only exist on Sand
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_TALL_GRASS:
case BLK_ROSE:
case BLK_DANDELION:
if ((blockTypeBelow != BLK_GRASS) && (blockTypeBelow != BLK_DIRT)) {
// Tall grass and flowers can only exist on Grass or Dirt blocks
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_RED_MUSHROOM:
case BLK_BROWN_MUSHROOM:
if ((blockTypeBelow != BLK_GRASS) && (blockTypeBelow != BLK_DIRT) && (blockTypeBelow != BLK_MYCELIUM) && (blockTypeBelow != BLK_STONE)) {
// Mushrooms can only exist on Grass, Dirt, Mycelium or Stone (in caves) blocks
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_SNOW:
if ((blockTypeBelow == BLK_ICE) || (blockTypeBelow == BLK_SNOW) || (blockTypeBelow == BLK_AIR) || (blockTypeBelow == BLK_PACKED_ICE)) {
// Snow can't be on ice, or another snow block, or air
// (well it could be, but it makes no sense, would
// disappear when touched, and it makes this algorithm
// remove stacks of snow blocks correctly)
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_WHEAT:
if (blockTypeBelow != BLK_TILLED_DIRT) {
// Wheat can only exist on Tilled Dirt blocks
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_LARGE_FLOWERS:
int data = minecraftWorld.getDataAt(x, y, z);
if ((data & 0x8) == 0x8) {
// Bit 4 set; top half of double high plant; check
// there's a lower half beneath
// If the block below is another double high plant
// block we don't need to check whether it is of the
// correct type because the combo was already
// checked when the lower half was encountered
// in the previous iteration
if (blockTypeBelow != BLK_LARGE_FLOWERS) {
// There's a non-double high plant block below;
// replace this block with air
if (traceEnabled) {
logger.trace("Block @ " + x + "," + z + "," + y + " is upper large flower block; block below is " + BLOCK_TYPE_NAMES[blockTypeBelow] + "; removing block");
}
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
} else {
// Otherwise: lower half of double high plant; check
// there's a top half above and grass or dirt below
if (blockTypeAbove == BLK_LARGE_FLOWERS) {
if ((minecraftWorld.getDataAt(x, y, z + 1) & 0x8) == 0) {
// There's another lower half above. Replace
// this block with air
if (traceEnabled) {
logger.trace("Block @ " + x + "," + z + "," + y + " is lower large flower block; block above is another lower large flower block; removing block");
}
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
} else if ((blockTypeBelow != BLK_GRASS) && (blockTypeBelow != BLK_DIRT)) {
// Double high plants can (presumably; TODO:
// check) only exist on grass or dirt
if (traceEnabled) {
logger.trace("Block @ " + x + "," + z + "," + y + " is lower large flower block; block above is " + BLOCK_TYPE_NAMES[blockTypeBelow] + "; removing block");
}
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
} else {
// There's a non-double high plant block above;
// replace this block with air
if (traceEnabled) {
logger.trace("Block @ " + x + "," + z + "," + y + " is lower large flower block; block above is " + BLOCK_TYPE_NAMES[blockTypeBelow] + "; removing block");
}
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
}
break;
case BLK_CACTUS:
if ((blockTypeBelow != BLK_SAND) && (blockTypeBelow != BLK_CACTUS)) {
// Cactus blocks can only be on top of sand or other cactus blocks
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_SUGAR_CANE:
if ((blockTypeBelow != BLK_GRASS) && (blockTypeBelow != BLK_DIRT) && (blockTypeBelow != BLK_SAND) && (blockTypeBelow != BLK_SUGAR_CANE)) {
// Sugar cane blocks can only be on top of grass, dirt, sand or other sugar cane blocks
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_NETHER_WART:
if (blockTypeBelow != BLK_SOUL_SAND) {
// Nether wart blocks can only be on top of soul sand
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_CHORUS_FLOWER:
case BLK_CHORUS_PLANT:
if ((blockTypeBelow != BLK_END_STONE) && (blockTypeBelow != BLK_CHORUS_PLANT)) {
// Chorus flower and plant blocks can only be on top of end stone or other chorus plant blocks
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
case BLK_FIRE:
// We don't know which blocks are flammable, but at
// least check whether the fire is not floating in
// the air
if ((blockTypeBelow == BLK_AIR)
&& (blockTypeAbove == BLK_AIR)
&& (minecraftWorld.getBlockTypeAt(x - 1, y, z) == BLK_AIR)
&& (minecraftWorld.getBlockTypeAt(x + 1, y, z) == BLK_AIR)
&& (minecraftWorld.getBlockTypeAt(x, y - 1, z) == BLK_AIR)
&& (minecraftWorld.getBlockTypeAt(x, y + 1, z) == BLK_AIR)) {
minecraftWorld.setMaterialAt(x, y, z, Material.AIR);
blockType = BLK_AIR;
}
break;
}
blockTypeBelow = blockType;
}
}
if (progressReceiver != null) {
progressReceiver.setProgress((float) (x - x1 + 1) / (x2 - x1 + 1));
}
}
}
private static void dropBlock(MinecraftWorld world, int x, int y, int z) {
int solidFloor = z - 1;
for (; solidFloor > 0; solidFloor--) {
int blockType = world.getBlockTypeAt(x, y, solidFloor);
if (BLOCKS[blockType].insubstantial) {
// Remove insubstantial blocks (as the falling block would have
// obliterated them) but keep looking for a solid floor
world.setMaterialAt(x, y, solidFloor, Material.AIR);
} else if ((blockType != BLK_AIR) && (blockType != BLK_WATER) && (blockType != BLK_STATIONARY_WATER) && (blockType != BLK_LAVA) && (blockType != BLK_STATIONARY_LAVA)) {
break;
}
}
if (solidFloor < 0) {
// The block would have fallen into the void, so just remove it
world.setMaterialAt(x, y, z, Material.AIR);
} else if (solidFloor < z - 1) {
Material block = world.getMaterialAt(x, y, z);
world.setMaterialAt(x, y, z, Material.AIR);
world.setMaterialAt(x, y, solidFloor + 1, block);
}
}
private static final boolean enabled = ! "false".equalsIgnoreCase(System.getProperty("org.pepsoft.worldpainter.enforceBlockRules"));
private static final Logger logger = LoggerFactory.getLogger(PostProcessor.class);
static {
if (! enabled) {
logger.warn("Block rule enforcement disabled");
}
}
enum FloatMode {DROP, SUPPORT, LEAVE_FLOATING}
}