/* * To change this template, choose Tools | Templates * and open the template in the editor. */ package org.pepsoft.worldpainter.layers.exporters; import org.pepsoft.minecraft.Block; import org.pepsoft.minecraft.Entity; import org.pepsoft.minecraft.Material; import org.pepsoft.minecraft.TileEntity; import org.pepsoft.util.Box; import org.pepsoft.util.MathUtils; import org.pepsoft.util.ProgressReceiver; import org.pepsoft.worldpainter.Dimension; import org.pepsoft.worldpainter.exporting.*; import org.pepsoft.worldpainter.layers.Frost; import org.pepsoft.worldpainter.layers.Layer; import org.pepsoft.worldpainter.objects.WPObject; import javax.vecmath.Point3i; import java.awt.*; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.pepsoft.minecraft.Block.*; import static org.pepsoft.minecraft.Constants.*; import static org.pepsoft.worldpainter.objects.WPObject.*; /** * An exporter which knows how to render {@link WPObject}s to a * {@link MinecraftWorld}. * * @author pepijn */ public abstract class WPObjectExporter<L extends Layer> extends AbstractLayerExporter<L> { public WPObjectExporter(L layer) { super(layer); } public WPObjectExporter(L layer, ExporterSettings defaultSettings) { super(layer, defaultSettings); } /** * Export an object to the world, taking into account the blocks that are * already there. * * @param world The Minecraft world to which to export the object. * @param dimension The dimension corresponding to the exported world, used * to determine the terrain height. * @param object The object to export. * @param x The X coordinate at which to export the object. * @param y The Y coordinate at which to export the object. * @param z The Z coordinate at which to export the object. */ public static void renderObject(MinecraftWorld world, Dimension dimension, WPObject object, int x, int y, int z) { renderObject(world, dimension, object, x, y, z, false); } /** * Export an object to the world, optionally taking into account the blocks * that are already there. * * @param world The Minecraft world to which to export the object. * @param dimension The dimension corresponding to the exported world, used * to determine the terrain height. * @param object The object to export. * @param x The X coordinate at which to export the object. * @param y The Y coordinate at which to export the object. * @param z The Z coordinate at which to export the object. * @param obliterate When <code>true</code>, all blocks of the object are * placed regardless of what is already there. When <code>false</code>, * rules are followed and some or all blocks may not be placed, * depending on what is already there. */ public static void renderObject(MinecraftWorld world, Dimension dimension, WPObject object, int x, int y, int z, boolean obliterate) { final Point3i dim = object.getDimensions(); final Point3i offset = object.getOffset(); final int undergroundMode = object.getAttribute(ATTRIBUTE_UNDERGROUND_MODE); final int leafDecayMode = object.getAttribute(ATTRIBUTE_LEAF_DECAY_MODE); final boolean bottomless = dimension.isBottomless(); final int[] replaceBlockIds = object.getAttribute(ATTRIBUTE_REPLACE_WITH_AIR); final boolean replaceBlocks = replaceBlockIds != null; final boolean extendFoundation = object.getAttribute(ATTRIBUTE_EXTEND_FOUNDATION); if ((z + offset.z + dim.z - 1) >= world.getMaxHeight()) { // Object doesn't fit in the world vertically return; } // System.out.println("Object dimensions: " + dim + ", origin: " + orig); for (int dx = 0; dx < dim.x; dx++) { for (int dy = 0; dy < dim.y; dy++) { final int worldX = x + dx + offset.x; final int worldY = y + dy + offset.y; final int terrainHeight = dimension.getIntHeightAt(worldX, worldY); for (int dz = 0; dz < dim.z; dz++) { if (object.getMask(dx, dy, dz)) { final Material objectMaterial = object.getMaterial(dx, dy, dz); final Material finalMaterial = (replaceBlocks && (objectMaterial.blockType == replaceBlockIds[0]) && (objectMaterial.data == replaceBlockIds[1])) ? Material.AIR : objectMaterial; final int worldZ = z + dz + offset.z; if ((bottomless || obliterate) ? (worldZ < 0) : (worldZ < 1)) { continue; } else if (obliterate) { placeBlock(world, worldX, worldY, worldZ, finalMaterial, leafDecayMode); } else { final int existingBlockType = world.getBlockTypeAt(worldX, worldY, worldZ); if (worldZ <= terrainHeight) { switch (undergroundMode) { case COLLISION_MODE_ALL: // Replace every block placeBlock(world, worldX, worldY, worldZ, finalMaterial, leafDecayMode); break; case COLLISION_MODE_SOLID: // Only replace if object block is solid if (! objectMaterial.block.veryInsubstantial) { placeBlock(world, worldX, worldY, worldZ, finalMaterial, leafDecayMode); } break; case COLLISION_MODE_NONE: // Only replace less solid blocks if (BLOCKS[existingBlockType].veryInsubstantial) { placeBlock(world, worldX, worldY, worldZ, finalMaterial, leafDecayMode); } break; } } else { // Above ground only replace less solid blocks if (BLOCKS[existingBlockType].veryInsubstantial) { placeBlock(world, worldX, worldY, worldZ, finalMaterial, leafDecayMode); } } } if (extendFoundation && (dz == 0) && (terrainHeight != -1) && (worldZ > terrainHeight) && (! finalMaterial.block.veryInsubstantial)) { int legZ = worldZ - 1; while ((legZ >= 0) && world.getMaterialAt(worldX, worldY, legZ).block.veryInsubstantial) { placeBlock(world, worldX, worldY, legZ, finalMaterial, leafDecayMode); legZ--; } } } } } } List<Entity> entities = object.getEntities(); if (entities != null) { for (Entity entity: entities) { double[] pos = entity.getPos(); double entityX = x + pos[0] + offset.x, entityY = y + pos[2] + offset.y, entityZ = z + pos[1] + offset.z; if ((entityZ < 0) || (entityY > (world.getMaxHeight() - 1))) { if (logger.isTraceEnabled()) { logger.trace("NOT adding entity " + entity.getId() + " @ " + entityX + "," + entityY + "," + entityZ + " because z coordinate is out of range!"); } } else { if (logger.isTraceEnabled()) { logger.trace("Adding entity " + entity.getId() + " @ " + entityX + "," + entityY + "," + entityZ); } world.addEntity(entityX, entityY, entityZ, entity); } } } List<TileEntity> tileEntities = object.getTileEntities(); if (tileEntities != null) { for (TileEntity tileEntity: tileEntities) { final int tileEntityX = x + tileEntity.getX() + offset.x, tileEntityY = y + tileEntity.getZ() + offset.y, tileEntityZ = z + tileEntity.getY() + offset.z; final String entityId = tileEntity.getId(); if ((tileEntityZ < 0) || (tileEntityZ >= world.getMaxHeight())) { if (logger.isTraceEnabled()) { logger.trace("NOT adding tile entity " + entityId + " @ " + tileEntityX + "," + tileEntityY + "," + tileEntityZ + " because z coordinate is out of range!"); } } else { final int existingBlockType = world.getBlockTypeAt(tileEntityX, tileEntityY, tileEntityZ); if (! TILE_ENTITY_MAP.containsKey(entityId)) { if (logger.isTraceEnabled()) { logger.trace("Adding unknown tile entity " + entityId + " @ " + tileEntityX + "," + tileEntityY + "," + tileEntityZ + " (block type: " + BLOCK_TYPE_NAMES[existingBlockType] + "; not able to detect whether the block type is correct; map may cause errors!)"); } world.addTileEntity(tileEntityX, tileEntityY, tileEntityZ, tileEntity); } else if (TILE_ENTITY_MAP.get(entityId).contains(existingBlockType)) { if (logger.isTraceEnabled()) { logger.trace("Adding tile entity " + entityId + " @ " + tileEntityX + "," + tileEntityY + "," + tileEntityZ + " (block type: " + BLOCK_TYPE_NAMES[existingBlockType] + ")"); } world.addTileEntity(tileEntityX, tileEntityY, tileEntityZ, tileEntity); } else { // The tile entity is not there, for whatever reason (there // are all kinds of legitimate reasons why this would // happen, for instance if the block was not placed because // it collided with another block, or it was below or above // the world limits) if (logger.isTraceEnabled()) { logger.trace("NOT adding tile entity " + entityId + " @ " + tileEntityX + "," + tileEntityY + "," + tileEntityZ + " because the block there is not a (or not the same) tile entity: " + BLOCK_TYPE_NAMES[existingBlockType] + "!"); } } } } } } /** * Check whether the coordinates of the extents of the object make sense. In * other words: whether it could potentially be placeable at all given its * dimensions and location. * * @return <code>true</code> if the object could potentially be placeable * and the caller can proceed with further checks. */ public static boolean isSane(WPObject object, int x, int y, int z, int maxHeight) { final Point3i dimensions = object.getDimensions(); final Point3i offset = object.getOffset(); if ((((long) x + offset.x) < Integer.MIN_VALUE) || (((long) x + offset.x) > Integer.MAX_VALUE)) { if (logger.isDebugEnabled()) { logger.debug("Object {0}@{1},{2},{3} extends beyond the limits of a 32 bit signed integer in the X dimension", object.getName(), x, y, z); } return false; } if ((((long) x + dimensions.x - 1 + offset.x) < Integer.MIN_VALUE) || (((long) x + dimensions.x - 1 + offset.x) > Integer.MAX_VALUE)) { // The object extends beyond the limits of a 32 bit signed integer in the X dimension return false; } if ((((long) y + offset.y) < Integer.MIN_VALUE) || (((long) y + offset.y) > Integer.MAX_VALUE)) { // The object extends beyond the limits of a 32 bit signed integer in the Y dimension return false; } if ((((long) y + dimensions.y - 1 + offset.y) < Integer.MIN_VALUE) || (((long) y + dimensions.y - 1 + offset.y) > Integer.MAX_VALUE)) { // The object extends beyond the limits of a 32 bit signed integer in the Y dimension return false; } if (((long) z + offset.z) >= maxHeight) { // The object is entirely above maxHeight return false; } if (((long) z + dimensions.z - 1 + offset.z) < 0) { // The object is entirely below bedrock return false; } return true; } /** * Checks block by block and taking the object's collision mode attributes * and other rules into account whether it can be placed at a particular * location. This is a slow operation, so use * {@link #isSane(WPObject, int, int, int, int)} first to weed out objects * for which this check does not even apply. * * @return <code>true</code> if the object may be placed at the specified * location according to its collision mode attributes. */ public static boolean isRoom(final MinecraftWorld world, final Dimension dimension, final WPObject object, final int x, final int y, final int z, final Placement placement) { final Point3i dimensions = object.getDimensions(); final Point3i offset = object.getOffset(); final int collisionMode = object.getAttribute(ATTRIBUTE_COLLISION_MODE), maxHeight = world.getMaxHeight(); final boolean allowConnectingBlocks = false, bottomlessWorld = dimension.isBottomless(); // Check if the object fits vertically if (((long) z + dimensions.z - 1 + offset.z) >= world.getMaxHeight()) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it does not fit below the map height of " + world.getMaxHeight()); } return false; } if (((long) z + dimensions.z - 1 + offset.z) < 0) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it is entirely below the bedrock"); } return false; } if ((placement == Placement.ON_LAND) && (collisionMode != COLLISION_MODE_NONE)) { // Check block by block whether there is room for (int dx = 0; dx < dimensions.x; dx++) { for (int dy = 0; dy < dimensions.y; dy++) { final int worldX = x + dx + offset.x, worldY = y + dy + offset.y; for (int dz = 0; dz < dimensions.z; dz++) { if (object.getMask(dx, dy, dz)) { final Block objectBlock = object.getMaterial(dx, dy, dz).block; if (! objectBlock.veryInsubstantial) { final int worldZ = z + dz + offset.z; if (worldZ < (bottomlessWorld ? 0 : 1)) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it extends below the bottom of the map"); } return false; } else if (worldZ >= maxHeight) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it extends above the top of the map"); } return false; } else if (worldZ > dimension.getIntHeightAt(worldX, worldY)) { if ((collisionMode == COLLISION_MODE_ALL) ? (!AIR_AND_FLUIDS.contains(world.getBlockTypeAt(worldX, worldY, worldZ))) : (!BLOCKS[world.getBlockTypeAt(worldX, worldY, worldZ)].veryInsubstantial)) { // The block is above ground, it is present in the // custom object, is substantial, and there is already a // substantial block at the same location in the world; // there is no room for this object if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " due to collision with existing above ground block of type " + BLOCKS[world.getBlockTypeAt(worldX, worldY, worldZ)]); } return false; } else if ((!allowConnectingBlocks) && wouldConnect(world, worldX, worldY, worldZ, objectBlock.id)) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it would cause a connecting block"); } return false; } } } } } } } } else if (placement == Placement.FLOATING) { // When floating on fluid, the object is not allowed to collide // with the floor for (int dx = 0; dx < dimensions.x; dx++) { for (int dy = 0; dy < dimensions.y; dy++) { final int worldX = x + dx + offset.x, worldY = y + dy + offset.y; final int terrainHeight = dimension.getIntHeightAt(worldX, worldY); for (int dz = 0; dz < dimensions.z; dz++) { if (object.getMask(dx, dy, dz)) { final Block objectBlock = object.getMaterial(dx, dy, dz).block; if (! objectBlock.veryInsubstantial) { final int worldZ = z + dz + offset.z; if (worldZ < (bottomlessWorld ? 0 : 1)) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it extends below the bottom of the map"); } return false; } else if (worldZ >= maxHeight) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it extends above the top of the map"); } return false; } else if (worldZ <= terrainHeight) { // A solid block in the object collides with // the floor if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " due to collision with floor"); } return false; } else if (collisionMode != COLLISION_MODE_NONE) { if ((collisionMode == COLLISION_MODE_ALL) ? (!AIR_AND_FLUIDS.contains(world.getBlockTypeAt(worldX, worldY, worldZ))) : (!BLOCKS[world.getBlockTypeAt(worldX, worldY, worldZ)].veryInsubstantial)) { // The block is present in the custom object, is // substantial, and there is already a // substantial block at the same location in the // world; there is no room for this object if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " due to collision with existing above ground block of type " + BLOCK_TYPE_NAMES[world.getBlockTypeAt(worldX, worldY, worldZ)]); } return false; } else if ((!allowConnectingBlocks) && wouldConnect(world, worldX, worldY, worldZ, objectBlock.id)) { if (logger.isTraceEnabled()) { logger.trace("No room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement + " because it would cause a connecting block"); } return false; } } } } } } } } if (logger.isTraceEnabled()) { logger.trace("There is room for object " + object.getName() + " @ " + x + "," + y + "," + z + " with placement " + placement); } return true; } /** * Determine whether placing a block of the specified type at the specified * location in the specified world would cause the block to connect to * surrounding blocks (for instance a fence block to a solid block, or * another fence block, but not another fence of a different type). */ private static boolean wouldConnect(MinecraftWorld world, int worldX, int worldY, int worldZ, int objectBlock) { if (wouldConnect(objectBlock, world.getBlockTypeAt(worldX - 1, worldY, worldZ))) { if (logger.isTraceEnabled()) { logger.trace(BLOCK_TYPE_NAMES[objectBlock] + " @ " + worldX + "," + worldY + "," + worldZ + " would connect to " + BLOCK_TYPE_NAMES[world.getBlockTypeAt(worldX - 1, worldY, worldZ)] + " @ dx = -1"); } return true; } else if (wouldConnect(objectBlock, world.getBlockTypeAt(worldX, worldY - 1, worldZ))) { if (logger.isTraceEnabled()) { logger.trace(BLOCK_TYPE_NAMES[objectBlock] + " @ " + worldX + "," + worldY + "," + worldZ + " would connect to " + BLOCK_TYPE_NAMES[world.getBlockTypeAt(worldX, worldY - 1, worldZ)] + " @ dy = -1"); } return true; } else if (wouldConnect(objectBlock, world.getBlockTypeAt(worldX + 1, worldY, worldZ))) { if (logger.isTraceEnabled()) { logger.trace(BLOCK_TYPE_NAMES[objectBlock] + " @ " + worldX + "," + worldY + "," + worldZ + " would connect to " + BLOCK_TYPE_NAMES[world.getBlockTypeAt(worldX + 1, worldY, worldZ)] + " @ dx = 1"); } return true; } else if (wouldConnect(objectBlock, world.getBlockTypeAt(worldX, worldY + 1, worldZ))) { if (logger.isTraceEnabled()) { logger.trace(BLOCK_TYPE_NAMES[objectBlock] + " @ " + worldX + "," + worldY + "," + worldZ + " would connect to " + BLOCK_TYPE_NAMES[world.getBlockTypeAt(worldX, worldY + 1, worldZ)] + " @ dy = 1"); } return true; } else { return false; } } /** * Determine whether two blocks would connect to each other in some way * (forming a fence, for instance). */ private static boolean wouldConnect(int blockTypeOne, int blockTypeTwo) { return ((blockTypeOne == BLK_FENCE) && ((blockTypeTwo == BLK_FENCE) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_NETHER_BRICK_FENCE) && ((blockTypeTwo == BLK_NETHER_BRICK_FENCE) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_PINE_WOOD_FENCE) && ((blockTypeTwo == BLK_PINE_WOOD_FENCE) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_BIRCH_WOOD_FENCE) && ((blockTypeTwo == BLK_BIRCH_WOOD_FENCE) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_JUNGLE_WOOD_FENCE) && ((blockTypeTwo == BLK_JUNGLE_WOOD_FENCE) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_DARK_OAK_WOOD_FENCE) && ((blockTypeTwo == BLK_DARK_OAK_WOOD_FENCE) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_ACACIA_WOOD_FENCE) && ((blockTypeTwo == BLK_ACACIA_WOOD_FENCE) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_COBBLESTONE_WALL) && ((blockTypeTwo == BLK_COBBLESTONE_WALL) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_IRON_BARS) && ((blockTypeTwo == BLK_IRON_BARS) || isSolid(blockTypeTwo))) || ((blockTypeOne == BLK_GLASS_PANE) && ((blockTypeTwo == BLK_GLASS_PANE) || isSolid(blockTypeTwo))) || (isSolid(blockTypeOne) && ((blockTypeTwo == BLK_FENCE) || (blockTypeTwo == BLK_NETHER_BRICK_FENCE) || (blockTypeTwo == BLK_PINE_WOOD_FENCE) || (blockTypeTwo == BLK_BIRCH_WOOD_FENCE) || (blockTypeTwo == BLK_JUNGLE_WOOD_FENCE) || (blockTypeTwo == BLK_DARK_OAK_WOOD_FENCE) || (blockTypeTwo == BLK_ACACIA_WOOD_FENCE) || (blockTypeTwo == BLK_COBBLESTONE_WALL) || (blockTypeTwo == BLK_IRON_BARS) || (blockTypeTwo == BLK_GLASS_PANE))); } private static boolean isSolid(int blockType) { return (blockType <= HIGHEST_KNOWN_BLOCK_ID) && (BLOCK_TRANSPARENCY[blockType] == 15); } private static Box getBounds(WPObject object, int x, int y, int z) { Point3i dimensions = object.getDimensions(); Point3i offset = object.getOffset(); return new Box(x + offset.x, x + offset.x + dimensions.x - 1, y + offset.y, y + offset.y + dimensions.y - 1, z + offset.z, z + offset.z + dimensions.z - 1); } private static void placeBlock(MinecraftWorld world, int x, int y, int z, Material material, int leafDecayMode) { final int blockType = material.blockType; if (((blockType == BLK_LEAVES) || (blockType == BLK_LEAVES2)) && (leafDecayMode != LEAF_DECAY_NO_CHANGE)) { if (leafDecayMode == LEAF_DECAY_ON) { world.setMaterialAt(x, y, z, Material.get(blockType, material.data & 0xb)); // Reset bit 2 } else { world.setMaterialAt(x, y, z, Material.get(blockType, material.data | 0x4)); // Set bit 2 } } else { world.setMaterialAt(x, y, z, material); } } private static final Set<Integer> AIR_AND_FLUIDS = new HashSet<>(Arrays.asList(BLK_AIR, BLK_WATER, BLK_STATIONARY_WATER, BLK_LAVA, BLK_STATIONARY_LAVA)); private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(WPObjectExporter.class); public static class WPObjectFixup implements Fixup { public WPObjectFixup(WPObject object, int x, int y, int z, Placement placement) { this.object = object; this.x = x; this.y = y; this.z = z; this.placement = placement; } @Override public void fixup(MinecraftWorld world, Dimension dimension) { // Recheck whether there is room if (isRoom(world, dimension, object, x, y, z, placement)) { if (logger.isTraceEnabled()) { logger.trace("Placing custom object " + object.getName() + " @ " + x + "," + y + "," + z + " in fixup"); } WPObjectExporter.renderObject(world, dimension, object, x, y, z); // Reapply the Frost layer to the area, if necessary frostExporter.setSettings(dimension.getLayerSettings(Frost.INSTANCE)); Point3i offset = object.getOffset(); Point3i dim = object.getDimensions(); Rectangle area = new Rectangle(x + offset.x, y + offset.y, dim.x, dim.y); frostExporter.render(dimension, area, null, world); // Fixups are done *after* post processing, so post process // again Box bounds = getBounds(object, x, y, z); // Include the layer below and above the object for post // processing, as those blocks may also haev been affected bounds.setZ1(Math.max(bounds.getZ1() - 1, 0)); bounds.setZ2(Math.min(bounds.getZ2() + 1, world.getMaxHeight() - 1)); try { PostProcessor.postProcess(world, bounds, null); } catch (ProgressReceiver.OperationCancelled e) { // Can't happen since we don't pass in a progress receiver throw new InternalError(); } // Fixups are done *after* lighting, so we have to relight the // area recalculateLight(world, bounds); } else if (logger.isTraceEnabled()) { logger.trace("No room for custom object " + object.getName() + " @ " + x + "," + y + "," + z + " in fixup"); } } private void recalculateLight(final MinecraftWorld world, final Box lightBox) { LightingCalculator lightingCalculator = new LightingCalculator(world); // Transpose coordinates from WP to MC coordinate system. Also // expand the box to light around it and try to account for uneven // terrain underneath the object Box dirtyArea = new Box(lightBox.getX1() - 1, lightBox.getX2() + 1, MathUtils.clamp(0, lightBox.getZ1() - 4, world.getMaxHeight() - 1), lightBox.getZ2(), lightBox.getY1() - 1, lightBox.getY2() + 1); if (dirtyArea.getVolume() == 0) { if (logger.isTraceEnabled()) { logger.trace("Dirty area for lighting calculation is empty; skipping lighting calculation"); } return; } lightingCalculator.setDirtyArea(dirtyArea); if (logger.isTraceEnabled()) { logger.trace("Recalculating light in " + lightingCalculator.getDirtyArea()); } lightingCalculator.recalculatePrimaryLight(); while (lightingCalculator.calculateSecondaryLight()); } private final WPObject object; private final int x, y, z; private final Placement placement; private static final FrostExporter frostExporter = new FrostExporter(); } public enum Placement { NONE, FLOATING, ON_LAND } }