/*
* 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.objects;
import org.pepsoft.minecraft.Chunk;
import org.pepsoft.minecraft.Entity;
import org.pepsoft.minecraft.Material;
import org.pepsoft.minecraft.TileEntity;
import org.pepsoft.util.Box;
import org.pepsoft.worldpainter.exporting.MinecraftWorld;
import javax.vecmath.Point3i;
import java.io.Serializable;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import static org.pepsoft.minecraft.Constants.BLK_AIR;
import static org.pepsoft.minecraft.Material.AIR;
/**
* A memory only combination of {@link MinecraftWorld} and {@link WPObject},
* allowing to render worlds and layers to it and then treat it as an object,
* for instance for generating previews. As such it does not support entities,
* tile entities, lighting information, etc., just basic block info. Trying to
* use the unsupported features will fail silently, except for the chunk related
* operations, which will throw an {@link UnsupportedOperationException}. The
* exception is adding chunks, which works by copying the block data over.
*
* <p>For the <code>MinecraftWorld</code> interface the supported coordinates
* are those specified by the <code>volume</code> parameter. For the
* <code>WPObject</code> interface, the coordinates are translated so that the
* lower north west corner is at 0,0,0. In other words, the
* <code>WPObject</code> has no offset and all the coordinates are positive.
*
* <p>An offset may in fact be specified but it has no effect on the coordinates
* used by this object; it is purely meant to communicate to a consumer of the
* <code>WPObject</code> that the object should be shifted when placed.
*
* @author SchmitzP
*/
public final class MinecraftWorldObject implements MinecraftWorld, WPObject {
/**
* Create a new <code>MinecraftWorldObject</code> which is initialised with
* all air and has no offset.
*
* @param name The name of the world/object.
* @param volume The volume of blocks in world coordinates which the object should encompass. Blocks outside this
* volume are returned as air for read operations and silently ignored for write operations.
* @param maxHeight The height to return from {@link MinecraftWorld#getMaxHeight()}. Must be a power of two and may
* be higher than the volume; that just means the blocks between the top of the volume and
* maxHeight won't be stored.
*/
public MinecraftWorldObject(String name, Box volume, int maxHeight) {
this(name, volume, maxHeight, null, new Point3i(0, 0, 0));
}
/**
* Create a new <code>MinecraftWorldObject</code>.
*
* @param name The name of the world/object.
* @param volume The volume of blocks in world coordinates which the object should encompass. Blocks outside this
* volume are returned as air for read operations and silently ignored for write operations.
* @param maxHeight The height to return from {@link MinecraftWorld#getMaxHeight()}. Must be a power of two and may
* be higher than the volume; that just means the blocks between the top of the volume and
* maxHeight won't be stored.
* @param lowestBlocks An optional column of materials with which the bottom of the volume should be filled. All
* other blocks will be initialised as air. May be <code>null</code>, in which case all blocks
* will be initialised as air.
* @param offset The offset to return from {@link WPObject#getOffset()}.
*/
public MinecraftWorldObject(String name, Box volume, int maxHeight, Material[] lowestBlocks, Point3i offset) {
this.name = name;
this.volume = volume;
this.maxHeight = maxHeight;
this.offset = offset;
dx = volume.getX1();
dy = volume.getY1();
dz = volume.getZ1();
dimensions = new Point3i(volume.getWidth(), volume.getLength(), volume.getHeight());
data = new short[volume.getWidth()][volume.getLength()][volume.getHeight()];
if (lowestBlocks != null) {
this.lowestBlocks = new short[lowestBlocks.length];
for (int i = 0; i < lowestBlocks.length; i++) {
this.lowestBlocks[i] = (short) ((lowestBlocks[i].blockType << 4) | lowestBlocks[i].data);
}
for (short[][] slice: data) {
for (short[] row: slice) {
System.arraycopy(this.lowestBlocks, 0, row, 0, this.lowestBlocks.length);
}
}
} else {
this.lowestBlocks = null;
}
}
// Copy constructor for clone()
private MinecraftWorldObject(String name, Box volume, int maxHeight, short[][][] data, short[] lowestBlocks, Point3i offset) {
this.name = name;
this.volume = volume;
this.maxHeight = maxHeight;
this.data = data;
this.lowestBlocks = lowestBlocks;
dx = volume.getX1();
dy = volume.getY1();
dz = volume.getZ1();
dimensions = new Point3i(volume.getWidth(), volume.getLength(), volume.getHeight());
this.offset = offset;
}
public void reset() {
for (short[][] slice: data) {
for (short[] row: slice) {
Arrays.fill(row, (short) 0);
if ((lowestBlocks != null) && (lowestBlocks.length > 0)) {
System.arraycopy(lowestBlocks, 0, row, 0, lowestBlocks.length);
}
}
}
}
public Box getVolume() {
return volume.clone();
}
// MinecraftWorld
@Override
public int getBlockTypeAt(int x, int y, int height) {
if (volume.contains(x, y, height)) {
return data[x - dx][y - dy][height - dz] >> 4;
} else {
return BLK_AIR;
}
}
@Override
public int getDataAt(int x, int y, int height) {
if (volume.contains(x, y, height)) {
return data[x - dx][y - dy][height - dz] & 0xf;
} else {
return 0;
}
}
@Override
public Material getMaterialAt(int x, int y, int height) {
if (volume.contains(x, y, height)) {
return Material.getByCombinedIndex(data[x - dx][y - dy][height - dz]);
} else {
return AIR;
}
}
@Override
public void setBlockTypeAt(int x, int y, int height, int blockType) {
if (volume.contains(x, y, height)) {
data[x - dx][y - dy][height - dz] = (short) ((blockType << 4) | (data[x - dx][y - dy][height - dz] & 0xf));
}
}
@Override
public void setDataAt(int x, int y, int height, int data) {
if (volume.contains(x, y, height)) {
this.data[x - dx][y - dy][height - dz] = (short) ((this.data[x - dx][y - dy][height - dz] & 0xfff0) | data);
}
}
@Override
public void setMaterialAt(int x, int y, int height, Material material) {
if (volume.contains(x, y, height)) {
data[x - dx][y - dy][height - dz] = (short) material.index;
}
}
@Override
public int getMaxHeight() {
return maxHeight;
}
@Override
public void addEntity(int x, int y, int height, Entity entity) {
// Do nothing
}
@Override
public void addEntity(double x, double y, double height, Entity entity) {
// Do nothing
}
@Override
public void addTileEntity(int x, int y, int height, TileEntity tileEntity) {
// Do nothing
}
@Override
public int getBlockLightLevel(int x, int y, int height) {
return 0;
}
@Override
public void setBlockLightLevel(int x, int y, int height, int blockLightLevel) {
// Do nothing
}
@Override
public int getSkyLightLevel(int x, int y, int height) {
return 15;
}
@Override
public void setSkyLightLevel(int x, int y, int height, int skyLightLevel) {
// Do nothing
}
@Override
public boolean isChunkPresent(int x, int y) {
return volume.containsXY(x << 4, y << 4);
}
/**
* Copies the block IDs and data values from the specified chunk to this
* object, insofar as it intersects the object bounds.
*
* @param chunk The chunk to copy.
*/
@Override
public void addChunk(Chunk chunk) {
for (int x = 0; x < 16; x++) {
for (int z = 0; z < 16; z++) {
int blockX = (chunk.getxPos() << 4) | x, blockY = (chunk.getzPos() << 4) | z;
if (volume.containsXY(blockX, blockY)) {
for (int y = Math.min(chunk.getHighestNonAirBlock(x, z), dz + volume.getHeight() - 1); y >= dz; y--) {
data[blockX - dx][blockY - dy][y - dz] = (short) chunk.getMaterial(x, y, z).index;
}
}
}
}
}
@Override
public int getHighestNonAirBlock(int x, int y) {
if (volume.containsXY(x, y)) {
for (int z = volume.getHeight() - 1; z >= 0; z--) {
if (data[x - dx][y - dy][z] != 0) {
return z + dz;
}
}
return -1;
} else {
return -1;
}
}
@Override
public Chunk getChunk(int x, int z) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public Chunk getChunkForEditing(int x, int z) {
throw new UnsupportedOperationException("Not supported");
}
// WPObject
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public Point3i getDimensions() {
return dimensions;
}
@Override
public Material getMaterial(int x, int y, int z) {
return Material.getByCombinedIndex(data[x][y][z]);
}
@Override
public boolean getMask(int x, int y, int z) {
return data[x][y][z] != 0;
}
@Override
public List<Entity> getEntities() {
return null;
}
@Override
public List<TileEntity> getTileEntities() {
return null;
}
@Override
public Map<String, Serializable> getAttributes() {
return null;
}
@Override
public <T extends Serializable> T getAttribute(AttributeKey<T> key) {
if (key.equals(ATTRIBUTE_OFFSET)) {
//noinspection unchecked // Responsibility of caller
return (T) offset;
} else {
return key.defaultValue;
}
}
@Override
public void setAttributes(Map<String, Serializable> attributes) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public <T extends Serializable> void setAttribute(AttributeKey<T> key, T value) {
throw new UnsupportedOperationException("Not supported");
}
@Override
public Point3i getOffset() {
return getAttribute(ATTRIBUTE_OFFSET);
}
@Override
@SuppressWarnings("CloneDoesntCallSuperClone")
public MinecraftWorldObject clone() {
return new MinecraftWorldObject(name, volume.clone(), maxHeight, data.clone(), lowestBlocks.clone(), (Point3i) offset.clone());
}
private final String name;
private final Box volume;
private final int dx, dy, dz, maxHeight;
private final Point3i dimensions, offset;
private final short[][][] data;
private final short[] lowestBlocks;
}