From bc2279f2f612345032d526966a353054654d9a52 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 24 Jul 2018 11:50:57 +1000 Subject: [PATCH] Refactor schematics to use kenzierocks registerable system. --- .../src/main/java/com/sk89q/jnbt/ListTag.java | 5 +- .../com/sk89q/worldedit/CuboidClipboard.java | 388 ------------------ .../java/com/sk89q/worldedit/WorldEdit.java | 92 ++--- .../worldedit/command/SchematicCommands.java | 24 +- .../clipboard/io/BuiltInClipboardFormat.java | 107 +++++ .../extent/clipboard/io/ClipboardFormat.java | 109 +---- .../extent/clipboard/io/ClipboardFormats.java | 125 ++++++ .../extent/clipboard/io/ClipboardReader.java | 3 +- .../extent/clipboard/io/SchematicReader.java | 4 + .../schematic/MCEditSchematicFormat.java | 249 ----------- .../worldedit/schematic/SchematicFormat.java | 107 ----- 11 files changed, 309 insertions(+), 904 deletions(-) delete mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/BuiltInClipboardFormat.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormats.java delete mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/schematic/MCEditSchematicFormat.java delete mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/schematic/SchematicFormat.java diff --git a/worldedit-core/src/main/java/com/sk89q/jnbt/ListTag.java b/worldedit-core/src/main/java/com/sk89q/jnbt/ListTag.java index 3cbe42977..1b8d1fa21 100644 --- a/worldedit-core/src/main/java/com/sk89q/jnbt/ListTag.java +++ b/worldedit-core/src/main/java/com/sk89q/jnbt/ListTag.java @@ -79,11 +79,10 @@ public final class ListTag extends Tag { */ @Nullable public Tag getIfExists(int index) { - try { - return value.get(index); - } catch (NoSuchElementException e) { + if (index >= value.size()) { return null; } + return value.get(index); } /** diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java b/worldedit-core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java deleted file mode 100644 index b286802bb..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/CuboidClipboard.java +++ /dev/null @@ -1,388 +0,0 @@ -/* - * 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; - -import static com.google.common.base.Preconditions.checkNotNull; - -import com.sk89q.worldedit.blocks.BaseBlock; -import com.sk89q.worldedit.command.ClipboardCommands; -import com.sk89q.worldedit.command.SchematicCommands; -import com.sk89q.worldedit.entity.Entity; -import com.sk89q.worldedit.extent.Extent; -import com.sk89q.worldedit.extent.clipboard.Clipboard; -import com.sk89q.worldedit.function.operation.ForwardExtentCopy; -import com.sk89q.worldedit.regions.CuboidRegion; -import com.sk89q.worldedit.regions.Region; -import com.sk89q.worldedit.world.block.BlockStateHolder; -import com.sk89q.worldedit.world.block.BlockTypes; - -import java.util.ArrayList; -import java.util.List; - -/** - * The clipboard remembers the state of a cuboid region. - * - * @deprecated This is slowly being replaced with {@link Clipboard}, which is - * far more versatile. Transforms are supported using affine - * transformations and full entity support is provided because - * the clipboard properly implements {@link Extent}. However, - * the new clipboard class is only available in WorldEdit 6.x and - * beyond. We intend on keeping this deprecated class in WorldEdit - * for an extended amount of time so there is no rush to - * switch (but new features will not be supported). To copy between - * a clipboard and a world (or between any two {@code Extent}s), - * one can use {@link ForwardExtentCopy}. See - * {@link ClipboardCommands} and {@link SchematicCommands} for - * more information. - */ -@Deprecated -public class CuboidClipboard { - - /** - * An enum of possible flip directions. - */ - public enum FlipDirection { - NORTH_SOUTH, - WEST_EAST, - UP_DOWN - } - - private BlockStateHolder[][][] data; - private Vector offset; - private Vector origin; - private Vector size; - private List entities = new ArrayList<>(); - - /** - * Constructs the clipboard. - * - * @param size the dimensions of the clipboard (should be at least 1 on every dimension) - */ - public CuboidClipboard(Vector size) { - checkNotNull(size); - - this.size = size; - data = new BlockStateHolder[size.getBlockX()][size.getBlockY()][size.getBlockZ()]; - origin = new Vector(); - offset = new Vector(); - } - - /** - * Constructs the clipboard. - * - * @param size the dimensions of the clipboard (should be at least 1 on every dimension) - * @param origin the origin point where the copy was made, which must be the - * {@link CuboidRegion#getMinimumPoint()} relative to the copy - */ - public CuboidClipboard(Vector size, Vector origin) { - checkNotNull(size); - checkNotNull(origin); - - this.size = size; - data = new BlockStateHolder[size.getBlockX()][size.getBlockY()][size.getBlockZ()]; - this.origin = origin; - offset = new Vector(); - } - - /** - * Constructs the clipboard. - * - * @param size the dimensions of the clipboard (should be at least 1 on every dimension) - * @param origin the origin point where the copy was made, which must be the - * {@link CuboidRegion#getMinimumPoint()} relative to the copy - * @param offset the offset from the minimum point of the copy where the user was - */ - public CuboidClipboard(Vector size, Vector origin, Vector offset) { - checkNotNull(size); - checkNotNull(origin); - checkNotNull(offset); - - this.size = size; - data = new BlockStateHolder[size.getBlockX()][size.getBlockY()][size.getBlockZ()]; - this.origin = origin; - this.offset = offset; - } - - /** - * Get the width (X-direction) of the clipboard. - * - * @return width - */ - public int getWidth() { - return size.getBlockX(); - } - - /** - * Get the length (Z-direction) of the clipboard. - * - * @return length - */ - public int getLength() { - return size.getBlockZ(); - } - - /** - * Get the height (Y-direction) of the clipboard. - * - * @return height - */ - public int getHeight() { - return size.getBlockY(); - } - - /** - * Copies blocks to the clipboard. - * - * @param editSession the EditSession from which to take the blocks - */ - public void copy(EditSession editSession) { - for (int x = 0; x < size.getBlockX(); ++x) { - for (int y = 0; y < size.getBlockY(); ++y) { - for (int z = 0; z < size.getBlockZ(); ++z) { - BaseBlock fullBlock = editSession.getFullBlock(new Vector(x, y, z).add(getOrigin())); - if (fullBlock.getNbtData() != null) { - data[x][y][z] = fullBlock; - } else { - data[x][y][z] = fullBlock.toImmutableState(); - } - } - } - } - } - - /** - * Copies blocks to the clipboard. - * - * @param editSession The EditSession from which to take the blocks - * @param region A region that further constrains which blocks to take. - */ - public void copy(EditSession editSession, Region region) { - for (int x = 0; x < size.getBlockX(); ++x) { - for (int y = 0; y < size.getBlockY(); ++y) { - for (int z = 0; z < size.getBlockZ(); ++z) { - final Vector pt = new Vector(x, y, z).add(getOrigin()); - if (region.contains(pt)) { - BaseBlock fullBlock = editSession.getFullBlock(pt); - if (fullBlock.getNbtData() != null) { - data[x][y][z] = fullBlock; - } else { - data[x][y][z] = fullBlock.toImmutableState(); - } - } else { - data[x][y][z] = null; - } - } - } - } - } - - /** - * Paste the clipboard at the given location using the given {@code EditSession}. - * - *

This method blocks the server/game until the entire clipboard is - * pasted. In the future, {@link ForwardExtentCopy} will be recommended, - * which, if combined with the proposed operation scheduler framework, - * will not freeze the game/server.

- * - * @param editSession the EditSession to which blocks are to be copied to - * @param newOrigin the new origin point (must correspond to the minimum point of the cuboid) - * @param noAir true to not copy air blocks in the source - * @throws MaxChangedBlocksException thrown if too many blocks were changed - */ - public void paste(EditSession editSession, Vector newOrigin, boolean noAir) throws MaxChangedBlocksException { - paste(editSession, newOrigin, noAir, false); - } - - /** - * Paste the clipboard at the given location using the given {@code EditSession}. - * - *

This method blocks the server/game until the entire clipboard is - * pasted. In the future, {@link ForwardExtentCopy} will be recommended, - * which, if combined with the proposed operation scheduler framework, - * will not freeze the game/server.

- * - * @param editSession the EditSession to which blocks are to be copied to - * @param newOrigin the new origin point (must correspond to the minimum point of the cuboid) - * @param noAir true to not copy air blocks in the source - * @param entities true to copy entities - * @throws MaxChangedBlocksException thrown if too many blocks were changed - */ - public void paste(EditSession editSession, Vector newOrigin, boolean noAir, boolean entities) throws MaxChangedBlocksException { - place(editSession, newOrigin.add(offset), noAir); - if (entities) { - pasteEntities(newOrigin.add(offset)); - } - } - - /** - * Paste the clipboard at the given location using the given {@code EditSession}. - * - *

This method blocks the server/game until the entire clipboard is - * pasted. In the future, {@link ForwardExtentCopy} will be recommended, - * which, if combined with the proposed operation scheduler framework, - * will not freeze the game/server.

- * - * @param editSession the EditSession to which blocks are to be copied to - * @param newOrigin the new origin point (must correspond to the minimum point of the cuboid) - * @param noAir true to not copy air blocks in the source - * @throws MaxChangedBlocksException thrown if too many blocks were changed - */ - public void place(EditSession editSession, Vector newOrigin, boolean noAir) throws MaxChangedBlocksException { - for (int x = 0; x < size.getBlockX(); ++x) { - for (int y = 0; y < size.getBlockY(); ++y) { - for (int z = 0; z < size.getBlockZ(); ++z) { - final BlockStateHolder block = data[x][y][z]; - if (block == null) { - continue; - } - - if (noAir && block.getBlockType() == BlockTypes.AIR) { - continue; - } - - editSession.setBlock(new Vector(x, y, z).add(newOrigin), block); - } - } - } - } - - /** - * Paste the stored entities to the given position. - * - * @param newOrigin the new origin - * @return a list of entities that were pasted - */ - public Entity[] pasteEntities(Vector newOrigin) { - Entity[] entities = new Entity[this.entities.size()]; - for (int i = 0; i < this.entities.size(); ++i) { - CopiedEntity copied = this.entities.get(i); - if (copied.entity.getExtent().createEntity( - copied.entity.getLocation().setPosition(copied.relativePosition.add(newOrigin)), - copied.entity.getState() - ) != null) { - entities[i] = copied.entity; - } - } - return entities; - } - - /** - * Store an entity. - * - * @param entity the entity - */ - public void storeEntity(Entity entity) { - this.entities.add(new CopiedEntity(entity)); - } - - /** - * Get the block at the given position. - * - *

If the position is out of bounds, air will be returned.

- * - * @param position the point, relative to the origin of the copy (0, 0, 0) and not to the actual copy origin - * @return null, if this block was outside the (non-cuboid) selection while copying - * @throws ArrayIndexOutOfBoundsException if the position is outside the bounds of the CuboidClipboard - */ - public BlockStateHolder getBlock(Vector position) throws ArrayIndexOutOfBoundsException { - return data[position.getBlockX()][position.getBlockY()][position.getBlockZ()]; - } - - /** - * Set the block at a position in the clipboard. - * - * @param position the point, relative to the origin of the copy (0, 0, 0) and not to the actual copy origin. - * @param block the block to set - * @throws ArrayIndexOutOfBoundsException if the position is outside the bounds of the CuboidClipboard - */ - public void setBlock(Vector position, BlockStateHolder block) { - data[position.getBlockX()][position.getBlockY()][position.getBlockZ()] = block; - } - - /** - * Get the dimensions of the clipboard. - * - * @return the dimensions, where (1, 1, 1) is 1 wide, 1 across, 1 deep - */ - public Vector getSize() { - return size; - } - - /** - * Get the origin point, which corresponds to where the copy was - * originally copied from. The origin is the lowest possible X, Y, and - * Z components of the cuboid region that was copied. - * - * @return the origin - */ - public Vector getOrigin() { - return origin; - } - - /** - * Set the origin point, which corresponds to where the copy was - * originally copied from. The origin is the lowest possible X, Y, and - * Z components of the cuboid region that was copied. - * - * @param origin the origin to set - */ - public void setOrigin(Vector origin) { - checkNotNull(origin); - this.origin = origin; - } - - /** - * Get the offset of the player to the clipboard's minimum point - * (minimum X, Y, Z coordinates). - * - *

The offset is inverse (multiplied by -1).

- * - * @return the offset the offset - */ - public Vector getOffset() { - return offset; - } - - /** - * Set the offset of the player to the clipboard's minimum point - * (minimum X, Y, Z coordinates). - * - *

The offset is inverse (multiplied by -1).

- * - * @param offset the new offset - */ - public void setOffset(Vector offset) { - this.offset = offset; - } - - /** - * Stores a copied entity. - */ - private class CopiedEntity { - private final Entity entity; - private final Vector relativePosition; - - private CopiedEntity(Entity entity) { - this.entity = entity; - this.relativePosition = entity.getLocation().toVector().subtract(getOrigin()); - } - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java index 5880047d8..c2c18ede2 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/WorldEdit.java @@ -22,10 +22,10 @@ package com.sk89q.worldedit; import static com.sk89q.worldedit.event.platform.Interaction.HIT; import static com.sk89q.worldedit.event.platform.Interaction.OPEN; -import com.sk89q.worldedit.CuboidClipboard.FlipDirection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import com.sk89q.worldedit.blocks.BaseBlock; import com.sk89q.worldedit.blocks.BaseItem; -import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.entity.Player; import com.sk89q.worldedit.event.platform.BlockInteractEvent; import com.sk89q.worldedit.event.platform.InputType; @@ -51,6 +51,7 @@ import com.sk89q.worldedit.util.io.file.FilenameException; import com.sk89q.worldedit.util.io.file.FilenameResolutionException; import com.sk89q.worldedit.util.io.file.InvalidFilenameException; import com.sk89q.worldedit.util.logging.WorldEditPrefixHandler; +import com.sk89q.worldedit.world.block.BlockType; import com.sk89q.worldedit.world.registry.BundledBlockData; import com.sk89q.worldedit.world.registry.BundledItemData; import com.sk89q.worldedit.world.registry.LegacyMapper; @@ -61,6 +62,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; +import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -253,15 +256,8 @@ public class WorldEdit { throw new FileSelectionAbortedException("No file selected"); } } else { - if (defaultExt != null && filename.lastIndexOf('.') == -1) { - filename += "." + defaultExt; - } - - if (!filename.matches("^[A-Za-z0-9_\\- \\./\\\\'\\$@~!%\\^\\*\\(\\)\\[\\]\\+\\{\\},\\?]+\\.[A-Za-z0-9]+$")) { - throw new InvalidFilenameException(filename, "Invalid characters or extension missing"); - } - - f = new File(dir, filename); + List exts = extensions == null ? ImmutableList.of(defaultExt) : Lists.asList(defaultExt, extensions); + return getSafeFileWithExtensions(dir, filename, exts, isSave); } try { @@ -280,6 +276,39 @@ public class WorldEdit { } } + private File getSafeFileWithExtensions(File dir, String filename, List exts, boolean isSave) throws InvalidFilenameException { + if (isSave) { + // First is default, only use that. + if (exts.size() != 1) { + exts = exts.subList(0, 1); + } + } + File result = null; + for (Iterator iter = exts.iterator(); iter.hasNext() && (result == null || !result.exists());) { + result = getSafeFileWithExtension(dir, filename, iter.next()); + } + if (result == null) { + throw new InvalidFilenameException(filename, "Invalid characters or extension missing"); + } + return result; + } + + private File getSafeFileWithExtension(File dir, String filename, String extension) { + if (extension != null && filename.lastIndexOf('.') == -1) { + filename += "." + extension; + } + + if (!checkFilename(filename)) { + return null; + } + + return new File(dir, filename); + } + + private boolean checkFilename(String filename) { + return filename.matches("^[A-Za-z0-9_\\- \\./\\\\'\\$@~!%\\^\\*\\(\\)\\[\\]\\+\\{\\},\\?]+\\.[A-Za-z0-9]+$"); + } + /** * Load the bundled mappings. */ @@ -431,47 +460,6 @@ public class WorldEdit { return dir; } - /** - * Get diagonal direction vector for a player's direction. May return - * null if a direction could not be found. - * - * @param player the player - * @param dirStr the direction string - * @return a direction vector - * @throws UnknownDirectionException thrown if the direction is not known - */ - public Vector getDiagonalDirection(Player player, String dirStr) throws UnknownDirectionException { - return getPlayerDirection(player, dirStr.toLowerCase()).vector(); - } - - /** - * Get the flip direction for a player's direction. - * - * @param player the player - * @param dirStr the direction string - * @return a direction vector - * @throws UnknownDirectionException thrown if the direction is not known - */ - public FlipDirection getFlipDirection(Player player, String dirStr) throws UnknownDirectionException { - final PlayerDirection dir = getPlayerDirection(player, dirStr); - switch (dir) { - case WEST: - case EAST: - return FlipDirection.WEST_EAST; - - case NORTH: - case SOUTH: - return FlipDirection.NORTH_SOUTH; - - case UP: - case DOWN: - return FlipDirection.UP_DOWN; - - default: - throw new UnknownDirectionException(dir.name()); - } - } - /** * Flush a block bag's changes to a player. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java index f3744bcb5..2f85734e7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/command/SchematicCommands.java @@ -35,6 +35,7 @@ import com.sk89q.worldedit.extension.platform.Actor; import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormat; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats; import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; import com.sk89q.worldedit.function.operation.Operations; @@ -44,7 +45,6 @@ import com.sk89q.worldedit.util.command.binding.Switch; import com.sk89q.worldedit.util.command.parametric.Optional; import com.sk89q.worldedit.util.io.Closer; import com.sk89q.worldedit.util.io.file.FilenameException; -import com.sk89q.worldedit.world.registry.Registries; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -93,14 +93,17 @@ public class SchematicCommands { LocalConfiguration config = worldEdit.getConfiguration(); File dir = worldEdit.getWorkingDirectoryFile(config.saveDir); - File f = worldEdit.getSafeOpenFile(player, dir, filename, "schematic", "schematic"); + File f = worldEdit.getSafeOpenFile(player, dir, filename, "schematic", ClipboardFormats.getFileExtensionArray()); if (!f.exists()) { player.printError("Schematic " + filename + " does not exist!"); return; } - ClipboardFormat format = ClipboardFormat.findByAlias(formatName); + ClipboardFormat format = ClipboardFormats.findByFile(f); + if (format == null) { + format = ClipboardFormats.findByAlias(formatName); + } if (format == null) { player.printError("Unknown schematic format: " + formatName); return; @@ -109,7 +112,7 @@ public class SchematicCommands { try (Closer closer = Closer.create()) { FileInputStream fis = closer.register(new FileInputStream(f)); BufferedInputStream bis = closer.register(new BufferedInputStream(fis)); - ClipboardReader reader = format.getReader(bis); + ClipboardReader reader = closer.register(format.getReader(bis)); Clipboard clipboard = reader.read(); session.setClipboard(new ClipboardHolder(clipboard)); @@ -134,14 +137,15 @@ public class SchematicCommands { LocalConfiguration config = worldEdit.getConfiguration(); File dir = worldEdit.getWorkingDirectoryFile(config.saveDir); - File f = worldEdit.getSafeSaveFile(player, dir, filename, "schematic", "schematic"); - ClipboardFormat format = ClipboardFormat.findByAlias(formatName); + ClipboardFormat format = ClipboardFormats.findByAlias(formatName); if (format == null) { player.printError("Unknown schematic format: " + formatName); return; } + File f = worldEdit.getSafeSaveFile(player, dir, filename, format.getPrimaryFileExtension()); + ClipboardHolder holder = session.getClipboard(); Clipboard clipboard = holder.getClipboard(); Transform transform = holder.getTransform(); @@ -218,9 +222,9 @@ public class SchematicCommands { actor.print("Available clipboard formats (Name: Lookup names)"); StringBuilder builder; boolean first = true; - for (ClipboardFormat format : ClipboardFormat.values()) { + for (ClipboardFormat format : ClipboardFormats.getAll()) { builder = new StringBuilder(); - builder.append(format.name()).append(": "); + builder.append(format.getName()).append(": "); for (String lookupName : format.getAliases()) { if (!first) { builder.append(", "); @@ -325,10 +329,10 @@ public class SchematicCommands { StringBuilder build = new StringBuilder(); build.append("\u00a72"); - ClipboardFormat format = ClipboardFormat.findByFile(file); + ClipboardFormat format = ClipboardFormats.findByFile(file); boolean inRoot = file.getParentFile().getName().equals(prefix); build.append(inRoot ? file.getName() : file.getPath().split(Pattern.quote(prefix + File.separator))[1]) - .append(": ").append(format == null ? "Unknown" : format.name()); + .append(": ").append(format == null ? "Unknown" : format.getName()); result.add(build.toString()); } return result; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/BuiltInClipboardFormat.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/BuiltInClipboardFormat.java new file mode 100644 index 000000000..46ee0eab3 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/BuiltInClipboardFormat.java @@ -0,0 +1,107 @@ +/* + * 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.google.common.collect.ImmutableSet; +import com.sk89q.jnbt.NBTConstants; +import com.sk89q.jnbt.NBTInputStream; + +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.Set; +import java.util.zip.GZIPInputStream; + +/** + * A collection of supported clipboard formats. + */ +public enum BuiltInClipboardFormat implements ClipboardFormat { + + /** + * The Schematic format used by many software. + */ + MCEDIT_SCHEMATIC("mcedit", "mce", "schematic") { + + @Override + public String getPrimaryFileExtension() { + return "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 { + throw new IOException("This format does not support saving"); + } + + @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 final ImmutableSet aliases; + + BuiltInClipboardFormat(String... aliases) { + this.aliases = ImmutableSet.copyOf(aliases); + } + + @Override + public String getName() { + return name(); + } + + @Override + public Set getAliases() { + return this.aliases; + } + + @Override + public Set getFileExtensions() { + return ImmutableSet.of(getPrimaryFileExtension()); + } + +} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java index 2c035094f..fd16d57c2 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java @@ -19,84 +19,30 @@ package com.sk89q.worldedit.extent.clipboard.io; -import com.sk89q.jnbt.NBTConstants; -import com.sk89q.jnbt.NBTInputStream; - -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 static com.google.common.base.Preconditions.checkNotNull; /** * A collection of supported clipboard formats. */ -public enum ClipboardFormat { +public interface 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 { - throw new IOException("This clipboard format no longer supports saving."); - } - - @Override - public boolean isFormat(File file) { - try (DataInputStream 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; - } - } - }; - - private static final Map aliasMap = new HashMap<>(); - - private final String[] aliases; - - /** - * Create a new instance. + * Returns the name of this format. * - * @param aliases an array of aliases by which this format may be referred to + * @return The name of the format */ - ClipboardFormat(String... aliases) { - this.aliases = aliases; - } + String getName(); /** * Get a set of aliases. * * @return a set of aliases */ - public Set getAliases() { - return Collections.unmodifiableSet(new HashSet<>(Arrays.asList(aliases))); - } + Set getAliases(); /** * Create a reader. @@ -105,7 +51,7 @@ public enum ClipboardFormat { * @return a reader * @throws IOException thrown on I/O error */ - public abstract ClipboardReader getReader(InputStream inputStream) throws IOException; + ClipboardReader getReader(InputStream inputStream) throws IOException; /** * Create a writer. @@ -114,7 +60,7 @@ public enum ClipboardFormat { * @return a writer * @throws IOException thrown on I/O error */ - public abstract ClipboardWriter getWriter(OutputStream outputStream) throws IOException; + ClipboardWriter getWriter(OutputStream outputStream) throws IOException; /** * Return whether the given file is of this format. @@ -122,45 +68,20 @@ public enum ClipboardFormat { * @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); - } - } - } + boolean isFormat(File file); /** - * Find the clipboard format named by the given alias. + * Get the file extension this format primarily uses. * - * @param alias the alias - * @return the format, otherwise null if none is matched + * @return The primary file extension */ - @Nullable - public static ClipboardFormat findByAlias(String alias) { - checkNotNull(alias); - return aliasMap.get(alias.toLowerCase().trim()); - } + String getPrimaryFileExtension(); /** - * Detect the format given a file. + * Get the file extensions this format is commonly known to use. This should + * include {@link #getPrimaryFileExtension()}. * - * @param file the file - * @return the format, otherwise null if one cannot be detected + * @return The file extensions this format might be known by */ - @Nullable - public static ClipboardFormat findByFile(File file) { - checkNotNull(file); - - for (ClipboardFormat format : EnumSet.allOf(ClipboardFormat.class)) { - if (format.isFormat(file)) { - return format; - } - } - - return null; - } - + Set getFileExtensions(); } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormats.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormats.java new file mode 100644 index 000000000..9663a9b1f --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormats.java @@ -0,0 +1,125 @@ +/* + * 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 static com.google.common.base.Preconditions.checkNotNull; + +import com.google.common.collect.HashMultimap; +import com.google.common.collect.Multimap; +import com.google.common.collect.Multimaps; +import com.sk89q.worldedit.WorldEdit; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.annotation.Nullable; + +public class ClipboardFormats { + + private static final Map aliasMap = new HashMap<>(); + private static final Multimap fileExtensionMap = HashMultimap.create(); + private static final List registeredFormats = new ArrayList<>(); + + public static void registerClipboardFormat(ClipboardFormat format) { + checkNotNull(format); + + for (String key : format.getAliases()) { + String lowKey = key.toLowerCase(Locale.ENGLISH); + ClipboardFormat old = aliasMap.put(lowKey, format); + if (old != null) { + aliasMap.put(lowKey, old); + WorldEdit.logger.warning(format.getClass().getName() + " cannot override existing alias '" + lowKey + "' used by " + old.getClass().getName()); + } + } + for (String ext : format.getFileExtensions()) { + String lowExt = ext.toLowerCase(Locale.ENGLISH); + fileExtensionMap.put(lowExt, format); + } + registeredFormats.add(format); + } + + static { + for (BuiltInClipboardFormat format : BuiltInClipboardFormat.values()) { + registerClipboardFormat(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(Locale.ENGLISH).trim()); + } + + /** + * Detect the format of 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 : registeredFormats) { + if (format.isFormat(file)) { + return format; + } + } + + return null; + } + + /** + * @return a multimap from a file extension to the potential matching formats. + */ + public static Multimap getFileExtensionMap() { + return Multimaps.unmodifiableMultimap(fileExtensionMap); + } + + public static Collection getAll() { + return Collections.unmodifiableCollection(registeredFormats); + } + + /** + * Not public API, only used by SchematicCommands. + * It is not in SchematicCommands because it may rely on internal register calls. + */ + public static String[] getFileExtensionArray() { + return fileExtensionMap.keySet().toArray(new String[fileExtensionMap.keySet().size()]); + } + + private ClipboardFormats() { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardReader.java index 40808d755..1448ca123 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardReader.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardReader.java @@ -22,6 +22,7 @@ package com.sk89q.worldedit.extent.clipboard.io; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.world.registry.Registries; +import java.io.Closeable; import java.io.IOException; /** @@ -29,7 +30,7 @@ import java.io.IOException; * * @see Clipboard */ -public interface ClipboardReader { +public interface ClipboardReader extends Closeable { /** * Read a {@code Clipboard}. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java index 28918fda3..89d88c1cc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicReader.java @@ -297,4 +297,8 @@ public class SchematicReader implements ClipboardReader { return expected.cast(test); } + @Override + public void close() throws IOException { + inputStream.close(); + } } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/schematic/MCEditSchematicFormat.java b/worldedit-core/src/main/java/com/sk89q/worldedit/schematic/MCEditSchematicFormat.java deleted file mode 100644 index 7c3f0fc7a..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/schematic/MCEditSchematicFormat.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * 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.schematic; - -import com.sk89q.jnbt.ByteArrayTag; -import com.sk89q.jnbt.CompoundTag; -import com.sk89q.jnbt.IntTag; -import com.sk89q.jnbt.ListTag; -import com.sk89q.jnbt.NBTConstants; -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.CuboidClipboard; -import com.sk89q.worldedit.Vector; -import com.sk89q.worldedit.blocks.BaseBlock; -import com.sk89q.worldedit.world.DataException; -import com.sk89q.worldedit.world.block.BlockState; -import com.sk89q.worldedit.world.registry.LegacyMapper; - -import java.io.DataInputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.zip.GZIPInputStream; - -public class MCEditSchematicFormat extends SchematicFormat { - - private static final int MAX_SIZE = Short.MAX_VALUE - Short.MIN_VALUE; - - protected MCEditSchematicFormat() { - super("MCEdit", "mcedit", "mce"); - } - - public CuboidClipboard load(InputStream stream) throws IOException, DataException { - NBTInputStream nbtStream = new NBTInputStream( - new GZIPInputStream(stream)); - - Vector origin = new Vector(); - Vector offset = new Vector(); - - // Schematic tag - NamedTag rootTag = nbtStream.readNamedTag(); - nbtStream.close(); - if (!rootTag.getName().equals("Schematic")) { - throw new DataException("Tag \"Schematic\" does not exist or is not first"); - } - - CompoundTag schematicTag = (CompoundTag) rootTag.getTag(); - - // Check - Map schematic = schematicTag.getValue(); - if (!schematic.containsKey("Blocks")) { - throw new DataException("Schematic file is missing a \"Blocks\" tag"); - } - - // Get information - short width = getChildTag(schematic, "Width", ShortTag.class).getValue(); - short length = getChildTag(schematic, "Length", ShortTag.class).getValue(); - short height = getChildTag(schematic, "Height", 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(); - origin = new Vector(originX, originY, originZ); - } catch (DataException e) { - // No origin data - } - - try { - int offsetX = getChildTag(schematic, "WEOffsetX", IntTag.class).getValue(); - int offsetY = getChildTag(schematic, "WEOffsetY", IntTag.class).getValue(); - int offsetZ = getChildTag(schematic, "WEOffsetZ", IntTag.class).getValue(); - offset = new Vector(offsetX, offsetY, offsetZ); - } catch (DataException e) { - // No offset data - } - - // Check type of Schematic - String materials = getChildTag(schematic, "Materials", StringTag.class).getValue(); - if (!materials.equals("Alpha")) { - throw new DataException("Schematic file is not an Alpha schematic"); - } - - // 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()) { - switch (entry.getKey()) { - case "x": - if (entry.getValue() instanceof IntTag) { - x = ((IntTag) entry.getValue()).getValue(); - } - break; - case "y": - if (entry.getValue() instanceof IntTag) { - y = ((IntTag) entry.getValue()).getValue(); - } - break; - case "z": - if (entry.getValue() instanceof IntTag) { - z = ((IntTag) entry.getValue()).getValue(); - } - break; - } - - values.put(entry.getKey(), entry.getValue()); - } - - BlockVector vec = new BlockVector(x, y, z); - tileEntitiesMap.put(vec, values); - } - - Vector size = new Vector(width, height, length); - CuboidClipboard clipboard = new CuboidClipboard(size); - clipboard.setOrigin(origin); - clipboard.setOffset(offset); - - 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); - BlockState state = LegacyMapper.getInstance().getBlockFromLegacy(blocks[index], blockData[index]); - - if (tileEntitiesMap.containsKey(pt)) { - clipboard.setBlock(pt, new BaseBlock(state, new CompoundTag(tileEntitiesMap.get(pt)))); - } else { - clipboard.setBlock(pt, state); - } - } - } - } - - return clipboard; - } - - @Override - public CuboidClipboard load(File file) throws IOException, DataException { - return load(new FileInputStream(file)); - } - - @Override - public void save(CuboidClipboard clipboard, File file) throws IOException, DataException { - throw new DataException("This clipboard format no longer supports saving."); - } - - @Override - public boolean isOfFormat(File file) { - try (DataInputStream 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; - } - // blargh - } - - /** - * 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 DataException 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 DataException { - - if (!items.containsKey(key)) { - throw new DataException("Schematic file is missing a \"" + key + "\" tag"); - } - Tag tag = items.get(key); - if (!expected.isInstance(tag)) { - throw new DataException( - key + " tag is not of tag type " + expected.getName()); - } - return expected.cast(tag); - } - -} \ No newline at end of file diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/schematic/SchematicFormat.java b/worldedit-core/src/main/java/com/sk89q/worldedit/schematic/SchematicFormat.java deleted file mode 100644 index 48c59f954..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/schematic/SchematicFormat.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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.schematic; - -import com.sk89q.worldedit.CuboidClipboard; -import com.sk89q.worldedit.blocks.BaseBlock; -import com.sk89q.worldedit.world.DataException; - -import java.io.File; -import java.io.IOException; -import java.util.*; - -public abstract class SchematicFormat { - - private static final Map SCHEMATIC_FORMATS = new HashMap<>(); - - // Built-in schematic formats - public static final SchematicFormat MCEDIT = new MCEditSchematicFormat(); - - public static Set getFormats() { - return Collections.unmodifiableSet(new HashSet<>(SCHEMATIC_FORMATS.values())); - } - - public static SchematicFormat getFormat(String lookupName) { - return SCHEMATIC_FORMATS.get(lookupName.toLowerCase()); - } - - public static SchematicFormat getFormat(File file) { - if (!file.isFile()) { - return null; - } - - for (SchematicFormat format : SCHEMATIC_FORMATS.values()) { - if (format.isOfFormat(file)) { - return format; - } - } - return null; - } - - private final String name; - private final String[] lookupNames; - - protected SchematicFormat(String name, String... lookupNames) { - this.name = name; - List registeredLookupNames = new ArrayList<>(lookupNames.length); - for (int i = 0; i < lookupNames.length; ++i) { - if (i == 0 || !SCHEMATIC_FORMATS.containsKey(lookupNames[i].toLowerCase())) { - SCHEMATIC_FORMATS.put(lookupNames[i].toLowerCase(), this); - registeredLookupNames.add(lookupNames[i].toLowerCase()); - } - } - this.lookupNames = registeredLookupNames.toArray(new String[registeredLookupNames.size()]); - } - - /** - * Gets the official/display name for this schematic format - * - * @return The display name for this schematic format - */ - public String getName() { - return name; - } - - public String[] getLookupNames() { - return lookupNames; - } - - /** - * Loads a schematic from the given file into a CuboidClipboard - * @param file The file to load from - * @return The CuboidClipboard containing the contents of this schematic - * @throws IOException If an error occurs while reading data - * @throws DataException if data is not in the correct format - */ - public abstract CuboidClipboard load(File file) throws IOException, DataException; - - /** - * Saves the data from the specified CuboidClipboard to the given file, overwriting any - * existing data in the file - * @param clipboard The clipboard to get data from - * @param file The file to save to - * @throws IOException If an error occurs while writing data - * @throws DataException If the clipboard has data which cannot be stored - */ - public abstract void save(CuboidClipboard clipboard, File file) throws IOException, DataException; - - public abstract boolean isOfFormat(File file); - -}