/*
* WorldEdit, a Minecraft world manipulation toolkit
* Copyright (C) sk89q <http://www.sk89q.com>
* Copyright (C) WorldEdit team and contributors
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by the
* Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.sk89q.worldedit.extent.clipboard.io;
import com.sk89q.jnbt.ByteArrayTag;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.IntTag;
import com.sk89q.jnbt.ListTag;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NamedTag;
import com.sk89q.jnbt.ShortTag;
import com.sk89q.jnbt.StringTag;
import com.sk89q.jnbt.Tag;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.blocks.BaseBlock;
import com.sk89q.worldedit.entity.BaseEntity;
import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.util.Location;
import com.sk89q.worldedit.world.registry.WorldData;
import com.sk89q.worldedit.world.storage.NBTConversions;
import javax.annotation.Nullable;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Reads schematic files based that are compatible with MCEdit and other editors.
*/
public class SchematicReader implements ClipboardReader {
private static final Logger log = Logger.getLogger(SchematicReader.class.getCanonicalName());
private final NBTInputStream inputStream;
/**
* Create a new instance.
*
* @param inputStream the input stream to read from
*/
public SchematicReader(NBTInputStream inputStream) {
checkNotNull(inputStream);
this.inputStream = inputStream;
}
@Override
public Clipboard read(WorldData data) throws IOException {
// Schematic tag
NamedTag rootTag = inputStream.readNamedTag();
if (!rootTag.getName().equals("Schematic")) {
throw new IOException("Tag 'Schematic' does not exist or is not first");
}
CompoundTag schematicTag = (CompoundTag) rootTag.getTag();
// Check
Map<String, Tag> schematic = schematicTag.getValue();
if (!schematic.containsKey("Blocks")) {
throw new IOException("Schematic file is missing a 'Blocks' tag");
}
// Check type of Schematic
String materials = requireTag(schematic, "Materials", StringTag.class).getValue();
if (!materials.equals("Alpha")) {
throw new IOException("Schematic file is not an Alpha schematic");
}
// ====================================================================
// Metadata
// ====================================================================
Vector origin;
Region region;
// Get information
short width = requireTag(schematic, "Width", ShortTag.class).getValue();
short height = requireTag(schematic, "Height", ShortTag.class).getValue();
short length = requireTag(schematic, "Length", ShortTag.class).getValue();
try {
int originX = requireTag(schematic, "WEOriginX", IntTag.class).getValue();
int originY = requireTag(schematic, "WEOriginY", IntTag.class).getValue();
int originZ = requireTag(schematic, "WEOriginZ", IntTag.class).getValue();
Vector min = new Vector(originX, originY, originZ);
int offsetX = requireTag(schematic, "WEOffsetX", IntTag.class).getValue();
int offsetY = requireTag(schematic, "WEOffsetY", IntTag.class).getValue();
int offsetZ = requireTag(schematic, "WEOffsetZ", IntTag.class).getValue();
Vector offset = new Vector(offsetX, offsetY, offsetZ);
origin = min.subtract(offset);
region = new CuboidRegion(min, min.add(width, height, length).subtract(Vector.ONE));
} catch (IOException ignored) {
origin = new Vector(0, 0, 0);
region = new CuboidRegion(origin, origin.add(width, height, length).subtract(Vector.ONE));
}
// ====================================================================
// Blocks
// ====================================================================
// Get blocks
byte[] blockId = requireTag(schematic, "Blocks", ByteArrayTag.class).getValue();
byte[] blockData = requireTag(schematic, "Data", ByteArrayTag.class).getValue();
byte[] addId = new byte[0];
short[] blocks = new short[blockId.length]; // Have to later combine IDs
// We support 4096 block IDs using the same method as vanilla Minecraft, where
// the highest 4 bits are stored in a separate byte array.
if (schematic.containsKey("AddBlocks")) {
addId = requireTag(schematic, "AddBlocks", ByteArrayTag.class).getValue();
}
// Combine the AddBlocks data with the first 8-bit block ID
for (int index = 0; index < blockId.length; index++) {
if ((index >> 1) >= addId.length) { // No corresponding AddBlocks index
blocks[index] = (short) (blockId[index] & 0xFF);
} else {
if ((index & 1) == 0) {
blocks[index] = (short) (((addId[index >> 1] & 0x0F) << 8) + (blockId[index] & 0xFF));
} else {
blocks[index] = (short) (((addId[index >> 1] & 0xF0) << 4) + (blockId[index] & 0xFF));
}
}
}
// Need to pull out tile entities
List<Tag> tileEntities = requireTag(schematic, "TileEntities", ListTag.class).getValue();
Map<BlockVector, Map<String, Tag>> tileEntitiesMap = new HashMap<BlockVector, Map<String, Tag>>();
for (Tag tag : tileEntities) {
if (!(tag instanceof CompoundTag)) continue;
CompoundTag t = (CompoundTag) tag;
int x = 0;
int y = 0;
int z = 0;
Map<String, Tag> values = new HashMap<String, Tag>();
for (Map.Entry<String, Tag> entry : t.getValue().entrySet()) {
if (entry.getKey().equals("x")) {
if (entry.getValue() instanceof IntTag) {
x = ((IntTag) entry.getValue()).getValue();
}
} else if (entry.getKey().equals("y")) {
if (entry.getValue() instanceof IntTag) {
y = ((IntTag) entry.getValue()).getValue();
}
} else if (entry.getKey().equals("z")) {
if (entry.getValue() instanceof IntTag) {
z = ((IntTag) entry.getValue()).getValue();
}
}
values.put(entry.getKey(), entry.getValue());
}
BlockVector vec = new BlockVector(x, y, z);
tileEntitiesMap.put(vec, values);
}
BlockArrayClipboard clipboard = new BlockArrayClipboard(region);
clipboard.setOrigin(origin);
// Don't log a torrent of errors
int failedBlockSets = 0;
for (int x = 0; x < width; ++x) {
for (int y = 0; y < height; ++y) {
for (int z = 0; z < length; ++z) {
int index = y * width * length + z * width + x;
BlockVector pt = new BlockVector(x, y, z);
BaseBlock block = new BaseBlock(blocks[index], blockData[index]);
if (tileEntitiesMap.containsKey(pt)) {
block.setNbtData(new CompoundTag(tileEntitiesMap.get(pt)));
}
try {
clipboard.setBlock(region.getMinimumPoint().add(pt), block);
} catch (WorldEditException e) {
switch (failedBlockSets) {
case 0:
log.log(Level.WARNING, "Failed to set block on a Clipboard", e);
break;
case 1:
log.log(Level.WARNING, "Failed to set block on a Clipboard (again) -- no more messages will be logged", e);
break;
default:
}
failedBlockSets++;
}
}
}
}
// ====================================================================
// Entities
// ====================================================================
try {
List<Tag> entityTags = requireTag(schematic, "Entities", ListTag.class).getValue();
for (Tag tag : entityTags) {
if (tag instanceof CompoundTag) {
CompoundTag compound = (CompoundTag) tag;
String id = compound.getString("id");
Location location = NBTConversions.toLocation(clipboard, compound.getListTag("Pos"), compound.getListTag("Rotation"));
if (!id.isEmpty()) {
BaseEntity state = new BaseEntity(id, compound);
clipboard.createEntity(location, state);
}
}
}
} catch (IOException ignored) { // No entities? No problem
}
return clipboard;
}
private static <T extends Tag> T requireTag(Map<String, Tag> items, String key, Class<T> expected) throws IOException {
if (!items.containsKey(key)) {
throw new IOException("Schematic file is missing a \"" + key + "\" tag");
}
Tag tag = items.get(key);
if (!expected.isInstance(tag)) {
throw new IOException(key + " tag is not of tag type " + expected.getName());
}
return expected.cast(tag);
}
@Nullable
private static <T extends Tag> T getTag(CompoundTag tag, Class<T> expected, String key) {
Map<String, Tag> items = tag.getValue();
if (!items.containsKey(key)) {
return null;
}
Tag test = items.get(key);
if (!expected.isInstance(test)) {
return null;
}
return expected.cast(test);
}
}