diff --git a/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index 0b61d2ff9..24fe89cfe 100644 --- a/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -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] ", - 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 = "[] ", + 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] ", - 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 = "[] ", + 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(); } diff --git a/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java new file mode 100644 index 000000000..460d23662 --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java @@ -0,0 +1,178 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * 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 . + */ + +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 aliasMap = new HashMap(); + + 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 getAliases() { + return Collections.unmodifiableSet(new HashSet(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; + } + +} diff --git a/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardReader.java b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardReader.java new file mode 100644 index 000000000..ccb74ee5b --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardReader.java @@ -0,0 +1,43 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * 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 . + */ + +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; + +} diff --git a/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardWriter.java b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardWriter.java new file mode 100644 index 000000000..9f5dc307b --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardWriter.java @@ -0,0 +1,44 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * 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 . + */ + +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; + +} diff --git a/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java new file mode 100644 index 000000000..2f2e50a6c --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java @@ -0,0 +1,236 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * 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 . + */ + +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 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 tileEntities = getChildTag(schematic, "TileEntities", ListTag.class).getValue(); + Map> tileEntitiesMap = new HashMap>(); + + for (Tag tag : tileEntities) { + if (!(tag instanceof CompoundTag)) continue; + CompoundTag t = (CompoundTag) tag; + + int x = 0; + int y = 0; + int z = 0; + + Map values = new HashMap(); + + for (Map.Entry 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 getChildTag(Map items, String key, Class 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); + } + +} diff --git a/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicWriter.java b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicWriter.java new file mode 100644 index 000000000..b512afb2d --- /dev/null +++ b/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicWriter.java @@ -0,0 +1,159 @@ +/* + * WorldEdit, a Minecraft world manipulation toolkit + * Copyright (C) sk89q + * 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 . + */ + +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 schematic = new HashMap(); + 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 tileEntities = new ArrayList(); + + 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 values = new HashMap(); + for (Entry 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())); + 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(); + } + }