From 870a96e9c00cac268af541dd29b7dce3ca310198 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Sun, 16 Jun 2024 17:15:35 +0200 Subject: [PATCH] chore/feat: more work on the fast v3 reader --- .../clipboard/io/FastSchematicReaderV3.java | 311 +++++++++++++++++- .../clipboard/io/FastSchematicWriterV2.java | 2 +- .../internal/io/VarIntStreamIterator.java | 70 ++++ .../worldedit/command/SchematicCommands.java | 2 +- .../clipboard/io/BuiltInClipboardFormat.java | 123 ++++--- .../clipboard/io/sponge/ReaderUtil.java | 92 +++--- .../io/sponge/VersionedDataFixer.java | 4 +- 7 files changed, 503 insertions(+), 101 deletions(-) create mode 100644 worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/VarIntStreamIterator.java diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV3.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV3.java index dc5bd7c34..28f44ec1e 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV3.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV3.java @@ -1,45 +1,326 @@ package com.fastasyncworldedit.core.extent.clipboard.io; +import com.fastasyncworldedit.core.extent.clipboard.LinearClipboard; +import com.fastasyncworldedit.core.extent.clipboard.SimpleClipboard; +import com.fastasyncworldedit.core.internal.io.VarIntStreamIterator; +import com.fastasyncworldedit.core.math.MutableBlockVector3; import com.sk89q.jnbt.NBTConstants; import com.sk89q.jnbt.NBTInputStream; import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.input.InputParseException; import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.clipboard.io.ClipboardReader; +import com.sk89q.worldedit.extent.clipboard.io.sponge.VersionedDataFixer; +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.math.BlockVector3; 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 org.apache.logging.log4j.Logger; import java.io.DataInputStream; import java.io.IOException; import java.util.Objects; +import java.util.OptionalInt; +import java.util.UUID; +import java.util.function.BiConsumer; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +/** + * ClipboardReader for the Sponge Schematic Format v3. + * Not necessarily much faster than {@link com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV3Reader}, but uses a + * stream based approach to keep the memory overhead minimal (especially in larger schematics) + */ @SuppressWarnings("removal") // JNBT public class FastSchematicReaderV3 implements ClipboardReader { - private final DataInputStream dataStream; - private final NBTInputStream nbtStream; + private static final Logger LOGGER = LogManagerCompat.getLogger(); - public FastSchematicReaderV3(DataInputStream dataInputStream, NBTInputStream inputStream) { - this.dataStream = Objects.requireNonNull(dataInputStream, "dataInputStream"); - this.nbtStream = Objects.requireNonNull(inputStream, "inputStream"); + private final DataInputStream dataInputStream; + private final NBTInputStream nbtInputStream; + + private VersionedDataFixer dataFixer; + private MutableBlockVector3 dimensions = MutableBlockVector3.at(0, 0, 0); + private BlockVector3 offset; + private BlockState[] blockPalette; + private BiomeType[] biomePalette; + private int dataVersion = -1; + + private boolean blocksWritten = false; + private boolean biomesWritten = false; + private boolean entitiesWritten = false; + + private boolean needAdditionalIterate = true; + + public FastSchematicReaderV3( + DataInputStream dataInputStream, + NBTInputStream nbtInputStream + ) throws IOException { + this.dataInputStream = Objects.requireNonNull(dataInputStream, "dataInputStream"); + this.nbtInputStream = Objects.requireNonNull(nbtInputStream, "nbtInputStream"); + if (!dataInputStream.markSupported()) { + throw new IOException("InputStream does not support mark"); + } } @Override - public Clipboard read() throws IOException { - final DataFixer dataFixer = - WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataFixer(); - dataStream.skipNBytes(1 + 2); // 1 Byte = TAG_Compound, 2 Bytes = Short (Length of tag name = "") - dataStream.skipNBytes(1 + 2 + 9); // as above + 9 bytes = "Schematic" + public Clipboard read(final UUID uuid, final Function createOutput) throws IOException { + dataInputStream.skipNBytes(1 + 2); // 1 Byte = TAG_Compound, 2 Bytes = Short (Length of tag name = "") + dataInputStream.skipNBytes(1 + 2 + 9); // as above + 9 bytes = "Schematic" + this.dataInputStream.mark(Integer.MAX_VALUE); // allow resets to basically the start of stream (file) - just skip header - byte type; - while ((type = dataStream.readByte()) != NBTConstants.TYPE_END) { - + Clipboard clipboard = null; + + while (needAdditionalIterate) { + this.needAdditionalIterate = false; + this.dataInputStream.reset(); + this.dataInputStream.mark(Integer.MAX_VALUE); + + while (dataInputStream.readByte() != NBTConstants.TYPE_END) { + String tag = readTagName(); + switch (tag) { + case "Version" -> this.dataInputStream.skipNBytes(4); // We know it's v3 (skip 4 byte version int) + case "DataVersion" -> { + this.dataVersion = this.dataInputStream.readInt(); + this.dataFixer = new VersionedDataFixer( + this.dataVersion, + WorldEdit + .getInstance() + .getPlatformManager() + .queryCapability(Capability.WORLD_EDITING) + .getDataFixer() + ); + } + case "Width", "Height", "Length" -> { + if (clipboard != null) { + continue; + } + if (tag.equals("Width")) { + this.dimensions.mutX(this.dataInputStream.readShort() & 0xFFFF); + } else if (tag.equals("Height")) { + this.dimensions.mutY(this.dataInputStream.readShort() & 0xFFFF); + } else { + this.dimensions.mutZ(this.dataInputStream.readShort() & 0xFFFF); + } + if (areDimensionsAvailable()) { + clipboard = createOutput.apply(this.dimensions); + } + } + case "Offset" -> { + this.dataInputStream.skipNBytes(4); // Array Length field (4 byte int) + this.offset = BlockVector3.at( + this.dataInputStream.readInt(), + this.dataInputStream.readInt(), + this.dataInputStream.readInt() + ); + } + case "Metadata" -> this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_COMPOUND, 0); // Skip metadata + case "Blocks" -> { + if (clipboard == null) { + needAdditionalIterate = true; + } else { + this.readBlocks(clipboard); + } + } + case "Biomes" -> { + if (clipboard == null) { + needAdditionalIterate = true; + } else { + this.readBiomes(clipboard); + } + } + case "Entities" -> { + if (clipboard == null) { + needAdditionalIterate = true; + } else { + this.readEntities(clipboard); + } + } + } + } } - return null; + if (clipboard == null) { + throw new NullPointerException("Failed to read schematic: Clipboard is null"); + } + clipboard.setOrigin(this.offset); + if (clipboard instanceof SimpleClipboard simpleClipboard && !this.offset.equals(BlockVector3.ZERO)) { + clipboard = new BlockArrayClipboard(simpleClipboard, this.offset); + } + return clipboard; + } + + @Override + public OptionalInt getDataVersion() { + return this.dataVersion > -1 ? OptionalInt.of(this.dataVersion) : OptionalInt.empty(); + } + + // TODO: BlockEntities + private void readBlocks(Clipboard target) throws IOException { + BiConsumer blockStateApplier; + if (target instanceof LinearClipboard linearClipboard) { + blockStateApplier = (dataIndex, paletteIndex) -> linearClipboard.setBlock(dataIndex, this.blockPalette[paletteIndex]); + } else { + blockStateApplier = (dataIndex, paletteIndex) -> { + int y = dataIndex / (dimensions.x() * dimensions.z()); + int remainder = dataIndex - (y * dimensions.x() * dimensions.z()); + int z = remainder / dimensions.x(); + int x = remainder - z * dimensions.x(); + target.setBlock(x, y, z, this.blockPalette[paletteIndex]); + }; + } + this.blockPalette = new BlockState[BlockTypesCache.states.length]; + readPalette( + () -> this.blockPalette.length == 0, + () -> this.blocksWritten, + () -> this.blocksWritten = true, + (value, index) -> { + value = dataFixer.fixUp(DataFixer.FixTypes.BLOCK_STATE, value); + try { + this.blockPalette[index] = BlockState.get(value); + } catch (InputParseException e) { + LOGGER.warn("Invalid BlockState in palette: {}. Block will be replaced with air.", value); + this.blockPalette[index] = BlockTypes.AIR.getDefaultState(); + } + }, + blockStateApplier, + (type, tag) -> { + if (!tag.equals("BlockEntities")) { + LOGGER.warn("Found additional tag in block palette: {}. Will skip tag.", tag); + try { + this.nbtInputStream.readTagPayloadLazy(type, 0); + } catch (IOException e) { + LOGGER.error("Failed to skip additional tag", e); + } + return; + } + // TODO: Process block entities + try { + this.nbtInputStream.readTagPayloadLazy(type, 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + ); + } + + private void readBiomes(Clipboard target) throws IOException { + BiConsumer biomeApplier; + if (target instanceof LinearClipboard linearClipboard) { + biomeApplier = (dataIndex, paletteIndex) -> linearClipboard.setBiome(dataIndex, this.biomePalette[paletteIndex]); + } else { + biomeApplier = (dataIndex, paletteIndex) -> { + int y = dataIndex / (dimensions.x() * dimensions.z()); + int remainder = dataIndex - (y * dimensions.x() * dimensions.z()); + int z = remainder / dimensions.x(); + int x = remainder - z * dimensions.x(); + target.setBiome(x, y, z, this.biomePalette[paletteIndex]); + }; + } + this.biomePalette = new BiomeType[BiomeType.REGISTRY.size()]; + readPalette( + () -> this.biomePalette.length == 0, + () -> this.biomesWritten, + () -> this.biomesWritten = true, + (value, index) -> { + value = dataFixer.fixUp(DataFixer.FixTypes.BIOME, value); + BiomeType biomeType = BiomeTypes.get(value); + if (biomeType == null) { + biomeType = BiomeTypes.PLAINS; + LOGGER.warn("Invalid biome type in palette: {}. Biome will be replaced with plains.", value); + } + this.biomePalette[index] = biomeType; + }, + biomeApplier, + (type, tag) -> { + LOGGER.warn("Found additional tag in biome palette: {}. Will skip tag.", tag); + try { + this.nbtInputStream.readTagPayloadLazy(type, 0); + } catch (IOException e) { + LOGGER.error("Failed to skip additional tag", e); + } + } + ); + } + + private void readEntities(Clipboard target) throws IOException { + // TODO + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_LIST, 0); + } + + /** + * The `Palette` tag is required first, as that contains the information of the actual palette size. + * Keeping the whole Data block in memory - which *could* be compressed - is just not it + * + * @param paletteInitializer Invoked for each 'Palette' entry using the actual palette value (e.g. block state) + index + * @param paletteDataApplier Invoked for each 'Data' entry using the data index and the palette index at the data index + */ + private void readPalette( + BooleanSupplier paletteAlreadyInitialized, + BooleanSupplier dataAlreadyWritten, + Runnable firstWrite, + BiConsumer paletteInitializer, + BiConsumer paletteDataApplier, + BiConsumer additionalTag + ) throws IOException { + if (dataAlreadyWritten.getAsBoolean()) { + return; + } + boolean hasPalette = paletteAlreadyInitialized.getAsBoolean(); + byte type; + String tag; + while ((type = this.dataInputStream.readByte()) != NBTConstants.TYPE_END) { + tag = readTagName(); + if (tag.equals("Palette")) { + if (hasPalette) { + // Skip data, as palette already exists + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_COMPOUND, 0); + } else { + // Read all palette entries + while (this.dataInputStream.readByte() != NBTConstants.TYPE_END) { + String value = this.dataInputStream.readUTF(); + char index = (char) this.dataInputStream.readInt(); + paletteInitializer.accept(value, index); + } + hasPalette = true; + } + continue; + } + if (tag.equals("Data")) { + // No palette or dimensions are yet available - will need to read Data next round + if (!hasPalette || !areDimensionsAvailable()) { + this.needAdditionalIterate = true; + return; + } + int length = this.dataInputStream.readInt(); + // Write data into clipboard + firstWrite.run(); + int i = 0; + for (var iter = new VarIntStreamIterator(this.dataInputStream, length); iter.hasNext(); i++) { + paletteDataApplier.accept(i, (char) iter.nextInt()); + } + continue; + } + additionalTag.accept(type, tag); + } + } + + private String readTagName() throws IOException { + return dataInputStream.readUTF(); + } + + private boolean areDimensionsAvailable() { + return this.dimensions.x() != 0 && this.dimensions.y() != 0 && this.dimensions.z() != 0; } @Override public void close() throws IOException { - nbtStream.close(); // closes the DataInputStream implicitly + nbtInputStream.close(); // closes the DataInputStream implicitly } } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV2.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV2.java index b84052794..93ac9acda 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV2.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV2.java @@ -50,7 +50,7 @@ import static com.google.common.base.Preconditions.checkNotNull; */ public class FastSchematicWriterV2 implements ClipboardWriter { - private static final int CURRENT_VERSION = 2; + public static final int CURRENT_VERSION = 2; private static final int MAX_SIZE = Short.MAX_VALUE - Short.MIN_VALUE; private final NBTOutputStream outputStream; diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/VarIntStreamIterator.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/VarIntStreamIterator.java new file mode 100644 index 000000000..17f4b9f0a --- /dev/null +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/VarIntStreamIterator.java @@ -0,0 +1,70 @@ +package com.fastasyncworldedit.core.internal.io; + +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.PrimitiveIterator; + +/** + * Basically {@link com.sk89q.worldedit.internal.util.VarIntIterator} but backed by {@link java.io.InputStream} + */ +public class VarIntStreamIterator implements PrimitiveIterator.OfInt { + + private final InputStream parent; + private final int limit; + private int index; + private boolean hasNextInt; + private int nextInt; + + public VarIntStreamIterator(final InputStream parent, int limit) { + this.parent = parent; + this.limit = limit; + } + + @Override + public boolean hasNext() { + if (hasNextInt) { + return true; + } + if (index >= limit) { + return false; + } + + try { + nextInt = readNextInt(); + } catch (IOException e) { + throw new RuntimeException(e); + } + return hasNextInt = true; + } + + @Override + public int nextInt() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + hasNextInt = false; + return nextInt; + } + + + private int readNextInt() throws IOException { + int value = 0; + for (int bitsRead = 0; ; bitsRead += 7) { + if (index >= limit) { + throw new IllegalStateException("Ran out of bytes while reading VarInt (probably corrupted data)"); + } + byte next = (byte) this.parent.read(); + index++; + value |= (next & 0x7F) << bitsRead; + if (bitsRead > 7 * 5) { + throw new IllegalStateException("VarInt too big (probably corrupted data)"); + } + if ((next & 0x80) == 0) { + break; + } + } + return value; + } + +} 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 920a24901..120327615 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 @@ -396,7 +396,7 @@ public class SchematicCommands { .isInSubDirectory(saveDir, file)) + ")")); return; } - if (format == null) { + if (format == null || !format.isFormat(file)) { format = ClipboardFormats.findByFile(file); if (format == null) { actor.print(Caption.of("worldedit.schematic.unknown-format", TextComponent.of(formatName))); 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 index 21e04f386..6db11e6cf 100644 --- 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 @@ -20,6 +20,7 @@ package com.sk89q.worldedit.extent.clipboard.io; import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReaderV2; +import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReaderV3; import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriterV2; import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriterV3; import com.fastasyncworldedit.core.extent.clipboard.io.schematic.MinecraftStructure; @@ -28,6 +29,7 @@ import com.fastasyncworldedit.core.internal.io.ResettableFileInputStream; import com.google.common.collect.ImmutableSet; import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.IntTag; +import com.sk89q.jnbt.NBTConstants; import com.sk89q.jnbt.NBTInputStream; import com.sk89q.jnbt.NBTOutputStream; import com.sk89q.jnbt.NamedTag; @@ -41,11 +43,13 @@ import org.anarres.parallelgzip.ParallelGZIPOutputStream; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +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.nio.file.Files; import java.util.Locale; import java.util.Map; import java.util.Set; @@ -55,13 +59,21 @@ import java.util.zip.GZIPOutputStream; /** * A collection of supported clipboard formats. */ +@SuppressWarnings("removal") //FAWE: suppress JNBT deprecations public enum BuiltInClipboardFormat implements ClipboardFormat { //FAWE start - register fast clipboard io - FAST_NEW("new_fast") { // For testing purposes + FAST_V3("fast", "fawe", "schem") { // For testing purposes + @Override - public ClipboardReader getReader(final InputStream inputStream) throws IOException { - return SPONGE_V3_SCHEMATIC.getReader(inputStream); // TODO: use FastSchematicReaderV3 when finished + public ClipboardReader getReader(InputStream inputStream) throws IOException { + if (inputStream instanceof FileInputStream fileInputStream) { + inputStream = new ResettableFileInputStream(fileInputStream); + } + BufferedInputStream buffered = new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(inputStream))); + DataInputStream dataInputStream = new DataInputStream(buffered); + NBTInputStream nbtStream = new NBTInputStream(buffered); + return new FastSchematicReaderV3(dataInputStream, nbtStream); } @Override @@ -79,45 +91,41 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { @Override public boolean isFormat(final File file) { - return FAST.isFormat(file); - /** - * TODO: test if this actually works - * try (final DataInputStream stream = new DataInputStream(new GZIPInputStream(Files.newInputStream(file.toPath()))); - * final NBTInputStream nbt = new NBTInputStream(stream)) { - * if (stream.readByte() != NBTConstants.TYPE_COMPOUND) { - * return false; - * } - * stream.readShort(); // TAG name length ("" = 0), no need to read name as no bytes are written for root tag - * if (stream.readByte() != NBTConstants.TYPE_COMPOUND) { - * return false; - * } - * stream.readShort(); // TAG name length ("Schematic" = 9) - * stream.skipNBytes(9); // "Schematic" - * - * // We can't guarantee the specific order of nbt data, so scan and skip, if required - * do { - * byte type = stream.readByte(); - * String name = stream.readUTF(); - * if (type == NBTConstants.TYPE_END) { - * return false; - * } - * if (type == NBTConstants.TYPE_INT && name.equals("Version")) { - * return stream.readInt() == FastSchematicWriterV3.CURRENT_VERSION; - * } - * nbt.readTagPayloadLazy(type, 1); - * } while (true); - * } catch (IOException ignored) { - * } - * return false; - */ + try (final DataInputStream stream = new DataInputStream(new GZIPInputStream(Files.newInputStream(file.toPath()))); + final NBTInputStream nbt = new NBTInputStream(stream)) { + if (stream.readByte() != NBTConstants.TYPE_COMPOUND) { + return false; + } + stream.skipNBytes(2); // TAG name length ("" = 0), no need to read name as no bytes are written for root tag + if (stream.readByte() != NBTConstants.TYPE_COMPOUND) { + return false; + } + stream.skipNBytes(2); // TAG name length ("Schematic" = 9) + stream.skipNBytes(9); // "Schematic" + + // We can't guarantee the specific order of nbt data, so scan and skip, if required + do { + byte type = stream.readByte(); + String name = stream.readUTF(); + if (type == NBTConstants.TYPE_END) { + return false; + } + if (type == NBTConstants.TYPE_INT && name.equals("Version")) { + return stream.readInt() == FastSchematicWriterV3.CURRENT_VERSION; + } + nbt.readTagPayloadLazy(type, 0); + } while (true); + } catch (IOException ignored) { + } + return false; } @Override public String getPrimaryFileExtension() { - return FAST.getPrimaryFileExtension(); + return "schem"; } }, - FAST("fast", "fawe", "sponge", "schem") { + FAST_V2("fast.2", "fawe.2", "schem.2") { @Override public String getPrimaryFileExtension() { return "schem"; @@ -148,8 +156,29 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { @Override public boolean isFormat(File file) { - String name = file.getName().toLowerCase(Locale.ROOT); - return name.endsWith(".schem") || name.endsWith(".sponge"); + try (final DataInputStream stream = new DataInputStream(new GZIPInputStream(Files.newInputStream(file.toPath()))); + final NBTInputStream nbt = new NBTInputStream(stream)) { + if (stream.readByte() != NBTConstants.TYPE_COMPOUND) { + return false; + } + stream.skipNBytes(2); // TAG name length ("Schematic" = 9) + stream.skipNBytes(9); // "Schematic" + + // We can't guarantee the specific order of nbt data, so scan and skip, if required + do { + byte type = stream.readByte(); + String name = stream.readUTF(); + if (type == NBTConstants.TYPE_END) { + return false; + } + if (type == NBTConstants.TYPE_INT && name.equals("Version")) { + return stream.readInt() == FastSchematicWriterV2.CURRENT_VERSION; + } + nbt.readTagPayloadLazy(type, 0); + } while (true); + } catch (IOException ignored) { + } + return false; } }, @@ -231,7 +260,7 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { * Avoid using with any large schematics/clipboards for reading/writing. */ @Deprecated - SPONGE_V2_SCHEMATIC("slow", "safe", "sponge.2") { + SPONGE_V2_SCHEMATIC("slow.2", "safe.2", "sponge.2") { @Override public String getPrimaryFileExtension() { return "schem"; @@ -271,8 +300,7 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { return true; } }, - SPONGE_V3_SCHEMATIC("sponge.3", "sponge", "schem") { - + SPONGE_V3_SCHEMATIC("sponge.3", "slow", "safe") { @Override public String getPrimaryFileExtension() { return "schem"; @@ -299,7 +327,7 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { return false; } Tag schematicTag = rootCompoundTag.getValue() - .get("Schematic"); + .get("Schematic"); if (!(schematicTag instanceof CompoundTag)) { return false; } @@ -426,6 +454,17 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { @Deprecated public static final BuiltInClipboardFormat SPONGE_SCHEMATIC = SPONGE_V2_SCHEMATIC; + //FAWE start + /** + * For backwards compatibility, this points to the fast implementation of the Sponge Schematic Specification (Version 2) + * format. This should not be used going forwards. + * + * @deprecated Use {@link #FAST_V2} or {@link #FAST_V3} + */ + @Deprecated + public static final BuiltInClipboardFormat FAST = FAST_V2; + //FAWE end + private final ImmutableSet aliases; BuiltInClipboardFormat(String... aliases) { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/ReaderUtil.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/ReaderUtil.java index ddcff3fbf..09706e017 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/ReaderUtil.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/ReaderUtil.java @@ -62,56 +62,64 @@ import static com.sk89q.worldedit.extent.clipboard.io.SchematicNbtUtil.requireTa /** * Common code shared between schematic readers. */ -class ReaderUtil { +public class ReaderUtil { //FAWE - make public + private static final Logger LOGGER = LogManagerCompat.getLogger(); static void checkSchematicVersion(int version, CompoundTag schematicTag) throws IOException { int schematicVersion = requireTag(schematicTag.getValue(), "Version", IntTag.class) - .getValue(); + .getValue(); checkState( - version == schematicVersion, - "Schematic is not version %s, but %s", version, schematicVersion + version == schematicVersion, + "Schematic is not version %s, but %s", version, schematicVersion ); } - static VersionedDataFixer getVersionedDataFixer(Map schematic, Platform platform, - int liveDataVersion) throws IOException { + static VersionedDataFixer getVersionedDataFixer( + Map schematic, Platform platform, + int liveDataVersion + ) throws IOException { + //FAWE - delegate to new method + return getVersionedDataFixer(requireTag(schematic, "DataVersion", IntTag.class).getValue(), platform, liveDataVersion); + } + + //FAWE start - make getVersionedDataFixer without schematic compound + public + public static VersionedDataFixer getVersionedDataFixer(int schematicDataVersion, Platform platform, int liveDataVersion) { DataFixer fixer = null; - int dataVersion = requireTag(schematic, "DataVersion", IntTag.class).getValue(); - if (dataVersion < 0) { + if (schematicDataVersion < 0) { LOGGER.warn( - "Schematic has an unknown data version ({}). Data may be incompatible.", - dataVersion + "Schematic has an unknown data version ({}). Data may be incompatible.", + schematicDataVersion ); - } else if (dataVersion > liveDataVersion) { + } else if (schematicDataVersion > liveDataVersion) { LOGGER.warn( - "Schematic was made in a newer Minecraft version ({} > {})." - + " Data may be incompatible.", - dataVersion, liveDataVersion + "Schematic was made in a newer Minecraft version ({} > {})." + + " Data may be incompatible.", + schematicDataVersion, liveDataVersion ); - } else if (dataVersion < liveDataVersion) { + } else if (schematicDataVersion < liveDataVersion) { fixer = platform.getDataFixer(); if (fixer != null) { LOGGER.debug( - "Schematic was made in an older Minecraft version ({} < {})," - + " will attempt DFU.", - dataVersion, liveDataVersion + "Schematic was made in an older Minecraft version ({} < {})," + + " will attempt DFU.", + schematicDataVersion, liveDataVersion ); } else { LOGGER.info( - "Schematic was made in an older Minecraft version ({} < {})," - + " but DFU is not available. Data may be incompatible.", - dataVersion, liveDataVersion + "Schematic was made in an older Minecraft version ({} < {})," + + " but DFU is not available. Data may be incompatible.", + schematicDataVersion, liveDataVersion ); } } - - return new VersionedDataFixer(dataVersion, fixer); + return new VersionedDataFixer(schematicDataVersion, fixer); } + //FAWE end static Map decodePalette( - Map paletteObject, VersionedDataFixer fixer + Map paletteObject, VersionedDataFixer fixer ) throws IOException { Map palette = new HashMap<>(); @@ -136,15 +144,15 @@ class ReaderUtil { } static void initializeClipboardFromBlocks( - Clipboard clipboard, Map palette, byte[] blocks, ListTag tileEntities, - VersionedDataFixer fixer, boolean dataIsNested + Clipboard clipboard, Map palette, byte[] blocks, ListTag tileEntities, + VersionedDataFixer fixer, boolean dataIsNested ) throws IOException { Map> tileEntitiesMap = new HashMap<>(); if (tileEntities != null) { List> tileEntityTags = tileEntities.getValue().stream() - .map(tag -> (CompoundTag) tag) - .map(CompoundTag::getValue) - .collect(Collectors.toList()); + .map(tag -> (CompoundTag) tag) + .map(CompoundTag::getValue) + .collect(Collectors.toList()); for (Map tileEntity : tileEntityTags) { int[] pos = requireTag(tileEntity, "Pos", IntArrayTag.class).getValue(); @@ -168,8 +176,8 @@ class ReaderUtil { values.put("id", tileEntity.get("Id")); if (fixer.isActive()) { tileEntity = ((CompoundTag) AdventureNBTConverter.fromAdventure(fixer.fixUp( - DataFixer.FixTypes.BLOCK_ENTITY, - new CompoundTag(values).asBinaryTag() + DataFixer.FixTypes.BLOCK_ENTITY, + new CompoundTag(values).asBinaryTag() ))).getValue(); } else { tileEntity = values; @@ -191,7 +199,7 @@ class ReaderUtil { Map tileEntity = tileEntitiesMap.get(offsetPos); if (tileEntity != null) { clipboard.setBlock( - offsetPos, state.toBaseBlock(new CompoundTag(tileEntity)) + offsetPos, state.toBaseBlock(new CompoundTag(tileEntity)) ); } else { clipboard.setBlock(offsetPos, state); @@ -222,8 +230,10 @@ class ReaderUtil { return BlockVector3.at(parts[0], parts[1], parts[2]); } - static void readEntities(BlockArrayClipboard clipboard, List entList, - VersionedDataFixer fixer, boolean positionIsRelative) throws IOException { + static void readEntities( + BlockArrayClipboard clipboard, List entList, + VersionedDataFixer fixer, boolean positionIsRelative + ) throws IOException { if (entList.isEmpty()) { return; } @@ -249,20 +259,21 @@ class ReaderUtil { } CompoundTag dataTag = dataTagBuilder.putString("id", id).build(); dataTag = ((CompoundTag) AdventureNBTConverter.fromAdventure(fixer.fixUp( - DataFixer.FixTypes.ENTITY, - dataTag.asBinaryTag() + DataFixer.FixTypes.ENTITY, + dataTag.asBinaryTag() ))); EntityType entityType = EntityTypes.get(id); if (entityType != null) { - Location location = NBTConversions.toLocation(clipboard, - requireTag(tags, "Pos", ListTag.class), - requireTag(dataTag.getValue(), "Rotation", ListTag.class) + Location location = NBTConversions.toLocation( + clipboard, + requireTag(tags, "Pos", ListTag.class), + requireTag(dataTag.getValue(), "Rotation", ListTag.class) ); BaseEntity state = new BaseEntity(entityType, dataTag); if (positionIsRelative) { location = location.setPosition( - location.toVector().add(clipboard.getMinimumPoint().toVector3()) + location.toVector().add(clipboard.getMinimumPoint().toVector3()) ); } clipboard.createEntity(location, state); @@ -274,4 +285,5 @@ class ReaderUtil { private ReaderUtil() { } + } diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/VersionedDataFixer.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/VersionedDataFixer.java index 46978a1b8..35f6d58bc 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/VersionedDataFixer.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/VersionedDataFixer.java @@ -24,12 +24,12 @@ import com.sk89q.worldedit.world.DataFixer; import javax.annotation.Nullable; -final class VersionedDataFixer { +public final class VersionedDataFixer { //FAWE - public private final int dataVersion; @Nullable private final DataFixer fixer; - VersionedDataFixer(int dataVersion, @Nullable DataFixer fixer) { + public VersionedDataFixer(int dataVersion, @Nullable DataFixer fixer) { //FAWE - public this.dataVersion = dataVersion; this.fixer = fixer; }