Add new Clipboard-compatible .schematic reader/writer.

This commit is contained in:
sk89q 2014-07-09 21:08:34 -07:00
parent 7b0e5a977f
commit 47ad03a013
6 changed files with 787 additions and 33 deletions

View File

@ -19,11 +19,14 @@
package com.sk89q.worldedit.command;
import com.google.common.io.Closer;
import com.sk89q.minecraft.util.commands.Command;
import com.sk89q.minecraft.util.commands.CommandContext;
import com.sk89q.minecraft.util.commands.CommandException;
import com.sk89q.minecraft.util.commands.CommandPermissions;
import com.sk89q.worldedit.EditSession;
import com.sk89q.worldedit.EmptyClipboardException;
import com.sk89q.worldedit.FilenameException;
import com.sk89q.worldedit.FilenameResolutionException;
import com.sk89q.worldedit.LocalConfiguration;
import com.sk89q.worldedit.LocalSession;
@ -31,12 +34,25 @@ import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.entity.Player;
import com.sk89q.worldedit.extension.platform.Actor;
import com.sk89q.worldedit.schematic.SchematicFormat;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader;
import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter;
import com.sk89q.worldedit.session.ClipboardHolder;
import com.sk89q.worldedit.util.command.parametric.Optional;
import com.sk89q.worldedit.world.registry.WorldData;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Comparator;
import java.util.logging.Level;
import java.util.logging.Logger;
import static com.google.common.base.Preconditions.checkNotNull;
@ -45,6 +61,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
*/
public class SchematicCommands {
private static final Logger log = Logger.getLogger(SchematicCommands.class.getCanonicalName());
private final WorldEdit worldEdit;
/**
@ -58,36 +75,114 @@ public class SchematicCommands {
}
@Command(
aliases = { "load", "l" },
usage = "[format] <filename>",
desc = "Load a file into your clipboard",
help = "Load a schematic file into your clipboard\n" +
"Format is a format from \"//schematic formats\"\n" +
"If the format is not provided, WorldEdit will\n" +
"attempt to automatically detect the format of the schematic",
flags = "f",
min = 1,
max = 2
aliases = { "load" },
usage = "[<format>] <filename>",
desc = "Load a schematic into your clipboard",
min = 0,
max = 1
)
@CommandPermissions({"worldedit.clipboard.load", "worldedit.schematic.load"}) // TODO: Remove 'clipboard' perm
public void load(Player player, LocalSession session, EditSession editSession, CommandContext args) throws WorldEditException, CommandException {
// TODO: Update for new clipboard
throw new CommandException("Needs to be re-written again");
@Deprecated
@CommandPermissions({ "worldedit.clipboard.load", "worldedit.schematic.load" })
public void load(Player player, LocalSession session, @Optional("schematic") String formatName, String filename) throws FilenameException {
LocalConfiguration config = worldEdit.getConfiguration();
File dir = worldEdit.getWorkingDirectoryFile(config.saveDir);
File f = worldEdit.getSafeOpenFile(player, dir, filename, "schematic", "schematic");
if (!f.exists()) {
player.printError("Schematic " + filename + " does not exist!");
return;
}
ClipboardFormat format = ClipboardFormat.findByAlias(formatName);
if (format == null) {
player.printError("Unknown schematic format: " + formatName);
return;
}
Closer closer = Closer.create();
try {
String filePath = f.getCanonicalPath();
String dirPath = dir.getCanonicalPath();
if (!filePath.substring(0, dirPath.length()).equals(dirPath)) {
player.printError("Clipboard file could not read or it does not exist.");
} else {
FileInputStream fis = closer.register(new FileInputStream(f));
BufferedInputStream bis = closer.register(new BufferedInputStream(fis));
ClipboardReader reader = format.getReader(bis);
WorldData worldData = player.getWorld().getWorldData();
Clipboard clipboard = reader.read(player.getWorld().getWorldData());
session.setClipboard(new ClipboardHolder(clipboard, worldData));
log.info(player.getName() + " loaded " + filePath);
player.print(filename + " loaded. Paste it with //paste");
}
} catch (IOException e) {
player.printError("Schematic could not read or it does not exist: " + e.getMessage());
log.log(Level.WARNING, "Failed to load a saved clipboard", e);
} finally {
try {
closer.close();
} catch (IOException ignored) {
}
}
}
@Command(
aliases = { "save", "s" },
usage = "[format] <filename>",
desc = "Save your clipboard to file",
help = "Save your clipboard to file\n" +
"Format is a format from \"//schematic formats\"\n",
min = 1,
max = 2
aliases = { "save" },
usage = "[<format>] <filename>",
desc = "Save a schematic into your clipboard",
min = 0,
max = 1
)
@CommandPermissions({"worldedit.clipboard.save", "worldedit.schematic.save"}) // TODO: Remove 'clipboard' perm
public void save(Player player, LocalSession session, EditSession editSession, CommandContext args) throws WorldEditException, CommandException {
// TODO: Update for new clipboard
throw new CommandException("Needs to be re-written again");
@Deprecated
@CommandPermissions({ "worldedit.clipboard.save", "worldedit.schematic.save" })
public void save(Player player, LocalSession session, @Optional("schematic") String formatName, String filename) throws FilenameException, CommandException, EmptyClipboardException {
LocalConfiguration config = worldEdit.getConfiguration();
File dir = worldEdit.getWorkingDirectoryFile(config.saveDir);
File f = worldEdit.getSafeSaveFile(player, dir, filename, "schematic", "schematic");
if (!f.exists()) {
player.printError("Schematic " + filename + " does not exist!");
return;
}
ClipboardFormat format = ClipboardFormat.findByAlias(formatName);
if (format == null) {
player.printError("Unknown schematic format: " + formatName);
return;
}
ClipboardHolder holder = session.getClipboard();
Closer closer = Closer.create();
try {
// Create parent directories
File parent = f.getParentFile();
if (parent != null && !parent.exists()) {
if (!parent.mkdirs()) {
throw new CommandException("Could not create folder for schematics!");
}
}
FileOutputStream fos = closer.register(new FileOutputStream(f));
BufferedOutputStream bos = closer.register(new BufferedOutputStream(fos));
ClipboardWriter writer = closer.register(format.getWriter(bos));
writer.write(holder.getClipboard(), holder.getWorldData());
log.info(player.getName() + " saved " + f.getCanonicalPath());
player.print(filename + " saved.");
} catch (IOException e) {
player.printError("Schematic could not written: " + e.getMessage());
log.log(Level.WARNING, "Failed to write a saved clipboard", e);
} finally {
try {
closer.close();
} catch (IOException ignored) {
}
}
}
@Command(
@ -127,13 +222,13 @@ public class SchematicCommands {
)
@CommandPermissions("worldedit.schematic.formats")
public void formats(Actor actor) throws WorldEditException {
actor.print("Available schematic formats (Name: Lookup names)");
actor.print("Available clipboard formats (Name: Lookup names)");
StringBuilder builder;
boolean first = true;
for (SchematicFormat format : SchematicFormat.getFormats()) {
for (ClipboardFormat format : ClipboardFormat.values()) {
builder = new StringBuilder();
builder.append(format.getName()).append(": ");
for (String lookupName : format.getLookupNames()) {
builder.append(format.name()).append(": ");
for (String lookupName : format.getAliases()) {
if (!first) {
builder.append(", ");
}
@ -204,9 +299,8 @@ public class SchematicCommands {
}
build.append("\n\u00a79");
SchematicFormat format = SchematicFormat.getFormat(file);
build.append(prefix).append(file.getName())
.append(": ").append(format == null ? "Unknown" : format.getName());
ClipboardFormat format = ClipboardFormat.findByFile(file);
build.append(prefix).append(file.getName()).append(": ").append(format == null ? "Unknown" : format.name());
}
return build.toString();
}

View File

@ -0,0 +1,178 @@
/*
* 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.NBTConstants;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NBTOutputStream;
import javax.annotation.Nullable;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* A collection of supported clipboard formats.
*/
public enum ClipboardFormat {
/**
* The Schematic format used by many software.
*/
SCHEMATIC("mcedit", "mce", "schematic") {
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
NBTInputStream nbtStream = new NBTInputStream(new GZIPInputStream(inputStream));
return new SchematicReader(nbtStream);
}
@Override
public ClipboardWriter getWriter(OutputStream outputStream) throws IOException {
NBTOutputStream nbtStream = new NBTOutputStream(new GZIPOutputStream(outputStream));
return new SchematicWriter(nbtStream);
}
@Override
public boolean isFormat(File file) {
DataInputStream str = null;
try {
str = new DataInputStream(new GZIPInputStream(new FileInputStream(file)));
if ((str.readByte() & 0xFF) != NBTConstants.TYPE_COMPOUND) {
return false;
}
byte[] nameBytes = new byte[str.readShort() & 0xFFFF];
str.readFully(nameBytes);
String name = new String(nameBytes, NBTConstants.CHARSET);
return name.equals("Schematic");
} catch (IOException e) {
return false;
} finally {
if (str != null) {
try {
str.close();
} catch (IOException ignored) {
}
}
}
}
};
private static final Map<String, ClipboardFormat> aliasMap = new HashMap<String, ClipboardFormat>();
private final String[] aliases;
/**
* Create a new instance.
*
* @param aliases an array of aliases by which this format may be referred to
*/
private ClipboardFormat(String ... aliases) {
this.aliases = aliases;
}
/**
* Get a set of aliases.
*
* @return a set of aliases
*/
public Set<String> getAliases() {
return Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(aliases)));
}
/**
* Create a reader.
*
* @param inputStream the input stream
* @return a reader
* @throws IOException thrown on I/O error
*/
public abstract ClipboardReader getReader(InputStream inputStream) throws IOException;
/**
* Create a writer.
*
* @param outputStream the output stream
* @return a writer
* @throws IOException thrown on I/O error
*/
public abstract ClipboardWriter getWriter(OutputStream outputStream) throws IOException;
/**
* Return whether the given file is of this format.
*
* @param file the file
* @return true if the given file is of this format
*/
public abstract boolean isFormat(File file);
static {
for (ClipboardFormat format : EnumSet.allOf(ClipboardFormat.class)) {
for (String key : format.aliases) {
aliasMap.put(key, format);
}
}
}
/**
* Find the clipboard format named by the given alias.
*
* @param alias the alias
* @return the format, otherwise null if none is matched
*/
@Nullable
public static ClipboardFormat findByAlias(String alias) {
checkNotNull(alias);
return aliasMap.get(alias.toLowerCase().trim());
}
/**
* Detect the format given a file.
*
* @param file the file
* @return the format, otherwise null if one cannot be detected
*/
@Nullable
public static ClipboardFormat findByFile(File file) {
checkNotNull(file);
for (ClipboardFormat format : EnumSet.allOf(ClipboardFormat.class)) {
if (format.isFormat(file)) {
return format;
}
}
return null;
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.world.registry.WorldData;
import java.io.IOException;
/**
* Reads {@code Clipboard}s.
*
* @see Clipboard
*/
public interface ClipboardReader {
/**
* Read a {@code Clipboard}.
*
* @param data the world data space to convert the blocks to
* @return the read clipboard
* @throws IOException thrown on I/O error
*/
Clipboard read(WorldData data) throws IOException;
}

View File

@ -0,0 +1,44 @@
/*
* 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.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.world.registry.WorldData;
import java.io.Closeable;
import java.io.IOException;
/**
* Writes {@code Clipboard}s.
*
* @see Clipboard
*/
public interface ClipboardWriter extends Closeable {
/**
* Writes a clipboard.
*
* @param clipboard the clipboard
* @param data the world data instance
* @throws IOException thrown on I/O error
*/
void write(Clipboard clipboard, WorldData data) throws IOException;
}

View File

@ -0,0 +1,236 @@
/*
* 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.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.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.world.registry.WorldData;
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
CompoundTag schematicTag = (CompoundTag) inputStream.readTag();
if (!schematicTag.getName().equals("Schematic")) {
throw new IOException("Tag 'Schematic' does not exist or is not first");
}
// 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 = getChildTag(schematic, "Materials", StringTag.class).getValue();
if (!materials.equals("Alpha")) {
throw new IOException("Schematic file is not an Alpha schematic");
}
// Parse origin and region from WEOrigin and WEOffset
Vector origin;
Region region;
// Get information
short width = getChildTag(schematic, "Width", ShortTag.class).getValue();
short height = getChildTag(schematic, "Height", ShortTag.class).getValue();
short length = getChildTag(schematic, "Length", ShortTag.class).getValue();
try {
int originX = getChildTag(schematic, "WEOriginX", IntTag.class).getValue();
int originY = getChildTag(schematic, "WEOriginY", IntTag.class).getValue();
int originZ = getChildTag(schematic, "WEOriginZ", IntTag.class).getValue();
Vector min = new Vector(originX, originY, originZ);
int offsetX = getChildTag(schematic, "WEOffsetX", IntTag.class).getValue();
int offsetY = getChildTag(schematic, "WEOffsetY", IntTag.class).getValue();
int offsetZ = getChildTag(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));
}
// Get blocks
byte[] blockId = getChildTag(schematic, "Blocks", ByteArrayTag.class).getValue();
byte[] blockData = getChildTag(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 = getChildTag(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 = getChildTag(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++;
}
}
}
}
return clipboard;
}
/**
* Get child tag of a NBT structure.
*
* @param items The parent tag map
* @param key The name of the tag to get
* @param expected The expected type of the tag
* @return child tag casted to the expected type
* @throws IOException if the tag does not exist or the tag is not of the expected type
*/
private static <T extends Tag> T getChildTag(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);
}
}

View File

@ -0,0 +1,159 @@
/*
* 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.NBTOutputStream;
import com.sk89q.jnbt.ShortTag;
import com.sk89q.jnbt.StringTag;
import com.sk89q.jnbt.Tag;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.blocks.BaseBlock;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.world.registry.WorldData;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Writes schematic files based that are compatible with MCEdit and other editors.
*/
public class SchematicWriter implements ClipboardWriter {
private static final int MAX_SIZE = Short.MAX_VALUE - Short.MIN_VALUE;
private final NBTOutputStream outputStream;
/**
* Create a new schematic writer.
*
* @param outputStream the output stream to write to
*/
public SchematicWriter(NBTOutputStream outputStream) {
checkNotNull(outputStream);
this.outputStream = outputStream;
}
@Override
public void write(Clipboard clipboard, WorldData data) throws IOException {
Region region = clipboard.getRegion();
Vector origin = clipboard.getOrigin();
Vector min = region.getMinimumPoint();
Vector offset = min.subtract(origin);
int width = region.getWidth();
int height = region.getHeight();
int length = region.getLength();
if (width > MAX_SIZE) {
throw new IllegalArgumentException("Width of region too large for a .schematic");
}
if (height > MAX_SIZE) {
throw new IllegalArgumentException("Height of region too large for a .schematic");
}
if (length > MAX_SIZE) {
throw new IllegalArgumentException("Length of region too large for a .schematic");
}
HashMap<String, Tag> schematic = new HashMap<String, Tag>();
schematic.put("Width", new ShortTag("Width", (short) width));
schematic.put("Length", new ShortTag("Length", (short) length));
schematic.put("Height", new ShortTag("Height", (short) height));
schematic.put("Materials", new StringTag("Materials", "Alpha"));
schematic.put("WEOriginX", new IntTag("WEOriginX", min.getBlockX()));
schematic.put("WEOriginY", new IntTag("WEOriginY", min.getBlockY()));
schematic.put("WEOriginZ", new IntTag("WEOriginZ", min.getBlockZ()));
schematic.put("WEOffsetX", new IntTag("WEOffsetX", offset.getBlockX()));
schematic.put("WEOffsetY", new IntTag("WEOffsetY", offset.getBlockY()));
schematic.put("WEOffsetZ", new IntTag("WEOffsetZ", offset.getBlockZ()));
// Copy
byte[] blocks = new byte[width * height * length];
byte[] addBlocks = null;
byte[] blockData = new byte[width * height * length];
ArrayList<Tag> tileEntities = new ArrayList<Tag>();
for (Vector point : region) {
Vector relative = point.subtract(min);
int x = relative.getBlockX();
int y = relative.getBlockY();
int z = relative.getBlockZ();
int index = y * width * length + z * width + x;
BaseBlock block = clipboard.getBlock(point);
// Save 4096 IDs in an AddBlocks section
if (block.getType() > 255) {
if (addBlocks == null) { // Lazily create section
addBlocks = new byte[(blocks.length >> 1) + 1];
}
addBlocks[index >> 1] = (byte) (((index & 1) == 0) ?
addBlocks[index >> 1] & 0xF0 | (block.getType() >> 8) & 0xF
: addBlocks[index >> 1] & 0xF | ((block.getType() >> 8) & 0xF) << 4);
}
blocks[index] = (byte) block.getType();
blockData[index] = (byte) block.getData();
// Store TileEntity data
CompoundTag rawTag = block.getNbtData();
if (rawTag != null) {
Map<String, Tag> values = new HashMap<String, Tag>();
for (Entry<String, Tag> entry : rawTag.getValue().entrySet()) {
values.put(entry.getKey(), entry.getValue());
}
values.put("id", new StringTag("id", block.getNbtId()));
values.put("x", new IntTag("x", x));
values.put("y", new IntTag("y", y));
values.put("z", new IntTag("z", z));
CompoundTag tileEntityTag = new CompoundTag("TileEntity", values);
tileEntities.add(tileEntityTag);
}
}
schematic.put("Blocks", new ByteArrayTag("Blocks", blocks));
schematic.put("Data", new ByteArrayTag("Data", blockData));
schematic.put("Entities", new ListTag("Entities", CompoundTag.class, new ArrayList<Tag>()));
schematic.put("TileEntities", new ListTag("TileEntities", CompoundTag.class, tileEntities));
if (addBlocks != null) {
schematic.put("AddBlocks", new ByteArrayTag("AddBlocks", addBlocks));
}
// Build and output
CompoundTag schematicTag = new CompoundTag("Schematic", schematic);
outputStream.writeTag(schematicTag);
}
@Override
public void close() throws IOException {
outputStream.close();
}
}