/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package org.pepsoft.worldpainter.layers.bo2;
import org.jnbt.CompoundTag;
import org.jnbt.NBTInputStream;
import org.pepsoft.minecraft.AbstractNBTItem;
import org.pepsoft.minecraft.Entity;
import org.pepsoft.minecraft.Material;
import org.pepsoft.minecraft.TileEntity;
import org.pepsoft.worldpainter.objects.WPObject;
import javax.vecmath.Point3i;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.GZIPInputStream;
import static org.pepsoft.minecraft.Constants.BLK_AIR;
/**
*
* @author pepijn
*/
public final class Schematic extends AbstractNBTItem implements WPObject, Bo2ObjectProvider {
public Schematic(String name, CompoundTag tag, Map<String, Serializable> attributes) {
super(tag);
this.name = name;
materials = getString("Materials");
if (! materials.equalsIgnoreCase("Alpha")) {
throw new IllegalArgumentException("Unsupported materials type " + materials);
}
blocks = getByteArray("Blocks");
addBlocks = getByteArray("AddBlocks");
data = getByteArray("Data");
List<CompoundTag> entityTags = getList("Entities");
if (entityTags.isEmpty()) {
entities = null;
} else {
entities = new ArrayList<>(entityTags.size());
entities.addAll(entityTags.stream().map(Entity::fromNBT).collect(Collectors.toList()));
}
List<CompoundTag> tileEntityTags = getList("TileEntities");
if (tileEntityTags.isEmpty()) {
tileEntities = null;
} else {
tileEntities = new ArrayList<>(tileEntityTags.size());
tileEntities.addAll(tileEntityTags.stream().map(TileEntity::fromNBT).collect(Collectors.toList()));
}
width = getShort("Width");
length = getShort("Length");
height = getShort("Height");
Point3i offset = null;
if (containsTag("WEOffsetX")) {
weOffsetX = getInt("WEOffsetX");
weOffsetY = getInt("WEOffsetY");
weOffsetZ = getInt("WEOffsetZ");
// System.out.println("Schematic has offset tag: " + weOffsetX + ", " + weOffsetY + ", " + weOffsetZ);
if ((weOffsetX > -width) && (weOffsetX <= 0) && (weOffsetZ> -length) && (weOffsetZ <= 0) && (weOffsetY > -height) && (weOffsetY <= 0)) {
// Schematic offset points inside the object; use it as the default
// System.out.println("That's inside");
offset = new Point3i(weOffsetX, weOffsetZ, weOffsetY);
}
} else {
weOffsetX = weOffsetY = weOffsetZ = 0;
}
if (offset == null) {
int offsetZ = Integer.MIN_VALUE, lowestX = 0, highestX = 0, lowestY = 0, highestY = 0;
for (int z = 0; (z < height) && (offsetZ == Integer.MIN_VALUE); z++) {
for (int x = 0; x < width; x++) {
for (int y = 0; y < length; y++) {
if (getMask(x, y, z)) {
if (offsetZ == Integer.MIN_VALUE) {
offsetZ = z;
lowestX = highestX = x;
lowestY = highestY = y;
} else {
if (x < lowestX) {
lowestX = x;
} else if (x > highestX) {
highestX = x;
}
if (y < lowestY) {
lowestY = y;
} else if (y > highestY) {
highestY = y;
}
}
}
}
}
}
if (offsetZ > Integer.MIN_VALUE) {
offset = new Point3i(-(lowestX + highestX) / 2, -(lowestY + highestY) / 2, -offsetZ);
}
}
if ((offset != null) && ((offset.x != 0) || (offset.y != 0) || (offset.z != 0))) {
if (attributes == null) {
attributes = new HashMap<>();
}
attributes.put(ATTRIBUTE_OFFSET.key, offset);
}
if (containsTag("WEOriginX")) {
weOriginX = getInt("WEOriginX");
weOriginY = getInt("WEOriginY");
weOriginZ = getInt("WEOriginZ");
} else {
weOriginX = weOriginY = weOriginZ = 0;
}
dimensions = new Point3i(width, length, height);
this.attributes = attributes;
}
// WPObject
@Override
public Point3i getDimensions() {
return dimensions;
}
@Override
public Material getMaterial(int x, int y, int z) {
final int offset = blockOffset(x, y, z);
int blockId = blocks[offset] & 0xFF;
if (addBlocks != null) {
if ((offset & 1) == 0) {
// Even offset; first nibble
blockId |= (addBlocks[offset >> 1] & 0x0f) << 8;
} else {
// Odd offset; second nibble
blockId |= (addBlocks[offset >> 1] & 0xf0) << 4;
}
}
return Material.get(blockId, data[offset] & 0xF);
}
@Override
public boolean getMask(int x, int y, int z) {
return blocks[blockOffset(x, y, z)] != BLK_AIR;
}
@Override
public List<Entity> getEntities() {
return entities;
}
@Override
public List<TileEntity> getTileEntities() {
return tileEntities;
}
@Override
public String getName() {
return name;
}
@Override
public void setName(String name) {
this.name = name;
}
// Bo2ObjectProvider
@Override
public WPObject getObject() {
return this;
}
@Override
public List<WPObject> getAllObjects() {
return Collections.singletonList(this);
}
@Override
public Map<String, Serializable> getAttributes() {
return attributes;
}
@Override
@SuppressWarnings("unchecked") // Responsibility of caller
public <T extends Serializable> T getAttribute(AttributeKey<T> key) {
return key.get(attributes);
}
@Override
public void setAttributes(Map<String, Serializable> attributes) {
this.attributes = attributes;
}
@Override
public <T extends Serializable> void setAttribute(AttributeKey<T> key, T value) {
if (value != null) {
if (attributes == null) {
attributes = new HashMap<>();
}
attributes.put(key.key, value);
} else if (attributes != null) {
attributes.remove(key.key);
if (attributes.isEmpty()) {
attributes = null;
}
}
}
@Override
public void setSeed(long seed) {
// Do nothing
}
@Override
public Point3i getOffset() {
return getAttribute(ATTRIBUTE_OFFSET);
}
@Override
public Schematic clone() {
Schematic clone = (Schematic) super.clone();
clone.dimensions = (Point3i) dimensions.clone();
if (origin != null) {
clone.origin = (Point3i) origin.clone();
}
if (entities != null) {
clone.entities = new ArrayList<>(entities.size());
clone.entities.addAll(entities.stream().map(entity -> (Entity) entity.clone()).collect(Collectors.toList()));
}
if (tileEntities != null) {
clone.tileEntities = new ArrayList<>(tileEntities.size());
clone.tileEntities.addAll(tileEntities.stream().map(tileEntity -> (TileEntity) tileEntity.clone()).collect(Collectors.toList()));
}
if (attributes != null) {
clone.attributes = new HashMap<>(attributes);
}
return clone;
}
/**
* Load a custom object in schematic format from a file. The name of the
* object will be the name of the file, minus the extension.
*
* @param file The file from which to load the object.
* @return A new <code>Schematic</code> containing the contents of the
* specified file.
* @throws IOException If an I/O error occurred while reading the file.
*/
public static Schematic load(File file) throws IOException {
String name = file.getName();
int p = name.lastIndexOf('.');
if (p != -1) {
name = name.substring(0, p);
}
return load(name, file);
}
/**
* Load a custom object in schematic format from a file.
*
* @param name The name of the object.
* @param file The file from which to load the object.
* @return A new <code>Schematic</code> containing the contents of the
* specified file.
* @throws IOException If an I/O error occurred while reading the file.
*/
public static Schematic load(String name, File file) throws IOException {
Schematic object = load(name, new FileInputStream(file));
object.setAttribute(WPObject.ATTRIBUTE_FILE, file);
return object;
}
/**
* Load a custom object in schematic format from an input stream. The stream
* is closed before exiting the method.
*
* @param name The name of the object.
* @param stream The input stream from which to load the object.
* @return A new <code>Schematic</code> containing the contents of the
* specified stream.
* @throws IOException If an I/O error occurred while reading the stream.
*/
public static Schematic load(String name, InputStream stream) throws IOException {
InputStream in = new BufferedInputStream(stream);
//noinspection TryFinallyCanBeTryWithResources // Not possible due to assignment of 'in' inside block
try {
byte[] magicNumber = new byte[2];
in.mark(2);
in.read(magicNumber);
in.reset();
if ((magicNumber[0] == (byte) 0x1f) && (magicNumber[1] == (byte) 0x8b)) {
in = new GZIPInputStream(in);
}
NBTInputStream nbtIn = new NBTInputStream(in);
CompoundTag tag = (CompoundTag) nbtIn.readTag();
return new Schematic(name, tag, null);
} finally {
in.close();
}
}
private int blockOffset(int x, int y, int z) {
return x + width * y + width * length * z;
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// Legacy
if (origin != null) {
if (attributes == null) {
attributes = new HashMap<>();
}
attributes.put(ATTRIBUTE_OFFSET.key, new Point3i(-origin.x, -origin.y, -origin.z));
origin = null;
}
if (version == 0) {
if (! attributes.containsKey(ATTRIBUTE_LEAF_DECAY_MODE.key)) {
attributes.put(ATTRIBUTE_LEAF_DECAY_MODE.key, LEAF_DECAY_ON);
}
version = 1;
}
}
private String name;
private final String materials;
private final byte[] data, blocks, addBlocks;
private final short width, length, height;
private final int weOffsetX, weOffsetY, weOffsetZ;
private final int weOriginX, weOriginY, weOriginZ;
private List<Entity> entities;
private List<TileEntity> tileEntities;
private Point3i dimensions;
@Deprecated
private Point3i origin;
private Map<String, Serializable> attributes;
private int version = 1;
private static final long serialVersionUID = 1L;
}