Fix fast schematic reader/writer

- Have both sponge and fast r/w available but default to "fast"
 - Correctly "offset" entities in the schematic, and add a legacy mode for loading old FAWE schematics with entities required.
 - Lazily reading means it's read in order of appearance in the inputstream so we need to read schematic version first (skip past everything) and then reset the stream. Fixes #740
 - Add an FAWEVersion to the metadata
 - Correctly actually return a BlockArrayClipboard when required. Fixes #454
This commit is contained in:
dordsor21 2021-01-01 15:01:35 +00:00
parent 82f640d132
commit fbfe3221d7
No known key found for this signature in database
GPG Key ID: 1E53E88969FFCF0B
8 changed files with 181 additions and 57 deletions

View File

@ -200,7 +200,7 @@ public class StreamDelegate {
Object raw = is.readTagPayloadRaw(type, depth);
valueReader.apply(0, raw);
} else {
is.readTagPaylodLazy(type, depth + 1, this);
is.readTagPayloadLazy(type, depth + 1, this);
}
}

View File

@ -61,6 +61,14 @@ public final class NBTInputStream implements Closeable {
this.is = dis;
}
public void mark(int mark) {
is.mark(mark);
}
public void reset() throws IOException {
is.reset();
}
/**
* Reads an NBT tag from the stream.
*
@ -99,7 +107,7 @@ public final class NBTInputStream implements Closeable {
if (child != null) {
child.acceptRoot(this, type, 0);
} else {
readTagPaylodLazy(type, 0);
readTagPayloadLazy(type, 0);
}
} catch (Throwable e) {
e.printStackTrace();
@ -119,7 +127,7 @@ public final class NBTInputStream implements Closeable {
private byte[] buf;
public void readTagPaylodLazy(int type, int depth) throws IOException {
public void readTagPayloadLazy(int type, int depth) throws IOException {
switch (type) {
case NBTConstants.TYPE_END:
return;
@ -152,7 +160,7 @@ public final class NBTInputStream implements Closeable {
int childType = is.readByte();
length = is.readInt();
for (int i = 0; i < length; ++i) {
readTagPaylodLazy(childType, depth + 1);
readTagPayloadLazy(childType, depth + 1);
}
return;
}
@ -165,7 +173,7 @@ public final class NBTInputStream implements Closeable {
return;
}
is.skipBytes(is.readShort() & 0xFFFF);
readTagPaylodLazy(childType, depth + 1);
readTagPayloadLazy(childType, depth + 1);
}
}
case NBTConstants.TYPE_INT_ARRAY: {
@ -181,7 +189,7 @@ public final class NBTInputStream implements Closeable {
}
}
public void readTagPaylodLazy(int type, int depth, StreamDelegate scope) throws IOException {
public void readTagPayloadLazy(int type, int depth, StreamDelegate scope) throws IOException {
switch (type) {
case NBTConstants.TYPE_END:
return;
@ -293,11 +301,11 @@ public final class NBTInputStream implements Closeable {
child = scope.get0();
if (child == null) {
for (int i = 0; i < length; ++i) {
readTagPaylodLazy(childType, depth + 1);
readTagPayloadLazy(childType, depth + 1);
}
} else {
for (int i = 0; i < length; ++i) {
readTagPaylodLazy(childType, depth + 1, child);
readTagPayloadLazy(childType, depth + 1, child);
}
}
return;
@ -330,9 +338,9 @@ public final class NBTInputStream implements Closeable {
}
StreamDelegate child = scope.get(is);
if (child == null) {
readTagPaylodLazy(childType, depth + 1);
readTagPayloadLazy(childType, depth + 1);
} else {
readTagPaylodLazy(childType, depth + 1, child);
readTagPayloadLazy(childType, depth + 1, child);
}
}
}

View File

@ -147,7 +147,7 @@ public class SchematicCommands {
@Deprecated
@CommandPermissions({"worldedit.clipboard.load", "worldedit.schematic.load", "worldedit.schematic.load.web", "worldedit.schematic.load.asset"})
public void loadall(Player player, LocalSession session,
@Arg(desc = "Format name.", def = "schematic")
@Arg(desc = "Format name.", def = "fast")
String formatName,
@Arg(desc = "File name.")
String filename,
@ -223,7 +223,7 @@ public class SchematicCommands {
public void load(Actor actor, LocalSession session,
@Arg(desc = "File name.")
String filename,
@Arg(desc = "Format name.", def = "sponge")
@Arg(desc = "Format name.", def = "fast")
String formatName) throws FilenameException {
LocalConfiguration config = worldEdit.getConfiguration();
@ -323,7 +323,7 @@ public class SchematicCommands {
public void save(Actor actor, LocalSession session,
@Arg(desc = "File name.")
String filename,
@Arg(desc = "Format name.", def = "sponge")
@Arg(desc = "Format name.", def = "fast")
String formatName,
@Switch(name = 'f', desc = "Overwrite an existing file.")
boolean allowOverwrite,

View File

@ -24,8 +24,11 @@ import com.boydti.fawe.object.io.ResettableFileInputStream;
import com.boydti.fawe.object.schematic.MinecraftStructure;
import com.boydti.fawe.object.schematic.PNGWriter;
import com.google.common.collect.ImmutableSet;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NBTOutputStream;
import com.sk89q.jnbt.NamedTag;
import com.sk89q.jnbt.Tag;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@ -35,6 +38,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
@ -44,6 +48,44 @@ import java.util.zip.GZIPOutputStream;
*/
public enum BuiltInClipboardFormat implements ClipboardFormat {
FAST("fast", "fawe") {
@Override
public String getPrimaryFileExtension() {
return "schem";
}
@Override
public ClipboardReader getReader(InputStream inputStream) throws IOException {
if (inputStream instanceof FileInputStream) {
inputStream = new ResettableFileInputStream((FileInputStream) inputStream);
}
BufferedInputStream buffered = new BufferedInputStream(inputStream);
NBTInputStream nbtStream = new NBTInputStream(new BufferedInputStream(new GZIPInputStream(buffered)));
return new FastSchematicReader(nbtStream);
}
@Override
public ClipboardWriter getWriter(OutputStream outputStream) throws IOException {
OutputStream gzip;
if (outputStream instanceof PGZIPOutputStream || outputStream instanceof GZIPOutputStream) {
gzip = outputStream;
} else {
outputStream = new BufferedOutputStream(outputStream);
gzip = new PGZIPOutputStream(outputStream);
}
NBTOutputStream nbtStream = new NBTOutputStream(new BufferedOutputStream(gzip));
return new FastSchematicWriter(nbtStream);
}
@Override
public boolean isFormat(File file) {
String name = file.getName().toLowerCase(Locale.ROOT);
return name.endsWith(".schem") || name.endsWith(".sponge");
}
},
/**
* The Schematic format used by MCEdit.
*/
@ -77,8 +119,49 @@ public enum BuiltInClipboardFormat implements ClipboardFormat {
return name.endsWith(".schematic") || name.endsWith(".mcedit") || name.endsWith(".mce");
}
},
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 (NBTInputStream str = new NBTInputStream(new GZIPInputStream(new FileInputStream(file)))) {
NamedTag rootTag = str.readNamedTag();
if (!rootTag.getName().equals("Schematic")) {
return false;
}
CompoundTag schematicTag = (CompoundTag) rootTag.getTag();
// Check
Map<String, Tag> schematic = schematicTag.getValue();
if (!schematic.containsKey("Version")) {
return false;
}
} catch (Exception e) {
return false;
}
return true;
}
},
BROKENENTITY("brokenentity", "legacyentity", "le", "be", "brokenentities", "legacyentities") {
@Override
public String getPrimaryFileExtension() {
return "schem";
@ -91,7 +174,9 @@ public enum BuiltInClipboardFormat implements ClipboardFormat {
}
BufferedInputStream buffered = new BufferedInputStream(inputStream);
NBTInputStream nbtStream = new NBTInputStream(new BufferedInputStream(new GZIPInputStream(buffered)));
return new FastSchematicReader(nbtStream);
FastSchematicReader reader = new FastSchematicReader(nbtStream);
reader.setBrokenEntities(true);
return reader;
}
@Override
@ -104,13 +189,14 @@ public enum BuiltInClipboardFormat implements ClipboardFormat {
gzip = new PGZIPOutputStream(outputStream);
}
NBTOutputStream nbtStream = new NBTOutputStream(new BufferedOutputStream(gzip));
return new FastSchematicWriter(nbtStream);
FastSchematicWriter writer = new FastSchematicWriter(nbtStream);
writer.setBrokenEntities(true);
return writer;
}
@Override
public boolean isFormat(File file) {
String name = file.getName().toLowerCase(Locale.ROOT);
return name.endsWith(".schem") || name.endsWith(".sponge");
return false;
}
},

View File

@ -30,10 +30,12 @@ import com.boydti.fawe.object.io.FastByteArraysInputStream;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.IntTag;
import com.sk89q.jnbt.NBTInputStream;
import com.sk89q.jnbt.NamedTag;
import com.sk89q.jnbt.StringTag;
import com.sk89q.jnbt.Tag;
import com.sk89q.worldedit.WorldEdit;
import com.sk89q.worldedit.entity.BaseEntity;
import com.sk89q.worldedit.entity.Entity;
import com.sk89q.worldedit.extension.input.InputParseException;
import com.sk89q.worldedit.extension.platform.Capability;
import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard;
@ -45,6 +47,7 @@ import com.sk89q.worldedit.world.DataFixer;
import com.sk89q.worldedit.world.biome.BiomeType;
import com.sk89q.worldedit.world.biome.BiomeTypes;
import com.sk89q.worldedit.world.block.BlockState;
import com.sk89q.worldedit.world.block.BlockTypes;
import com.sk89q.worldedit.world.block.BlockTypesCache;
import com.sk89q.worldedit.world.entity.EntityType;
import com.sk89q.worldedit.world.entity.EntityTypes;
@ -58,6 +61,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.OptionalInt;
import java.util.UUID;
import java.util.function.Function;
@ -70,9 +74,10 @@ public class FastSchematicReader extends NBTSchematicReader {
private static final Logger log = LoggerFactory.getLogger(FastSchematicReader.class);
private final NBTInputStream inputStream;
private DataFixer fixer = null;
private DataFixer fixer;
private int dataVersion = -1;
private int version = -1;
private int faweWritten = -1;
private FastByteArrayOutputStream blocksOut;
private FaweOutputStream blocks;
@ -92,6 +97,7 @@ public class FastSchematicReader extends NBTSchematicReader {
private char[] palette;
private char[] biomePalette;
private BlockVector3 min = BlockVector3.ZERO;
private boolean brokenEntities = false;
/**
@ -105,6 +111,10 @@ public class FastSchematicReader extends NBTSchematicReader {
this.fixer = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataFixer();
}
public void setBrokenEntities(boolean brokenEntities) {
this.brokenEntities = brokenEntities;
}
private String fix(String palettePart) {
if (fixer == null || dataVersion == -1) {
return palettePart;
@ -133,7 +143,7 @@ public class FastSchematicReader extends NBTSchematicReader {
return fixer.fixUp(DataFixer.FixTypes.BIOME, biomePalettePart, dataVersion);
}
public StreamDelegate createDelegate() {
public StreamDelegate createVersionDelegate() {
StreamDelegate root = new StreamDelegate();
StreamDelegate schematic = root.add("Schematic");
schematic.add("DataVersion").withInt((i, v) -> dataVersion = v);
@ -143,6 +153,12 @@ public class FastSchematicReader extends NBTSchematicReader {
dataVersion = Constants.DATA_VERSION_MC_1_13_2;
}
});
return root;
}
public StreamDelegate createDelegate() {
StreamDelegate root = new StreamDelegate();
StreamDelegate schematic = root.add("Schematic");
schematic.add("Width").withInt((i, v) -> width = v);
schematic.add("Height").withInt((i, v) -> height = v);
schematic.add("Length").withInt((i, v) -> length = v);
@ -152,17 +168,19 @@ public class FastSchematicReader extends NBTSchematicReader {
metadata.add("WEOffsetX").withInt((i, v) -> offsetX = v);
metadata.add("WEOffsetY").withInt((i, v) -> offsetY = v);
metadata.add("WEOffsetZ").withInt((i, v) -> offsetZ = v);
metadata.add("FAWEVersion").withInt((i, v) -> faweWritten = v);
StreamDelegate paletteDelegate = schematic.add("Palette");
paletteDelegate.withValue((ValueReader<Map<String, Object>>) (ignore, v) -> {
palette = new char[v.size()];
for (Entry<String, Object> entry : v.entrySet()) {
BlockState state = null;
try {
BlockState state;
String palettePart = fix(entry.getKey());
try {
state = BlockState.get(palettePart);
} catch (InputParseException e) {
e.printStackTrace();
} catch (InputParseException ignored) {
log.warn("Invalid BlockState in palette: " + palettePart + ". Block will be replaced with air.");
state = BlockTypes.AIR.getDefaultState();
}
int index = (int) entry.getValue();
palette[index] = (char) state.getOrdinal();
@ -224,10 +242,14 @@ public class FastSchematicReader extends NBTSchematicReader {
@Override
public Clipboard read(UUID uuid, Function<BlockVector3, Clipboard> createOutput) throws IOException {
StreamDelegate root = createDelegate();
StreamDelegate versions = createVersionDelegate();
inputStream.mark(Integer.MAX_VALUE);
inputStream.readNamedTagLazy(versions);
inputStream.reset();
inputStream.readNamedTagLazy(root);
if (version != 1 && version != 2) {
throw new IOException("This schematic version is currently not supported");
throw new IOException("This schematic version is currently not supported (" + version + ")");
}
if (blocks != null) {
@ -240,9 +262,11 @@ public class FastSchematicReader extends NBTSchematicReader {
biomes = null;
BlockVector3 dimensions = BlockVector3.at(width, height, length);
BlockVector3 origin = BlockVector3.ZERO;
BlockVector3 origin;
if (offsetX != Integer.MIN_VALUE && offsetY != Integer.MIN_VALUE && offsetZ != Integer.MIN_VALUE) {
origin = BlockVector3.at(-offsetX, -offsetY, -offsetZ);
} else {
origin = BlockVector3.ZERO;
}
Clipboard clipboard = createOutput.apply(dimensions);
@ -352,7 +376,7 @@ public class FastSchematicReader extends NBTSchematicReader {
if (id == null) {
id = (StringTag) value.get("id");
if (id == null) {
return null;
continue;
}
}
value.put("id", id);
@ -363,7 +387,26 @@ public class FastSchematicReader extends NBTSchematicReader {
ent = fixEntity(ent);
BaseEntity state = new BaseEntity(type, ent);
Location loc = ent.getEntityLocation(clipboard);
if (brokenEntities) {
clipboard.createEntity(loc, state);
continue;
}
if (faweWritten == -1) {
int locX = loc.getBlockX();
int locY = loc.getBlockY();
int locZ = loc.getBlockZ();
BlockVector3 max = min.add(dimensions).subtract(BlockVector3.ONE);
if (locX < min.getX() || locY < min.getY() || locZ < min.getZ()
|| locX > max.getX() || locY > max.getY() || locZ > max.getZ()) {
for (Entity e : clipboard.getEntities()) {
clipboard.removeEntity(e);
}
log.error("Detected schematic entity outside clipboard region. FAWE will not load entities. "
+ "Please try loading the schematic with the format \"legacyentity\"");
break;
}
}
clipboard.createEntity(loc.setPosition(loc.subtract(min.toVector3())), state);
} else {
log.debug("Invalid entity: " + id);
}
@ -372,7 +415,7 @@ public class FastSchematicReader extends NBTSchematicReader {
clipboard.setOrigin(origin);
if (!min.equals(BlockVector3.ZERO)) {
new BlockArrayClipboard(clipboard, min);
clipboard = new BlockArrayClipboard(clipboard, min);
}
return clipboard;

View File

@ -19,10 +19,10 @@
package com.sk89q.worldedit.extent.clipboard.io;
import com.boydti.fawe.Fawe;
import com.boydti.fawe.jnbt.streamer.IntValueReader;
import com.boydti.fawe.object.FaweOutputStream;
import com.boydti.fawe.util.IOUtil;
import com.google.common.collect.Maps;
import com.sk89q.jnbt.CompoundTag;
import com.sk89q.jnbt.IntArrayTag;
import com.sk89q.jnbt.ListTag;
@ -39,6 +39,7 @@ import com.sk89q.worldedit.extent.clipboard.Clipboard;
import com.sk89q.worldedit.function.visitor.Order;
import com.sk89q.worldedit.math.BlockVector3;
import com.sk89q.worldedit.regions.Region;
import com.sk89q.worldedit.util.Location;
import com.sk89q.worldedit.world.biome.BiomeType;
import com.sk89q.worldedit.world.biome.BiomeTypes;
import com.sk89q.worldedit.world.block.BaseBlock;
@ -58,8 +59,6 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import static com.google.common.base.Preconditions.checkNotNull;
@ -72,6 +71,7 @@ public class FastSchematicWriter implements ClipboardWriter {
private static final int MAX_SIZE = Short.MAX_VALUE - Short.MIN_VALUE;
private final NBTOutputStream outputStream;
private boolean brokenEntities = false;
/**
* Create a new schematic writer.
@ -83,6 +83,10 @@ public class FastSchematicWriter implements ClipboardWriter {
this.outputStream = outputStream;
}
public void setBrokenEntities(boolean brokenEntities) {
this.brokenEntities = brokenEntities;
}
@Override
public void write(Clipboard clipboard) throws IOException {
// For now always write the latest version. Maybe provide support for earlier if more appear.
@ -132,6 +136,7 @@ public class FastSchematicWriter implements ClipboardWriter {
out1.writeNamedTag("WEOffsetX", offset.getBlockX());
out1.writeNamedTag("WEOffsetY", offset.getBlockY());
out1.writeNamedTag("WEOffsetZ", offset.getBlockZ());
out1.writeNamedTag("FAWEVersion", Fawe.get().getVersion().build);
});
ByteArrayOutputStream blocksCompressed = new ByteArrayOutputStream();
@ -240,8 +245,12 @@ public class FastSchematicWriter implements ClipboardWriter {
// Store our location data, overwriting any
values.remove("id");
Location loc = entity.getLocation();
if (!brokenEntities) {
loc = loc.setPosition(loc.add(min.toVector3()));
}
values.put("Id", new StringTag(state.getType().getId()));
values.put("Pos", writeVector(entity.getLocation()));
values.put("Pos", writeVector(loc));
values.put("Rotation", writeRotation(entity.getLocation()));
CompoundTag entityTag = new CompoundTag(values);
@ -311,30 +320,6 @@ public class FastSchematicWriter implements ClipboardWriter {
}
}
private void writeEntities(Clipboard clipboard, NBTOutputStream schematic) throws IOException {
List<CompoundTag> entities = clipboard.getEntities().stream().map(e -> {
BaseEntity state = e.getState();
if (state == null) {
return null;
}
Map<String, Tag> values = Maps.newHashMap();
CompoundTag rawData = state.getNbtData();
if (rawData != null) {
values.putAll(rawData.getValue());
}
values.remove("id");
values.put("Id", new StringTag(state.getType().getId()));
values.put("Pos", writeVector(e.getLocation().toVector()));
values.put("Rotation", writeRotation(e.getLocation()));
return new CompoundTag(values);
}).filter(Objects::nonNull).collect(Collectors.toList());
if (entities.isEmpty()) {
return;
}
schematic.writeNamedTag("Entities", new ListTag(CompoundTag.class, entities));
}
@Override
public void close() throws IOException {
outputStream.close();

View File

@ -378,7 +378,7 @@ public class SchematicReader implements ClipboardReader {
BlockVector3 min = BlockVector3.at(originX, originY, originZ);
if (!min.equals(BlockVector3.ZERO)) {
new BlockArrayClipboard(clipboard, min);
clipboard = new BlockArrayClipboard(clipboard, min);
}
return clipboard;
}

View File

@ -19,6 +19,7 @@
package com.sk89q.worldedit.extent.clipboard.io;
import com.boydti.fawe.Fawe;
import com.google.common.collect.Maps;
import com.sk89q.jnbt.ByteArrayTag;
import com.sk89q.jnbt.CompoundTag;
@ -114,6 +115,7 @@ public class SpongeSchematicWriter implements ClipboardWriter {
metadata.put("WEOffsetX", new IntTag(offset.getBlockX()));
metadata.put("WEOffsetY", new IntTag(offset.getBlockY()));
metadata.put("WEOffsetZ", new IntTag(offset.getBlockZ()));
metadata.put("FAWEVersion", new IntTag(Fawe.get().getVersion().build));
schematic.put("Metadata", new CompoundTag(metadata));