Start work on the Sponge schematic format. This should work but it may not, it's untested.

This commit is contained in:
Matthew Miller 2018-08-02 14:15:49 +10:00
parent a4b9ceaeb2
commit a75d9e896b
8 changed files with 499 additions and 78 deletions

View File

@ -111,18 +111,6 @@ public class BaseBlock implements BlockStateHolder<BaseBlock>, TileEntityBlock {
this(other.toImmutableState(), other.getNbtData());
}
/**
* Get the block's data value.
*
* Broken - do not use
*
* @return data value (0-15)
*/
@Deprecated
public int getData() {
return 0;
}
/**
* Gets a map of state to statevalue
*

View File

@ -133,7 +133,7 @@ public class SchematicCommands {
)
@Deprecated
@CommandPermissions({ "worldedit.clipboard.save", "worldedit.schematic.save" })
public void save(Player player, LocalSession session, @Optional("schematic") String formatName, String filename) throws CommandException, WorldEditException {
public void save(Player player, LocalSession session, @Optional("sponge") String formatName, String filename) throws CommandException, WorldEditException {
LocalConfiguration config = worldEdit.getConfiguration();
File dir = worldEdit.getWorkingDirectoryFile(config.saveDir);

View File

@ -22,6 +22,7 @@ package com.sk89q.worldedit.extent.clipboard.io;
import com.google.common.collect.ImmutableSet;
import com.sk89q.jnbt.NBTConstants;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NBTOutputStream;
import java.io.DataInputStream;
import java.io.File;
@ -31,6 +32,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
/**
* A collection of supported clipboard formats.
@ -38,7 +40,7 @@ import java.util.zip.GZIPInputStream;
public enum BuiltInClipboardFormat implements ClipboardFormat {
/**
* The Schematic format used by many software.
* The Schematic format used by MCEdit.
*/
MCEDIT_SCHEMATIC("mcedit", "mce", "schematic") {
@ -50,7 +52,7 @@ public enum BuiltInClipboardFormat implements ClipboardFormat {
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
NBTInputStream nbtStream = new NBTInputStream(new GZIPInputStream(inputStream));
return new SchematicReader(nbtStream);
return new MCEditSchematicReader(nbtStream);
}
@Override
@ -60,25 +62,66 @@ public enum BuiltInClipboardFormat implements ClipboardFormat {
@Override
public boolean isFormat(File file) {
DataInputStream str = null;
try {
str = new DataInputStream(new GZIPInputStream(new FileInputStream(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");
if (!name.equals("Schematic")) {
return false;
}
str.readByte(); // Ignore
nameBytes = new byte[str.readShort() & 0xFFFF];
str.readFully(nameBytes);
name = new String(nameBytes, NBTConstants.CHARSET);
return !name.equals("Version");
} catch (IOException e) {
return false;
} finally {
if (str != null) {
try {
str.close();
} catch (IOException ignored) {
}
}
}
},
SPONGE_SCHEMATIC("sponge", "schem") {
@Override
public String getPrimaryFileExtension() {
return "schem";
}
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
NBTInputStream nbtStream = new NBTInputStream(new GZIPInputStream(inputStream));
return new SpongeSchematicReader(nbtStream);
}
@Override
public ClipboardWriter getWriter(OutputStream outputStream) throws IOException {
NBTOutputStream nbtStream = new NBTOutputStream(new GZIPOutputStream(outputStream));
return new SpongeSchematicWriter(nbtStream);
}
@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);
if (!name.equals("Schematic")) {
return false;
}
if ((str.readByte() & 0xFF) != NBTConstants.TYPE_INT) {
return false;
}
nameBytes = new byte[str.readShort() & 0xFFFF];
str.readFully(nameBytes);
name = new String(nameBytes, NBTConstants.CHARSET);
return name.equals("Version");
} catch (IOException e) {
return false;
}
}
};

View File

@ -56,20 +56,19 @@ import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
/**
* Reads schematic files based that are compatible with MCEdit and other editors.
* Reads schematic files that are compatible with MCEdit and other editors.
*/
public class SchematicReader implements ClipboardReader {
public class MCEditSchematicReader extends NBTSchematicReader {
private static final List<NBTCompatibilityHandler> COMPATIBILITY_HANDLERS = new ArrayList<>();
static {
COMPATIBILITY_HANDLERS.add(new SignCompatibilityHandler());
// TODO Add a handler for skulls, flower pots, note blocks, etc.
}
private static final Logger log = Logger.getLogger(SchematicReader.class.getCanonicalName());
private static final Logger log = Logger.getLogger(MCEditSchematicReader.class.getCanonicalName());
private final NBTInputStream inputStream;
/**
@ -77,7 +76,7 @@ public class SchematicReader implements ClipboardReader {
*
* @param inputStream the input stream to read from
*/
public SchematicReader(NBTInputStream inputStream) {
public MCEditSchematicReader(NBTInputStream inputStream) {
checkNotNull(inputStream);
this.inputStream = inputStream;
}
@ -282,35 +281,6 @@ public class SchematicReader implements ClipboardReader {
return clipboard;
}
private static <T extends Tag> T requireTag(Map<String, Tag> items, String key, Class<T> expected) throws IOException {
if (!items.containsKey(key)) {
throw new IOException("Schematic file is missing a \"" + key + "\" tag");
}
Tag tag = items.get(key);
if (!expected.isInstance(tag)) {
throw new IOException(key + " tag is not of tag type " + expected.getName());
}
return expected.cast(tag);
}
@Nullable
private static <T extends Tag> T getTag(CompoundTag tag, Class<T> expected, String key) {
Map<String, Tag> items = tag.getValue();
if (!items.containsKey(key)) {
return null;
}
Tag test = items.get(key);
if (!expected.isInstance(test)) {
return null;
}
return expected.cast(test);
}
@Override
public void close() throws IOException {
inputStream.close();

View File

@ -0,0 +1,44 @@
package com.sk89q.worldedit.extent.clipboard.io;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.Tag;
import java.io.IOException;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Base class for NBT schematic readers
*/
public abstract class NBTSchematicReader implements ClipboardReader {
protected static <T extends Tag> T requireTag(Map<String, Tag> items, String key, Class<T> expected) throws IOException {
if (!items.containsKey(key)) {
throw new IOException("Schematic file is missing a \"" + key + "\" tag");
}
Tag tag = items.get(key);
if (!expected.isInstance(tag)) {
throw new IOException(key + " tag is not of tag type " + expected.getName());
}
return expected.cast(tag);
}
@Nullable
protected static <T extends Tag> T getTag(CompoundTag tag, Class<T> expected, String key) {
Map<String, Tag> items = tag.getValue();
if (!items.containsKey(key)) {
return null;
}
Tag test = items.get(key);
if (!expected.isInstance(test)) {
return null;
}
return expected.cast(test);
}
}

View File

@ -0,0 +1,220 @@
/*
* 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 static com.google.common.base.Preconditions.checkNotNull;
import com.sk89q.jnbt.ByteArrayTag;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.IntArrayTag;
import com.sk89q.jnbt.IntTag;
import com.sk89q.jnbt.ListTag;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NamedTag;
import com.sk89q.jnbt.ShortTag;
import com.sk89q.jnbt.Tag;
import com.sk89q.worldedit.BlockVector;
import com.sk89q.worldedit.Vector;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.WorldEditException;
import com.sk89q.worldedit.blocks.BaseBlock;
import com.sk89q.worldedit.extension.input.InputParseException;
import com.sk89q.worldedit.extension.input.ParserContext;
import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.extent.clipboard.io.legacycompat.NBTCompatibilityHandler;
import com.sk89q.worldedit.regions.CuboidRegion;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.world.block.BlockState;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import java.util.stream.Collectors;
/**
* Reads schematic files using the Sponge Schematic Specification.
*/
public class SpongeSchematicReader extends NBTSchematicReader {
private static final List<NBTCompatibilityHandler> COMPATIBILITY_HANDLERS = new ArrayList<>();
static {
// If NBT Compat handlers are needed - add them here.
}
private static final Logger log = Logger.getLogger(SpongeSchematicReader.class.getCanonicalName());
private final NBTInputStream inputStream;
/**
* Create a new instance.
*
* @param inputStream the input stream to read from
*/
public SpongeSchematicReader(NBTInputStream inputStream) {
checkNotNull(inputStream);
this.inputStream = inputStream;
}
@Override
public Clipboard read() throws IOException {
NamedTag rootTag = inputStream.readNamedTag();
if (!rootTag.getName().equals("Schematic")) {
throw new IOException("Tag 'Schematic' does not exist or is not first");
}
CompoundTag schematicTag = (CompoundTag) rootTag.getTag();
// Check
Map<String, Tag> schematic = schematicTag.getValue();
int version = requireTag(schematic, "Version", IntTag.class).getValue();
switch (version) {
case 1:
return readVersion1(schematic);
default:
throw new IOException("This schematic version is currently not supported");
}
}
private Clipboard readVersion1(Map<String, Tag> schematic) throws IOException {
Vector origin;
Region region;
Map<String, Tag> metadata = requireTag(schematic, "Metadata", CompoundTag.class).getValue();
int width = requireTag(schematic, "Width", ShortTag.class).getValue();
int height = requireTag(schematic, "Height", ShortTag.class).getValue();
int length = requireTag(schematic, "Length", ShortTag.class).getValue();
int[] offsetParts = requireTag(schematic, "Offset", IntArrayTag.class).getValue();
if (offsetParts.length != 3) {
throw new IOException("Invalid offset specified in schematic.");
}
Vector offset = new Vector(offsetParts[0], offsetParts[1], offsetParts[2]);
if (metadata.containsKey("WEOriginX")) {
// We appear to have WorldEdit Metadata
int originX = requireTag(metadata, "WEOriginX", IntTag.class).getValue();
int originY = requireTag(metadata, "WEOriginY", IntTag.class).getValue();
int originZ = requireTag(metadata, "WEOriginZ", IntTag.class).getValue();
Vector min = new Vector(originX, originY, originZ);
origin = min.subtract(offset);
region = new CuboidRegion(min, min.add(width, height, length).subtract(Vector.ONE));
} else {
origin = Vector.ZERO.subtract(offset);
region = new CuboidRegion(origin, origin.add(width, height, length).subtract(Vector.ONE));
}
int paletteMax = requireTag(schematic, "PaletteMax", IntTag.class).getValue();
Map<String, Tag> paletteObject = requireTag(schematic, "Palette", CompoundTag.class).getValue();
if (paletteObject.size() != paletteMax) {
throw new IOException("Differing given palette size to actual size");
}
Map<Integer, BlockState> palette = new HashMap<>();
ParserContext parserContext = new ParserContext();
for (String palettePart : paletteObject.keySet()) {
int id = requireTag(paletteObject, palettePart, IntTag.class).getValue();
BlockState state;
try {
state = WorldEdit.getInstance().getBlockFactory().parseFromInput(palettePart, parserContext).toImmutableState();
} catch (InputParseException e) {
throw new IOException("Invalid BlockState in schematic: " + palettePart + ". Are you missing a mod of using a schematic made in a newer version of Minecraft?");
}
palette.put(id, state);
}
byte[] blocks = requireTag(schematic, "BlockData", ByteArrayTag.class).getValue();
Map<BlockVector, Map<String, Tag>> tileEntitiesMap = new HashMap<>();
try {
List<Map<String, Tag>> tileEntityTags = requireTag(schematic, "TileEntities", ListTag.class).getValue().stream()
.map(tag -> (CompoundTag) tag)
.map(CompoundTag::getValue)
.collect(Collectors.toList());
for (Map<String, Tag> tileEntity : tileEntityTags) {
int[] pos = requireTag(schematic, "Pos", IntArrayTag.class).getValue();
tileEntitiesMap.put(new BlockVector(pos[0], pos[1], pos[2]), tileEntity);
}
} catch (Exception e) {
throw new IOException("Failed to load Tile Entities: " + e.getMessage());
}
BlockArrayClipboard clipboard = new BlockArrayClipboard(region);
clipboard.setOrigin(origin);
int index = 0;
int i = 0;
int value = 0;
int varint_length = 0;
while (i < blocks.length) {
value = 0;
varint_length = 0;
while (true) {
value |= (blocks[i] & 127) << (varint_length++ * 7);
if (varint_length > 5) {
throw new RuntimeException("VarInt too big (probably corrupted data)");
}
if ((blocks[i] & 128) != 128) {
i++;
break;
}
i++;
}
// index = (y * length + z) * width + x
int y = index / (width * length);
int z = (index % (width * length)) / width;
int x = (index % (width * length)) % width;
BlockState state = palette.get(value);
BlockVector pt = new BlockVector(x, y, z);
try {
if (tileEntitiesMap.containsKey(pt)) {
Map<String, Tag> values = tileEntitiesMap.get(pt);
for (NBTCompatibilityHandler handler : COMPATIBILITY_HANDLERS) {
if (handler.isAffectedBlock(state)) {
handler.updateNBT(state, values);
}
}
clipboard.setBlock(pt, new BaseBlock(state, new CompoundTag(values)));
} else {
clipboard.setBlock(pt, state);
}
} catch (WorldEditException e) {
throw new IOException("Failed to load a block in the schematic");
}
index++;
}
return clipboard;
}
@Override
public void close() throws IOException {
inputStream.close();
}
}

View File

@ -0,0 +1,174 @@
/*
* 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 static com.google.common.base.Preconditions.checkNotNull;
import com.sk89q.jnbt.ByteArrayTag;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.IntArrayTag;
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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Writes schematic files using the Sponge schematic format.
*/
public class SpongeSchematicWriter 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 SpongeSchematicWriter(NBTOutputStream outputStream) {
checkNotNull(outputStream);
this.outputStream = outputStream;
}
@Override
public void write(Clipboard clipboard) throws IOException {
// For now always write the latest version. Maybe provide support for earlier if more appear.
outputStream.writeNamedTag("Schematic", new CompoundTag(write1(clipboard)));
}
/**
* Writes a version 1 schematic file.
*
* @param clipboard The clipboard
* @return The schematic map
* @throws IOException If an error occurs
*/
private Map<String, Tag> write1(Clipboard clipboard) 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");
}
Map<String, Tag> schematic = new HashMap<>();
schematic.put("Version", new IntTag(1));
Map<String, Tag> metadata = new HashMap<>();
metadata.put("WEOriginX", new IntTag(min.getBlockX()));
metadata.put("WEOriginY", new IntTag(min.getBlockY()));
metadata.put("WEOriginZ", new IntTag(min.getBlockZ()));
schematic.put("Metadata", new CompoundTag(metadata));
schematic.put("Width", new ShortTag((short) width));
schematic.put("Height", new ShortTag((short) height));
schematic.put("Length", new ShortTag((short) length));
schematic.put("Offset", new IntArrayTag(new int[]{
offset.getBlockX(),
offset.getBlockY(),
offset.getBlockZ(),
}));
int paletteMax = 0;
Map<String, Integer> palette = new HashMap<>();
List<Tag> tileEntities = new ArrayList<>();
ByteArrayOutputStream buffer = new ByteArrayOutputStream(width * height * length);
for (Vector point : region) {
BaseBlock block = clipboard.getFullBlock(point);
if (block.getNbtData() != null) {
Map<String, Tag> values = new HashMap<>();
for (Map.Entry<String, Tag> entry : block.getNbtData().getValue().entrySet()) {
values.put(entry.getKey(), entry.getValue());
}
values.put("Id", new StringTag(block.getNbtId()));
values.put("Pos", new IntArrayTag(new int[]{
point.getBlockX(),
point.getBlockY(),
point.getBlockZ()
}));
CompoundTag tileEntityTag = new CompoundTag(values);
tileEntities.add(tileEntityTag);
}
String blockKey = block.toImmutableState().getAsString();
int blockId;
if (palette.containsKey(blockKey)) {
blockId = palette.get(blockKey);
} else {
blockId = paletteMax;
palette.put(blockKey, blockId);
paletteMax ++;
}
while ((blockId & -128) != 0) {
buffer.write(blockId & 127 | 128);
blockId >>>= 7;
}
buffer.write(blockId);
}
schematic.put("PaletteMax", new IntTag(paletteMax));
Map<String, Tag> paletteTag = new HashMap<>();
palette.forEach((key, value) -> paletteTag.put(key, new IntTag(value)));
schematic.put("Palette", new CompoundTag(paletteTag));
schematic.put("BlockData", new ByteArrayTag(buffer.toByteArray()));
schematic.put("TileEntities", new ListTag(CompoundTag.class, tileEntities));
return schematic;
}
@Override
public void close() throws IOException {
outputStream.close();
}
}

View File

@ -23,7 +23,6 @@ import com.sk89q.worldedit.registry.NamespacedRegistry;
import com.sk89q.worldedit.world.block.BlockType;
import com.sk89q.worldedit.world.block.BlockTypes;
import com.sk89q.worldedit.world.registry.BundledItemData;
import com.sk89q.worldedit.world.registry.LegacyMapper;
import javax.annotation.Nullable;
@ -79,23 +78,6 @@ public class ItemType {
return BlockTypes.get(this.id);
}
/**
* Gets the legacy ID. Needed for legacy reasons.
*
* DO NOT USE THIS.
*
* @return legacy id or 0, if unknown
*/
@Deprecated
public int getLegacyId() {
int ids[] = LegacyMapper.getInstance().getLegacyFromItem(this);
if (ids != null) {
return ids[0];
} else {
return 0;
}
}
@Override
public int hashCode() {
return this.id.hashCode();