From 1ec87e7092d9bddf246713e49efc1e3036af1257 Mon Sep 17 00:00:00 2001 From: Pierre Maurice Schwang Date: Sat, 13 Jul 2024 14:30:34 +0200 Subject: [PATCH] Support Sponge Schematic v3 (#2776) * Update to Sponge Schematic 3 Includes a major refactoring of how schematics are read. (cherry picked from commit bd475b1d4acbcf2a95e5a8f3aee50d2fb2100ae8) * Licenses lol (cherry picked from commit a5ce8a47657aa987da8ca625cd658856d2eb3477) * Fix imports (cherry picked from commit e1892b7bd4ff0ca4592f8cb4e1b2d9363c4cd6ff) * Update for final changes (cherry picked from commit 2f6b50a4276b33b615d9dbc52e73e958308735f9) * chore: ensure flushed clipboard in spongev2 writer * feat: initial work on FastSchematicWriterV2 * fix: only write into palette once, write into data as varint * chore: more work on FastSchematicWriterV3 * fix: make FastSchematicWriterV3 work * fix/chore: write pos as doubles * chore: start on reader (class exists at least) * chore: replace while loop with simple if as char can be max 2 bytes * chore/feat: more work on the fast v3 reader * fix: offset must be inverted for origin * chore: use the actual FileChannel for mark / reset (if present) * chore: add null check again * chore: buffer streams in isFormat check * chore/feat: read schematic fully * chore: don't hold a lazyreference (seems harder to gc with already computed value?) * chore: remove debugs * chore: optimize FastSchematicReaderV3 * chore: remove logger warn for now * chore: constant not required anymore * chore/feat: support non-file based inputstreams (using in memory LZ4 cache) * chore: don't wrap streams unnecessary * chore: cleanup * chore: since comment for reader + writer * chore: FAST_V3 not for testing anymore * chore: update schematic and clipboard logic for linbus changes * chore: undo format check on load * fix: remove usages of old nbt types * fix: use LinBus in FaweDelegateSchematicHandler * fix: use ReaderUtil again * chore: update supported schematic types for Arkitektonika * chore: check for magic bytes in schematic (not tested yet) * revert: magic bytes check * fix: fix paletteAlreadyInitialized + biome placement on linear clipboards * Update worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV3.java --------- Co-authored-by: Octavia Togami Co-authored-by: Hannes Greule --- .../FaweDelegateSchematicHandler.java | 16 +- .../bukkit/util/DoNotMiniseThese.java | 4 + ...Reader.java => FastSchematicReaderV2.java} | 4 +- .../clipboard/io/FastSchematicReaderV3.java | 818 ++++++++++++++++++ ...Writer.java => FastSchematicWriterV2.java} | 8 +- .../clipboard/io/FastSchematicWriterV3.java | 295 +++++++ .../internal/io/VarIntStreamIterator.java | 70 ++ .../core/jnbt/CompressedSchematicTag.java | 4 +- .../java/com/sk89q/jnbt/NBTInputStream.java | 2 +- .../clipboard/io/BuiltInClipboardFormat.java | 280 ++++-- .../extent/clipboard/io/ClipboardFormat.java | 25 +- .../clipboard/io/NBTSchematicReader.java | 4 + .../extent/clipboard/io/SchematicNbtUtil.java | 61 ++ .../clipboard/io/SpongeSchematicReader.java | 453 ---------- .../BuiltInClipboardShareDestinations.java | 11 +- .../clipboard/io/sponge/ReaderUtil.java | 283 ++++++ .../io/sponge/SpongeSchematicV1Reader.java | 134 +++ .../io/sponge/SpongeSchematicV2Reader.java | 144 +++ .../SpongeSchematicV2Writer.java} | 188 ++-- .../io/sponge/SpongeSchematicV3Reader.java | 168 ++++ .../io/sponge/SpongeSchematicV3Writer.java | 233 +++++ .../io/sponge/VersionedDataFixer.java | 47 + .../clipboard/io/sponge/WriterUtil.java | 91 ++ .../clipboard/io/sponge/package-info.java | 26 + .../internal/util/VarIntIterator.java | 81 ++ 25 files changed, 2815 insertions(+), 635 deletions(-) rename worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/{FastSchematicReader.java => FastSchematicReaderV2.java} (99%) create mode 100644 worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV3.java rename worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/{FastSchematicWriter.java => FastSchematicWriterV2.java} (98%) create mode 100644 worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV3.java create mode 100644 worldedit-core/src/main/java/com/fastasyncworldedit/core/internal/io/VarIntStreamIterator.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicNbtUtil.java delete mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/ReaderUtil.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV1Reader.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Reader.java rename worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/{SpongeSchematicWriter.java => sponge/SpongeSchematicV2Writer.java} (51%) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Reader.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Writer.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/VersionedDataFixer.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/WriterUtil.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/package-info.java create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/VarIntIterator.java diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateSchematicHandler.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateSchematicHandler.java index 4b798981e..5c6f97d21 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateSchematicHandler.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/regions/plotsquared/FaweDelegateSchematicHandler.java @@ -3,8 +3,8 @@ package com.fastasyncworldedit.bukkit.regions.plotsquared; import com.fastasyncworldedit.core.Fawe; import com.fastasyncworldedit.core.FaweAPI; import com.fastasyncworldedit.core.FaweCache; -import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReader; -import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriter; +import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReaderV2; +import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriterV2; import com.fastasyncworldedit.core.jnbt.CompressedCompoundTag; import com.fastasyncworldedit.core.jnbt.CompressedSchematicTag; import com.fastasyncworldedit.core.util.IOUtil; @@ -29,17 +29,19 @@ import com.sk89q.worldedit.WorldEdit; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.clipboard.io.BuiltInClipboardFormat; import com.sk89q.worldedit.extent.clipboard.io.MCEditSchematicReader; -import com.sk89q.worldedit.extent.clipboard.io.SpongeSchematicReader; +import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV3Reader; import com.sk89q.worldedit.internal.util.LogManagerCompat; import com.sk89q.worldedit.math.BlockVector3; import com.sk89q.worldedit.regions.CuboidRegion; import net.jpountz.lz4.LZ4BlockInputStream; import org.anarres.parallelgzip.ParallelGZIPOutputStream; import org.apache.logging.log4j.Logger; +import org.enginehub.linbus.stream.LinBinaryIO; import javax.annotation.Nonnull; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileNotFoundException; @@ -182,7 +184,7 @@ public class FaweDelegateSchematicHandler { try (OutputStream stream = new FileOutputStream(tmp); NBTOutputStream output = new NBTOutputStream( new BufferedOutputStream(new ParallelGZIPOutputStream(stream)))) { - new FastSchematicWriter(output).write(clipboard); + new FastSchematicWriterV2(output).write(clipboard); } } else { try (OutputStream stream = new FileOutputStream(tmp); @@ -239,7 +241,7 @@ public class FaweDelegateSchematicHandler { public Schematic getSchematic(@Nonnull InputStream is) { try { - FastSchematicReader schematicReader = new FastSchematicReader( + FastSchematicReaderV2 schematicReader = new FastSchematicReaderV2( new NBTInputStream(new BufferedInputStream(new GZIPInputStream(new BufferedInputStream(is))))); Clipboard clip = schematicReader.read(); return new Schematic(clip); @@ -249,8 +251,8 @@ public class FaweDelegateSchematicHandler { return null; } try { - SpongeSchematicReader schematicReader = - new SpongeSchematicReader(new NBTInputStream(new GZIPInputStream(is))); + SpongeSchematicV3Reader schematicReader = + new SpongeSchematicV3Reader(LinBinaryIO.read(new DataInputStream(new GZIPInputStream(is)))); Clipboard clip = schematicReader.read(); return new Schematic(clip); } catch (IOException e2) { diff --git a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/DoNotMiniseThese.java b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/DoNotMiniseThese.java index 9916a7d9f..28fb6d990 100644 --- a/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/DoNotMiniseThese.java +++ b/worldedit-bukkit/src/main/java/com/fastasyncworldedit/bukkit/util/DoNotMiniseThese.java @@ -1,6 +1,8 @@ package com.fastasyncworldedit.bukkit.util; import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.io.FastBufferedInputStream; +import it.unimi.dsi.fastutil.io.FastBufferedOutputStream; import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.longs.LongArraySet; import it.unimi.dsi.fastutil.longs.LongIterator; @@ -19,5 +21,7 @@ final class DoNotMiniseThese { private final LongSet d = null; private final Int2ObjectMap e = null; private final Object2ObjectArrayMap f = null; + private final FastBufferedInputStream g = null; + private final FastBufferedOutputStream h = null; } diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReader.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV2.java similarity index 99% rename from worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReader.java rename to worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV2.java index 71d139789..97fdd1f27 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReader.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV2.java @@ -53,7 +53,7 @@ import static com.google.common.base.Preconditions.checkNotNull; /** * Reads schematic files using the Sponge Schematic Specification. */ -public class FastSchematicReader extends NBTSchematicReader { +public class FastSchematicReaderV2 extends NBTSchematicReader { private static final Logger LOGGER = LogManagerCompat.getLogger(); private final NBTInputStream inputStream; @@ -88,7 +88,7 @@ public class FastSchematicReader extends NBTSchematicReader { * * @param inputStream the input stream to read from */ - public FastSchematicReader(NBTInputStream inputStream) { + public FastSchematicReaderV2(NBTInputStream inputStream) { checkNotNull(inputStream); this.inputStream = inputStream; this.fixer = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataFixer(); 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 new file mode 100644 index 000000000..db98d2203 --- /dev/null +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicReaderV3.java @@ -0,0 +1,818 @@ +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.ResettableFileInputStream; +import com.fastasyncworldedit.core.internal.io.VarIntStreamIterator; +import com.fastasyncworldedit.core.math.MutableBlockVector3; +import com.fastasyncworldedit.core.util.IOUtil; +import com.fastasyncworldedit.core.util.MathMan; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.NBTConstants; +import com.sk89q.jnbt.NBTInputStream; +import com.sk89q.jnbt.NBTOutputStream; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +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.ReaderUtil; +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.util.Location; +import com.sk89q.worldedit.util.concurrency.LazyReference; +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 it.unimi.dsi.fastutil.io.FastBufferedInputStream; +import it.unimi.dsi.fastutil.io.FastBufferedOutputStream; +import net.jpountz.lz4.LZ4BlockInputStream; +import net.jpountz.lz4.LZ4BlockOutputStream; +import org.apache.logging.log4j.Logger; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.jetbrains.annotations.ApiStatus; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.OptionalInt; +import java.util.Set; +import java.util.UUID; +import java.util.function.BooleanSupplier; +import java.util.function.Function; +import java.util.zip.GZIPInputStream; + +/** + * 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) + * + * @since TODO + */ +@SuppressWarnings("removal") // JNBT +public class FastSchematicReaderV3 implements ClipboardReader { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + private static final byte CACHE_IDENTIFIER_END = 0x00; + private static final byte CACHE_IDENTIFIER_BLOCK = 0x01; + private static final byte CACHE_IDENTIFIER_BIOMES = 0x02; + private static final byte CACHE_IDENTIFIER_ENTITIES = 0x03; + private static final byte CACHE_IDENTIFIER_BLOCK_TILE_ENTITIES = 0x04; + + private final InputStream parentStream; + private final MutableBlockVector3 dimensions = MutableBlockVector3.at(0, 0, 0); + private final Set remainingTags; + + private DataInputStream dataInputStream; + private NBTInputStream nbtInputStream; + + private VersionedDataFixer dataFixer; + private BlockVector3 offset; + private BlockState[] blockPalette; + private BiomeType[] biomePalette; + private int dataVersion = -1; + + // Only used if the InputStream is not file based (and therefor does not support resets based on FileChannels) + // and the file is unordered + // Data and Palette cache is separated, as the data requires a fully populated palette - and the order is not guaranteed + private byte[] dataCache; + private byte[] paletteCache; + private OutputStream dataCacheWriter; + private OutputStream paletteCacheWriter; + + + public FastSchematicReaderV3(@NonNull InputStream stream) { + Objects.requireNonNull(stream, "stream"); + if (stream instanceof ResettableFileInputStream) { + stream.mark(Integer.MAX_VALUE); + this.remainingTags = new HashSet<>(); + } else if (stream instanceof FileInputStream fileInputStream) { + stream = new ResettableFileInputStream(fileInputStream); + stream.mark(Integer.MAX_VALUE); + this.remainingTags = new HashSet<>(); + } else if (stream instanceof FastBufferedInputStream || stream instanceof BufferedInputStream) { + this.remainingTags = null; + } else { + stream = new FastBufferedInputStream(stream); + this.remainingTags = null; + } + this.parentStream = stream; + } + + @Override + public Clipboard read(final UUID uuid, final Function createOutput) throws IOException { + Clipboard clipboard = null; + + this.setSubStreams(); + skipHeader(this.dataInputStream); + + byte type; + String tag; + while ((type = dataInputStream.readByte()) != NBTConstants.TYPE_END) { + tag = this.dataInputStream.readUTF(); + switch (tag) { + case "DataVersion" -> { + final Platform platform = + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); + this.dataVersion = this.dataInputStream.readInt(); + this.dataFixer = ReaderUtil.getVersionedDataFixer(this.dataVersion, platform, platform.getDataVersion()); + } + 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 "Width" -> this.dimensions.mutX(this.dataInputStream.readShort() & 0xFFFF); + case "Height" -> this.dimensions.mutY(this.dataInputStream.readShort() & 0xFFFF); + case "Length" -> this.dimensions.mutZ(this.dataInputStream.readShort() & 0xFFFF); + case "Blocks" -> readBlocks(clipboard); + case "Biomes" -> readBiomes(clipboard); + case "Entities" -> readEntities(clipboard); + default -> this.nbtInputStream.readTagPayloadLazy(type, 0); + } + if (clipboard == null && this.areDimensionsAvailable()) { + clipboard = createOutput.apply(this.dimensions); + } + } + + if (clipboard == null) { + throw new IOException("Invalid schematic - missing dimensions"); + } + if (dataFixer == null) { + throw new IOException("Invalid schematic - missing DataVersion"); + } + + if (this.supportsReset() && !remainingTags.isEmpty()) { + readRemainingDataReset(clipboard); + } else if (this.dataCacheWriter != null || this.paletteCacheWriter != null) { + readRemainingDataCache(clipboard); + } + + clipboard.setOrigin(this.offset.multiply(-1)); + if (clipboard instanceof SimpleClipboard simpleClipboard && !this.offset.equals(BlockVector3.ZERO)) { + clipboard = new BlockArrayClipboard(simpleClipboard, this.offset); + } + return clipboard; + } + + + /** + * Reads all locally cached data (due to reset not being available) and applies them to the clipboard. + *

+ * Firstly, closes all cache writers (which adds the END identifier to each and fills the cache byte arrays on this instance) + * If required, creates all missing palettes first (as needed by all remaining data). + * At last writes all missing data (block states, tile entities, biomes, entities). + * + * @param clipboard The clipboard to write into. + * @throws IOException on I/O error. + */ + private void readRemainingDataCache(Clipboard clipboard) throws IOException { + byte identifier; + if (this.paletteCacheWriter != null) { + this.paletteCacheWriter.close(); + } + if (this.dataCacheWriter != null) { + this.dataCacheWriter.close(); + } + if (this.paletteCache != null) { + try (final DataInputStream cacheStream = new DataInputStream(new FastBufferedInputStream( + new LZ4BlockInputStream(new FastBufferedInputStream(new ByteArrayInputStream(this.paletteCache)))))) { + while ((identifier = cacheStream.readByte()) != CACHE_IDENTIFIER_END) { + if (identifier == CACHE_IDENTIFIER_BLOCK) { + this.readPaletteMap(cacheStream, this.provideBlockPaletteInitializer()); + continue; + } + if (identifier == CACHE_IDENTIFIER_BIOMES) { + this.readPaletteMap(cacheStream, this.provideBiomePaletteInitializer()); + continue; + } + throw new IOException("invalid cache state - got identifier: 0x" + identifier); + } + } + } + try (final DataInputStream cacheStream = new DataInputStream(new FastBufferedInputStream( + new LZ4BlockInputStream(new FastBufferedInputStream(new ByteArrayInputStream(this.dataCache))))); + final NBTInputStream cacheNbtIn = new NBTInputStream(cacheStream)) { + while ((identifier = cacheStream.readByte()) != CACHE_IDENTIFIER_END) { + switch (identifier) { + case CACHE_IDENTIFIER_BLOCK -> this.readPaletteData(cacheStream, this.getBlockWriter(clipboard)); + case CACHE_IDENTIFIER_BIOMES -> this.readPaletteData(cacheStream, this.getBiomeWriter(clipboard)); + case CACHE_IDENTIFIER_ENTITIES -> { + cacheStream.skipNBytes(1); // list child type (TAG_Compound) + this.readEntityContainers( + cacheStream, + cacheNbtIn, + DataFixer.FixTypes.ENTITY, + this.provideEntityTransformer(clipboard) + ); + } + case CACHE_IDENTIFIER_BLOCK_TILE_ENTITIES -> { + cacheStream.skipNBytes(1); // list child type (TAG_Compound) + this.readEntityContainers( + cacheStream, + cacheNbtIn, + DataFixer.FixTypes.BLOCK_ENTITY, + this.provideTileEntityTransformer(clipboard) + ); + } + default -> throw new IOException("invalid cache state - got identifier: 0x" + identifier); + } + } + } + } + + /** + * Reset the main stream of this clipboard and reads all remaining data that could not be read or fixed yet. + * Might need two iterations if the DataVersion tag is after the Blocks tag while the Palette inside the Blocks tag is not + * at the first position. + * + * @param clipboard The clipboard to write into. + * @throws IOException on I/O error. + */ + private void readRemainingDataReset(Clipboard clipboard) throws IOException { + byte type; + String tag; + outer: + while (!this.remainingTags.isEmpty()) { + this.reset(); + skipHeader(this.dataInputStream); + while ((type = dataInputStream.readByte()) != NBTConstants.TYPE_END) { + tag = dataInputStream.readUTF(); + byte b = tag.equals("Blocks") ? CACHE_IDENTIFIER_BLOCK : + tag.equals("Biomes") ? CACHE_IDENTIFIER_BIOMES : + tag.equals("Entities") ? CACHE_IDENTIFIER_ENTITIES : + CACHE_IDENTIFIER_END; + if (!this.remainingTags.remove(b)) { + this.nbtInputStream.readTagPayloadLazy(type, 0); + continue; + } + switch (tag) { + case "Blocks" -> readBlocks(clipboard); + case "Biomes" -> readBiomes(clipboard); + case "Entities" -> readEntities(clipboard); + default -> this.nbtInputStream.readTagPayloadLazy(type, 0); // Should never happen, but just in case + } + if (this.remainingTags.isEmpty()) { + break outer; + } + } + } + } + + /** + * {@inheritDoc} + *

+ * Requires {@link #read()}, {@link #read(UUID)} or {@link #read(UUID, Function)} to be called before. + */ + @Override + public OptionalInt getDataVersion() { + return this.dataVersion > -1 ? OptionalInt.of(this.dataVersion) : OptionalInt.empty(); + } + + private void readBlocks(Clipboard target) throws IOException { + this.blockPalette = new BlockState[BlockTypesCache.states.length]; + readPalette( + target != null, + CACHE_IDENTIFIER_BLOCK, + () -> this.blockPalette[0] != null, + this.provideBlockPaletteInitializer(), + this.getBlockWriter(target), + (type, tag) -> { + if (!tag.equals("BlockEntities")) { + try { + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_LIST, 0); + } catch (IOException e) { + LOGGER.error("Failed to skip additional tag", e); + } + return; + } + try { + this.readTileEntities(target); + } catch (IOException e) { + LOGGER.warn("Failed to read tile entities", e); + } + } + ); + } + + private void readBiomes(Clipboard target) throws IOException { + this.biomePalette = new BiomeType[BiomeType.REGISTRY.size()]; + readPalette( + target != null, + CACHE_IDENTIFIER_BIOMES, + () -> this.biomePalette[0] != null, + this.provideBiomePaletteInitializer(), + this.getBiomeWriter(target), + (type, tag) -> { + try { + this.nbtInputStream.readTagPayloadLazy(type, 0); + } catch (IOException e) { + LOGGER.error("Failed to skip additional tag in biome container: {}", tag, e); + } + } + ); + } + + private void readEntities(@Nullable Clipboard target) throws IOException { + if (target == null || this.dataFixer == null) { + if (supportsReset()) { + this.remainingTags.add(CACHE_IDENTIFIER_ENTITIES); + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_LIST, 0); + return; + } + // Easier than streaming for now + final NBTOutputStream cacheStream = new NBTOutputStream(this.getDataCacheWriter()); + cacheStream.writeByte(CACHE_IDENTIFIER_ENTITIES); + cacheStream.writeTagPayload(this.nbtInputStream.readTagPayload(NBTConstants.TYPE_LIST, 0)); + return; + } + if (this.dataInputStream.read() != NBTConstants.TYPE_COMPOUND) { + throw new IOException("Expected a compound block for entity"); + } + this.readEntityContainers( + this.dataInputStream, this.nbtInputStream, DataFixer.FixTypes.ENTITY, this.provideEntityTransformer(target) + ); + } + + private void readTileEntities(Clipboard target) throws IOException { + if (target == null || this.dataFixer == null) { + if (supportsReset()) { + this.remainingTags.add(CACHE_IDENTIFIER_BLOCK); // use block identifier, as this method will be called by + // readBlocks again + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_LIST, 0); + return; + } + // Easier than streaming for now + final NBTOutputStream cacheStream = new NBTOutputStream(this.getDataCacheWriter()); + cacheStream.writeByte(CACHE_IDENTIFIER_BLOCK_TILE_ENTITIES); + cacheStream.writeTagPayload(this.nbtInputStream.readTagPayload(NBTConstants.TYPE_LIST, 0)); + return; + } + if (this.dataInputStream.read() != NBTConstants.TYPE_COMPOUND) { + throw new IOException("Expected a compound block for tile entity"); + } + this.readEntityContainers( + this.dataInputStream, + this.nbtInputStream, + DataFixer.FixTypes.BLOCK_ENTITY, + this.provideTileEntityTransformer(target) + ); + } + + private void readEntityContainers( + DataInputStream stream, + NBTInputStream nbtStream, + DataFixer.FixType fixType, + EntityTransformer transformer + ) throws IOException { + double x, y, z; + LinCompoundTag tag; + String id; + byte type; + int count = stream.readInt(); + while (count-- > 0) { + x = -1; + y = -1; + z = -1; + tag = null; + id = null; + while ((type = stream.readByte()) != NBTConstants.TYPE_END) { + switch (type) { + // Depending on the type of entity container (tile vs "normal") the pos consists of either doubles or ints + case NBTConstants.TYPE_INT_ARRAY -> { + if (!stream.readUTF().equals("Pos")) { + throw new IOException("Expected INT_ARRAY tag to be Pos"); + } + stream.skipNBytes(4); // count of following ints - for pos = 3 + x = stream.readInt(); + y = stream.readInt(); + z = stream.readInt(); + } + case NBTConstants.TYPE_LIST -> { + if (!stream.readUTF().equals("Pos")) { + throw new IOException("Expected LIST tag to be Pos"); + } + if (stream.readByte() != NBTConstants.TYPE_DOUBLE) { + throw new IOException("Expected LIST Pos tag to contain DOUBLE"); + } + stream.skipNBytes(4); // count of following doubles - for pos = 3 + x = stream.readDouble(); + y = stream.readDouble(); + z = stream.readDouble(); + } + case NBTConstants.TYPE_STRING -> { + if (!stream.readUTF().equals("Id")) { + throw new IOException("Expected STRING tag to be Id"); + } + id = stream.readUTF(); + } + case NBTConstants.TYPE_COMPOUND -> { + if (!stream.readUTF().equals("Data")) { + throw new IOException("Expected COMPOUND tag to be Data"); + } + if (!(nbtStream.readTagPayload(NBTConstants.TYPE_COMPOUND, 0).toLinTag() instanceof LinCompoundTag lin)) { + throw new IOException("Data tag could not be read into LinCompoundTag"); + } + tag = lin; + } + default -> throw new IOException("Unexpected tag in compound: " + type); + } + } + if (id == null) { + throw new IOException("Missing Id tag in compound"); + } + if (x < 0 || y < 0 || z < 0) { + throw new IOException("Missing position for entity " + id); + } + if (tag == null) { + transformer.transform(x, y, z, id, LinCompoundTag.of(Map.of())); + continue; + } + tag = this.dataFixer.fixUp(fixType, tag); + if (tag == null) { + LOGGER.warn("Failed to fix-up entity for {} @ {},{},{} - skipping", id, x, y, z); + continue; + } + transformer.transform(x, y, z, id, tag); + } + } + + /** + * 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( + boolean hasClipboard, + byte paletteType, + BooleanSupplier paletteAlreadyInitialized, + PaletteInitializer paletteInitializer, + PaletteDataApplier paletteDataApplier, + AdditionalTagConsumer additionalTag + ) throws IOException { + boolean hasPalette = paletteAlreadyInitialized.getAsBoolean(); + byte type; + String tag; + while ((type = this.dataInputStream.readByte()) != NBTConstants.TYPE_END) { + tag = this.dataInputStream.readUTF(); + if (tag.equals("Palette")) { + if (hasPalette) { + // Skip palette, as already exists + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_COMPOUND, 0); + continue; + } + if (!this.readPaletteMap(this.dataInputStream, paletteInitializer)) { + if (this.supportsReset()) { + // Couldn't read - skip palette for now + this.remainingTags.add(paletteType); + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_COMPOUND, 0); + continue; + } + // Reset not possible, write into cache + final NBTOutputStream cacheWriter = new NBTOutputStream(this.getPaletteCacheWriter()); + cacheWriter.write(paletteType); + cacheWriter.writeTagPayload(this.nbtInputStream.readTagPayload(NBTConstants.TYPE_COMPOUND, 0)); + continue; + } + hasPalette = true; + continue; + } + if (tag.equals("Data")) { + // No palette or dimensions are yet available + if (!hasPalette || this.dataFixer == null || !hasClipboard) { + if (this.supportsReset()) { + this.remainingTags.add(paletteType); + this.nbtInputStream.readTagPayloadLazy(NBTConstants.TYPE_BYTE_ARRAY, 0); + continue; + } + // Reset not possible, write into cache + int byteLen = this.dataInputStream.readInt(); + final DataOutputStream cacheWriter = new DataOutputStream(this.getDataCacheWriter()); + cacheWriter.write(paletteType); + cacheWriter.writeInt(byteLen); + IOUtil.copy(this.dataInputStream, cacheWriter, byteLen); + continue; + } + this.readPaletteData(this.dataInputStream, paletteDataApplier); + continue; + } + additionalTag.accept(type, tag); + } + } + + private void readPaletteData(DataInputStream stream, PaletteDataApplier applier) throws IOException { + int length = stream.readInt(); + // Write data into clipboard + int i = 0; + if (needsVarIntReading(length)) { + for (var iter = new VarIntStreamIterator(stream, length); iter.hasNext(); i++) { + applier.apply(i, (char) iter.nextInt()); + } + return; + } + while (i < length) { + applier.apply(i++, (char) stream.readUnsignedByte()); + } + } + + /** + * Reads the CompoundTag containing the palette mapping ({@code index: value}) and passes each entry to the + * {@link PaletteInitializer}. + *

+ * This method expects that the identifier ({@link NBTConstants#TYPE_COMPOUND}) is already consumed from the stream. + * + * @param stream The stream to read the data from. + * @param initializer The initializer called for each entry with its index and backed value. + * @return {@code true} if the mapping could be read, {@code false} otherwise (e.g. DataFixer is not yet available). + * @throws IOException on I/O error. + */ + private boolean readPaletteMap(DataInputStream stream, PaletteInitializer initializer) throws IOException { + if (this.dataFixer == null) { + return false; + } + while (stream.readByte() != NBTConstants.TYPE_END) { + String value = stream.readUTF(); + char index = (char) stream.readInt(); + initializer.initialize(index, value); + } + return true; + } + + private void indexToPosition(int index, PositionConsumer supplier) { + int y = index / (dimensions.x() * dimensions.z()); + int remainder = index - (y * dimensions.x() * dimensions.z()); + int z = remainder / dimensions.x(); + int x = remainder - z * dimensions.x(); + supplier.accept(x, y, z); + } + + private PaletteDataApplier getBlockWriter(Clipboard target) { + if (target instanceof LinearClipboard linearClipboard) { + return (index, ordinal) -> linearClipboard.setBlock(index, this.blockPalette[ordinal]); + } + return (index, ordinal) -> indexToPosition(index, (x, y, z) -> target.setBlock(x, y, z, this.blockPalette[ordinal])); + } + + private PaletteDataApplier getBiomeWriter(Clipboard target) { + return (index, ordinal) -> indexToPosition(index, (x, y, z) -> target.setBiome(x, y, z, this.biomePalette[ordinal])); + } + + private PaletteInitializer provideBlockPaletteInitializer() { + return (index, value) -> { + if (this.dataFixer == null) { + throw new IllegalStateException("Can't read block palette map if DataFixer is not yet available"); + } + 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(); + } + }; + } + + private PaletteInitializer provideBiomePaletteInitializer() { + return (index, value) -> { + if (this.dataFixer == null) { + throw new IllegalStateException("Can't read biome palette map if DataFixer is not yet available"); + } + 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; + }; + } + + private EntityTransformer provideEntityTransformer(Clipboard clipboard) { + return (x, y, z, id, tag) -> { + EntityType type = EntityType.REGISTRY.get(id); + if (type == null) { + LOGGER.warn("Invalid entity id: {} - skipping", id); + return; + } + clipboard.createEntity( + new Location(clipboard, Location.at(x, y, z).add(clipboard.getMinimumPoint().toVector3())), + new BaseEntity(type, LazyReference.computed(tag)) + ); + }; + } + + private EntityTransformer provideTileEntityTransformer(Clipboard clipboard) { + //noinspection deprecation + return (x, y, z, id, tag) -> clipboard.setTile( + MathMan.roundInt(x + clipboard.getMinimumPoint().x()), + MathMan.roundInt(y + clipboard.getMinimumPoint().y()), + MathMan.roundInt(z + clipboard.getMinimumPoint().z()), + new CompoundTag(tag) + ); + } + + /** + * @return {@code true} if {@code Width}, {@code Length} and {@code Height} are already read from the stream + */ + private boolean areDimensionsAvailable() { + return this.dimensions.x() != 0 && this.dimensions.y() != 0 && this.dimensions.z() != 0; + } + + /** + * Closes this reader instance and all underlying resources. + * + * @throws IOException on I/O error. + */ + @Override + public void close() throws IOException { + parentStream.close(); // closes all underlying resources implicitly + } + + /** + * Resets the main stream to the previously marked position ({@code 0}), if supported (see {@link #supportsReset()}). + * If the stream is reset, the sub streams (for DataInput and NBT) are re-created to respect the new position. + * + * @throws IOException on I/O error. + */ + private void reset() throws IOException { + if (this.supportsReset()) { + this.parentStream.reset(); + this.parentStream.mark(Integer.MAX_VALUE); + this.setSubStreams(); + } + } + + /** + * @return {@code true} if the stream used while instantiating the reader supports resets (without memory overhead). + */ + private boolean supportsReset() { + return this.remainingTags != null; + } + + /** + * Overwrites the DataInput- and NBT-InputStreams (e.g. when the marker of the backed stream updated). + * + * @throws IOException on I/O error. + */ + private void setSubStreams() throws IOException { + final FastBufferedInputStream buffer = new FastBufferedInputStream(new GZIPInputStream(this.parentStream)); + this.dataInputStream = new DataInputStream(buffer); + this.nbtInputStream = new NBTInputStream(buffer); + } + + /** + * Creates a new cache writer for non-palette data, if none exists yet. + * Returns either the already created or new one. + * + * @return the output stream for non-palette cache data. + */ + private OutputStream getDataCacheWriter() { + if (this.dataCacheWriter == null) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(512); + this.dataCacheWriter = new FastBufferedOutputStream(new LZ4BlockOutputStream(byteArrayOutputStream)) { + @Override + public void close() throws IOException { + this.write(CACHE_IDENTIFIER_END); + super.close(); + FastSchematicReaderV3.this.dataCache = byteArrayOutputStream.toByteArray(); + } + }; + } + return this.dataCacheWriter; + } + + /** + * Creates a new cache writer for palette data, if none exists yet. + * Returns either the already created or new one. + * + * @return the output stream for palette cache data. + */ + private OutputStream getPaletteCacheWriter() { + if (this.paletteCacheWriter == null) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(256); + this.paletteCacheWriter = new FastBufferedOutputStream(new LZ4BlockOutputStream(byteArrayOutputStream)) { + @Override + public void close() throws IOException { + this.write(CACHE_IDENTIFIER_END); + super.close(); + FastSchematicReaderV3.this.paletteCache = byteArrayOutputStream.toByteArray(); + } + }; + } + return this.paletteCacheWriter; + } + + private boolean needsVarIntReading(int byteArrayLength) { + return byteArrayLength > this.dimensions.x() * this.dimensions.y() * this.dimensions.z(); + } + + /** + * Skips the schematic header including the root compound (empty name) and the root's child compound ("Schematic") + * + * @param dataInputStream The stream containing the schematic data to skip + * @throws IOException on I/O error + */ + private static void skipHeader(DataInputStream dataInputStream) 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" + } + + @ApiStatus.Internal + @FunctionalInterface + private interface PositionConsumer { + + /** + * Called with block location coordinates. + * + * @param x the x coordinate. + * @param y the y coordinate. + * @param z the z coordinate. + */ + void accept(int x, int y, int z); + + } + + @ApiStatus.Internal + @FunctionalInterface + private interface EntityTransformer { + + /** + * Called for each entity from the Schematics {@code Entities} compound list. + * + * @param x the relative x coordinate of the entity. + * @param y the relative y coordinate of the entity. + * @param z the relative z coordinate of the entity. + * @param id the entity id as a resource location (e.g. {@code minecraft:sheep}). + * @param tag the - already fixed, if required - nbt data of the entity. + */ + void transform(double x, double y, double z, String id, LinCompoundTag tag); + + } + + @ApiStatus.Internal + @FunctionalInterface + private interface PaletteInitializer { + + /** + * Called for each palette entry (the mapping part, not data). + * + * @param index the index of the entry, as used in the Data byte array. + * @param value the value for this entry (either biome type as resource location or the block state as a string). + */ + void initialize(char index, String value); + + } + + @ApiStatus.Internal + @FunctionalInterface + private interface PaletteDataApplier { + + /** + * Called for each palette data entry (not the mapping part, but the var-int byte array). + * + * @param index The index of this data entry (due to var-int behaviour not necessarily the index in the data byte array). + * @param ordinal The ordinal of this entry as defined in the palette mapping. + */ + void apply(int index, char ordinal); + + } + + @ApiStatus.Internal + @FunctionalInterface + private interface AdditionalTagConsumer { + + /** + * Called for each unknown nbt tag. + * + * @param type The type of the tag (as defined by the constants in {@link NBTConstants}). + * @param name The name of the tag. + */ + void accept(byte type, String name); + + } + +} diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriter.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV2.java similarity index 98% rename from worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriter.java rename to worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV2.java index ac86fb249..3bd302876 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriter.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV2.java @@ -48,9 +48,9 @@ import static com.google.common.base.Preconditions.checkNotNull; /** * Writes schematic files using the Sponge schematic format. */ -public class FastSchematicWriter implements ClipboardWriter { +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; @@ -61,7 +61,7 @@ public class FastSchematicWriter implements ClipboardWriter { * * @param outputStream the output stream to write to */ - public FastSchematicWriter(NBTOutputStream outputStream) { + public FastSchematicWriterV2(NBTOutputStream outputStream) { checkNotNull(outputStream); this.outputStream = outputStream; } @@ -103,11 +103,11 @@ public class FastSchematicWriter implements ClipboardWriter { final DataOutput rawStream = outputStream.getOutputStream(); outputStream.writeLazyCompoundTag("Schematic", out -> { + out.writeNamedTag("Version", CURRENT_VERSION); out.writeNamedTag( "DataVersion", WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion() ); - out.writeNamedTag("Version", CURRENT_VERSION); out.writeNamedTag("Width", (short) width); out.writeNamedTag("Height", (short) height); out.writeNamedTag("Length", (short) length); diff --git a/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV3.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV3.java new file mode 100644 index 000000000..e00839eb0 --- /dev/null +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/extent/clipboard/io/FastSchematicWriterV3.java @@ -0,0 +1,295 @@ +package com.fastasyncworldedit.core.extent.clipboard.io; + +import com.fastasyncworldedit.core.function.visitor.Order; +import com.fastasyncworldedit.core.util.IOUtil; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.NBTConstants; +import com.sk89q.jnbt.NBTOutputStream; +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.block.BaseBlock; +import com.sk89q.worldedit.world.block.BlockStateHolder; +import com.sk89q.worldedit.world.block.BlockTypesCache; +import net.jpountz.lz4.LZ4BlockInputStream; +import net.jpountz.lz4.LZ4BlockOutputStream; +import org.enginehub.linbus.tree.LinCompoundTag; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataOutput; +import java.io.IOException; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +/** + * Faster, stream-based implementation of {@link com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV3Writer} for + * writing schematics conforming the sponge schematic v3 format. + * + * @since TODO + */ +@SuppressWarnings("removal") // Yes, JNBT is deprecated - we know +public class FastSchematicWriterV3 implements ClipboardWriter { + + public static final int CURRENT_VERSION = 3; + + private static final int MAX_SIZE = Short.MAX_VALUE - Short.MIN_VALUE; + private final NBTOutputStream outputStream; + + + public FastSchematicWriterV3(final NBTOutputStream outputStream) { + this.outputStream = Objects.requireNonNull(outputStream, "outputStream"); + } + + @Override + public void write(final Clipboard clipboard) throws IOException { + clipboard.flush(); + + // Validate dimensions before starting to write into stream + final Region region = clipboard.getRegion(); + if (region.getWidth() > MAX_SIZE) { + throw new IllegalArgumentException("Region width too large for schematic: " + region.getWidth()); + } + if (region.getHeight() > MAX_SIZE) { + throw new IllegalArgumentException("Region height too large for schematic: " + region.getHeight()); + } + if (region.getLength() > MAX_SIZE) { + throw new IllegalArgumentException("Region length too large for schematic: " + region.getLength()); + } + + this.outputStream.writeLazyCompoundTag( + "", root -> root.writeLazyCompoundTag("Schematic", out -> this.write2(out, clipboard)) + ); + } + + private void write2(NBTOutputStream schematic, Clipboard clipboard) throws IOException { + final Region region = clipboard.getRegion(); + final BlockVector3 origin = clipboard.getOrigin(); + final BlockVector3 min = clipboard.getMinimumPoint(); + final BlockVector3 offset = min.subtract(origin); + + schematic.writeNamedTag("Version", CURRENT_VERSION); + schematic.writeNamedTag( + "DataVersion", + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion() + ); + schematic.writeLazyCompoundTag("Metadata", out -> this.writeMetadata(out, clipboard)); + + schematic.writeNamedTag("Width", (short) region.getWidth()); + schematic.writeNamedTag("Height", (short) region.getHeight()); + schematic.writeNamedTag("Length", (short) region.getLength()); + + schematic.writeNamedTag("Offset", new int[]{ + offset.x(), offset.y(), offset.z() + }); + + schematic.writeLazyCompoundTag("Blocks", out -> this.writeBlocks(out, clipboard)); + if (clipboard.hasBiomes()) { + schematic.writeLazyCompoundTag("Biomes", out -> this.writeBiomes(out, clipboard)); + } + // Some clipboards have quite heavy operations on the getEntities method - only call once + List entities; + if (!(entities = clipboard.getEntities()).isEmpty()) { + schematic.writeNamedTagName("Entities", NBTConstants.TYPE_LIST); + schematic.write(NBTConstants.TYPE_COMPOUND); + schematic.writeInt(entities.size()); + for (final Entity entity : entities) { + this.writeEntity(schematic, clipboard, entity); + } + } + } + + private void writeBlocks(NBTOutputStream blocks, Clipboard clipboard) throws IOException { + final int[] tiles = new int[]{0}; + final ByteArrayOutputStream tileBytes = new ByteArrayOutputStream(); + try (LZ4BlockOutputStream lz4Stream = new LZ4BlockOutputStream(tileBytes); + NBTOutputStream tileOut = new NBTOutputStream(lz4Stream)) { + this.writePalette( + blocks, + BlockTypesCache.states.length, + pos -> { + BaseBlock block = pos.getFullBlock(clipboard); + LinCompoundTag tag; + if ((tag = block.getNbt()) != null) { + tiles[0]++; + try { + tileOut.writeNamedTag("Id", block.getNbtId()); + tileOut.writeNamedTag("Pos", new int[]{ + pos.x() - clipboard.getMinimumPoint().x(), + pos.y() - clipboard.getMinimumPoint().y(), + pos.z() - clipboard.getMinimumPoint().z() + }); + //noinspection deprecation + tileOut.writeNamedTag("Data", new CompoundTag(tag)); + tileOut.write(NBTConstants.TYPE_END); + } catch (IOException e) { + throw new RuntimeException("Failed to write tile data", e); + } + } + return block; + }, + block -> { + char ordinal = block.getOrdinalChar(); + if (ordinal == BlockTypesCache.ReservedIDs.__RESERVED__) { + ordinal = BlockTypesCache.ReservedIDs.AIR; + } + return ordinal; + }, + BlockStateHolder::getAsString, + clipboard + ); + lz4Stream.finish(); + } finally { + // Write Tiles + if (tiles[0] > 0) { + blocks.writeNamedTagName("BlockEntities", NBTConstants.TYPE_LIST); + blocks.write(NBTConstants.TYPE_COMPOUND); + blocks.writeInt(tiles[0]); + // Decompress cached data again + try (LZ4BlockInputStream reader = new LZ4BlockInputStream(new ByteArrayInputStream(tileBytes.toByteArray()))) { + IOUtil.copy(reader, blocks.getOutputStream()); + } + } + } + } + + private void writeBiomes(NBTOutputStream biomes, Clipboard clipboard) throws IOException { + this.writePalette( + biomes, BiomeType.REGISTRY.size(), + pos -> pos.getBiome(clipboard), + biome -> (char) biome.getInternalId(), + BiomeType::id, + clipboard + ); + } + + private void writeEntity(NBTOutputStream out, Clipboard clipboard, Entity entity) throws IOException { + final BaseEntity state = entity.getState(); + if (state == null) { + throw new IOException("Entity has no state"); + } + out.writeNamedTag("Id", state.getType().id()); + + out.writeNamedTagName("Pos", NBTConstants.TYPE_LIST); + out.write(NBTConstants.TYPE_DOUBLE); + out.writeInt(3); + out.writeDouble(entity.getLocation().x() - clipboard.getMinimumPoint().x()); + out.writeDouble(entity.getLocation().y() - clipboard.getMinimumPoint().y()); + out.writeDouble(entity.getLocation().z() - clipboard.getMinimumPoint().z()); + + out.writeLazyCompoundTag("Data", data -> { + //noinspection deprecation + CompoundTag nbt = state.getNbtData(); + if (nbt != null) { + nbt.getValue().forEach((s, tag) -> { + if (s.equals("id") || s.equals("Rotation")) { + return; + } + try { + data.writeNamedTag(s, tag); + } catch (IOException e) { + throw new RuntimeException("failed to write entity data", e); + } + }); + } + + // Write rotation list + data.writeNamedTagName("Rotation", NBTConstants.TYPE_LIST); + data.write(NBTConstants.TYPE_FLOAT); + data.writeInt(2); + data.writeFloat(entity.getLocation().getYaw()); + data.writeFloat(entity.getLocation().getPitch()); + }); + + out.write(NBTConstants.TYPE_END); // End the compound + } + + private void writePalette( + NBTOutputStream out, int capacity, + Function objectResolver, + Function ordinalResolver, + Function paletteEntryResolver, + Clipboard clipboard + ) throws IOException { + int dataBytesUsed = 0; + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + try (LZ4BlockOutputStream dataOut = new LZ4BlockOutputStream(bytes)) { + int index = 0; + char[] palette = new char[capacity]; + Arrays.fill(palette, Character.MAX_VALUE); + final Iterator iterator = clipboard.iterator(Order.YZX); + // Start Palette tag + out.writeNamedTagName("Palette", NBTConstants.TYPE_COMPOUND); + while (iterator.hasNext()) { + BlockVector3 pos = iterator.next(); + T obj = objectResolver.apply(pos); + char ordinal = ordinalResolver.apply(obj); + char value = palette[ordinal]; + if (value == Character.MAX_VALUE) { + palette[ordinal] = value = (char) index++; + if (index >= palette.length) { + throw new IOException("insufficient palette capacity: " + palette.length + ", index: " + index); + } + out.writeNamedTag(paletteEntryResolver.apply(obj), value); + } + if ((value & -128) != 0) { + dataBytesUsed++; + dataOut.write(value & 127 | 128); + value >>>= 7; + } + dataOut.write(value); + dataBytesUsed++; + } + // End Palette tag + out.write(NBTConstants.TYPE_END); + dataOut.finish(); + } finally { + // Write Data tag + if (dataBytesUsed > 0) { + try (LZ4BlockInputStream reader = new LZ4BlockInputStream(new ByteArrayInputStream(bytes.toByteArray()))) { + out.writeNamedTagName("Data", NBTConstants.TYPE_BYTE_ARRAY); + out.writeInt(dataBytesUsed); + IOUtil.copy(reader, (DataOutput) out); + } + } + } + } + + private void writeMetadata(NBTOutputStream metadata, Clipboard clipboard) throws IOException { + metadata.writeNamedTag("Date", System.currentTimeMillis()); + metadata.writeLazyCompoundTag("WorldEdit", out -> { + out.writeNamedTag("Version", WorldEdit.getVersion()); + out.writeNamedTag( + "EditingPlatform", + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getId() + ); + out.writeNamedTag("Origin", new int[]{ + clipboard.getOrigin().x(), clipboard.getOrigin().y(), clipboard.getOrigin().z() + }); + out.writeLazyCompoundTag("Platforms", platforms -> { + for (final Platform platform : WorldEdit.getInstance().getPlatformManager().getPlatforms()) { + platforms.writeLazyCompoundTag(platform.getId(), p -> { + p.writeNamedTag("Name", platform.getPlatformName()); + p.writeNamedTag("Version", platform.getPlatformVersion()); + }); + } + }); + }); + } + + @Override + public void close() throws IOException { + this.outputStream.close(); + } + +} 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/fastasyncworldedit/core/jnbt/CompressedSchematicTag.java b/worldedit-core/src/main/java/com/fastasyncworldedit/core/jnbt/CompressedSchematicTag.java index e622b1501..3c569f913 100644 --- a/worldedit-core/src/main/java/com/fastasyncworldedit/core/jnbt/CompressedSchematicTag.java +++ b/worldedit-core/src/main/java/com/fastasyncworldedit/core/jnbt/CompressedSchematicTag.java @@ -1,6 +1,6 @@ package com.fastasyncworldedit.core.jnbt; -import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriter; +import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriterV2; import com.fastasyncworldedit.core.internal.io.FastByteArrayOutputStream; import com.fastasyncworldedit.core.internal.io.FastByteArraysInputStream; import com.sk89q.jnbt.NBTOutputStream; @@ -21,7 +21,7 @@ public class CompressedSchematicTag extends CompressedCompoundTag { FastByteArrayOutputStream blocksOut = new FastByteArrayOutputStream(); try (LZ4BlockOutputStream lz4out = new LZ4BlockOutputStream(blocksOut)) { NBTOutputStream nbtOut = new NBTOutputStream(lz4out); - new FastSchematicWriter(nbtOut).write(getSource()); + new FastSchematicWriterV2(nbtOut).write(getSource()); } catch (IOException e) { throw new RuntimeException(e); } diff --git a/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java b/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java index 806168327..080ab3bea 100644 --- a/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java +++ b/worldedit-core/src/main/java/com/sk89q/jnbt/NBTInputStream.java @@ -572,7 +572,7 @@ public final class NBTInputStream implements Closeable { * @return the tag * @throws IOException if an I/O error occurs. */ - private Tag readTagPayload(int type, int depth) throws IOException { + public Tag readTagPayload(int type, int depth) throws IOException { //FAWE - public switch (type) { case NBTConstants.TYPE_END: if (depth == 0) { 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 1fd9dda9a..8a57411d6 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 @@ -19,28 +19,38 @@ package com.sk89q.worldedit.extent.clipboard.io; -import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReader; -import com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriter; +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; import com.fastasyncworldedit.core.extent.clipboard.io.schematic.PNGWriter; import com.fastasyncworldedit.core.internal.io.ResettableFileInputStream; import com.google.common.collect.ImmutableSet; -import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.NBTConstants; import com.sk89q.jnbt.NBTInputStream; import com.sk89q.jnbt.NBTOutputStream; import com.sk89q.jnbt.NamedTag; -import com.sk89q.jnbt.Tag; +import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV1Reader; +import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV2Reader; +import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV2Writer; +import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV3Reader; +import com.sk89q.worldedit.extent.clipboard.io.sponge.SpongeSchematicV3Writer; +import it.unimi.dsi.fastutil.io.FastBufferedInputStream; import org.anarres.parallelgzip.ParallelGZIPOutputStream; +import org.enginehub.linbus.stream.LinBinaryIO; +import org.enginehub.linbus.tree.LinRootEntry; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; 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; @@ -48,23 +58,14 @@ 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("fast", "fawe", "sponge", "schem") { - @Override - public String getPrimaryFileExtension() { - return "schem"; - } - + FAST_V3("fast", "fawe", "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); + return new FastSchematicReaderV3(inputStream); } @Override @@ -77,13 +78,77 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { gzip = new ParallelGZIPOutputStream(outputStream); } NBTOutputStream nbtStream = new NBTOutputStream(new BufferedOutputStream(gzip)); - return new FastSchematicWriter(nbtStream); + return new FastSchematicWriterV3(nbtStream); } @Override - public boolean isFormat(File file) { - String name = file.getName().toLowerCase(Locale.ROOT); - return name.endsWith(".schem") || name.endsWith(".sponge"); + public boolean isFormat(final InputStream inputStream) { + try (final DataInputStream stream = new DataInputStream(new FastBufferedInputStream(new GZIPInputStream(inputStream))); + 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 "schem"; + } + }, + FAST_V2("fast.2", "fawe.2", "schem.2") { + @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 FastSchematicReaderV2(nbtStream); + } + + @Override + public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { + OutputStream gzip; + if (outputStream instanceof ParallelGZIPOutputStream || outputStream instanceof GZIPOutputStream) { + gzip = outputStream; + } else { + outputStream = new BufferedOutputStream(outputStream); + gzip = new ParallelGZIPOutputStream(outputStream); + } + NBTOutputStream nbtStream = new NBTOutputStream(new BufferedOutputStream(gzip)); + return new FastSchematicWriterV2(nbtStream); + } + + @Override + public boolean isFormat(InputStream inputStream) { + return detectOldSpongeSchematic(inputStream, FastSchematicWriterV2.CURRENT_VERSION); } }, @@ -113,9 +178,39 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { } @Override - public boolean isFormat(File file) { - String name = file.getName().toLowerCase(Locale.ROOT); - return name.endsWith(".schematic") || name.endsWith(".mcedit") || name.endsWith(".mce"); + public boolean isFormat(InputStream inputStream) { + LinRootEntry rootEntry; + try { + DataInputStream stream = new DataInputStream(new GZIPInputStream(inputStream)); + rootEntry = LinBinaryIO.readUsing(stream, LinRootEntry::readFrom); + } catch (Exception e) { + return false; + } + if (!rootEntry.name().equals("Schematic")) { + return false; + } + return rootEntry.value().value().containsKey("Materials"); + } + }, + SPONGE_V1_SCHEMATIC("sponge.1") { + @Override + public String getPrimaryFileExtension() { + return "schem"; + } + + @Override + public ClipboardReader getReader(InputStream inputStream) throws IOException { + return new SpongeSchematicV1Reader(LinBinaryIO.read(new DataInputStream(new GZIPInputStream(inputStream)))); + } + + @Override + public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { + throw new IOException("This format does not support saving"); + } + + @Override + public boolean isFormat(InputStream inputStream) { + return detectOldSpongeSchematic(inputStream, 1); } }, @@ -125,7 +220,8 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { * Avoid using with any large schematics/clipboards for reading/writing. */ @Deprecated - SPONGE_SCHEMATIC("slow", "safe") { + SPONGE_V2_SCHEMATIC("slow.2", "safe.2", "sponge.2") { // FAWE - edit aliases for fast + @Override public String getPrimaryFileExtension() { return "schem"; @@ -133,38 +229,43 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { @Override public ClipboardReader getReader(InputStream inputStream) throws IOException { - NBTInputStream nbtStream = new NBTInputStream(new GZIPInputStream(inputStream)); - return new SpongeSchematicReader(nbtStream); + return new SpongeSchematicV2Reader(LinBinaryIO.read(new DataInputStream(new GZIPInputStream(inputStream)))); } @Override public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { - NBTOutputStream nbtStream = new NBTOutputStream(new GZIPOutputStream(outputStream)); - return new SpongeSchematicWriter(nbtStream); + return new SpongeSchematicV2Writer(new DataOutputStream(new GZIPOutputStream(outputStream))); + } + + @Override + public boolean isFormat(InputStream inputStream) { + return detectOldSpongeSchematic(inputStream, 2); + } + }, + SPONGE_V3_SCHEMATIC("sponge.3", "slow", "safe") { // FAWE - edit aliases for fast + + @Override + public String getPrimaryFileExtension() { + return "schem"; + } + + @Override + public ClipboardReader getReader(InputStream inputStream) throws IOException { + return new SpongeSchematicV3Reader(LinBinaryIO.read(new DataInputStream(new GZIPInputStream(inputStream)))); + } + + @Override + public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { + return new SpongeSchematicV3Writer(new DataOutputStream(new GZIPOutputStream(outputStream))); } @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> schematic = schematicTag.getValue(); - if (!schematic.containsKey("Version")) { - return false; - } - } catch (Exception e) { - return false; - } - - return true; + //FAWE start - delegate to stream-based isFormat approach of fast impl + return FAST_V3.isFormat(file); + //FAWE end } }, - //FAWE start - recover schematics with bad entity data & register other clipboard formats BROKENENTITY("brokenentity", "legacyentity", "le", "be", "brokenentities", "legacyentities") { @Override @@ -179,7 +280,7 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { } BufferedInputStream buffered = new BufferedInputStream(inputStream); NBTInputStream nbtStream = new NBTInputStream(new BufferedInputStream(new GZIPInputStream(buffered))); - FastSchematicReader reader = new FastSchematicReader(nbtStream); + FastSchematicReaderV2 reader = new FastSchematicReaderV2(nbtStream); reader.setBrokenEntities(true); return reader; } @@ -194,7 +295,7 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { gzip = new ParallelGZIPOutputStream(outputStream); } NBTOutputStream nbtStream = new NBTOutputStream(new BufferedOutputStream(gzip)); - FastSchematicWriter writer = new FastSchematicWriter(nbtStream); + FastSchematicWriterV2 writer = new FastSchematicWriterV2(nbtStream); writer.setBrokenEntities(true); return writer; } @@ -232,9 +333,37 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { } @Override - public boolean isFormat(File file) { - String name = file.getName().toLowerCase(Locale.ROOT); - return name.endsWith(".nbt"); + public boolean isFormat(InputStream inputStream) { + try (final DataInputStream stream = new DataInputStream(new FastBufferedInputStream(new GZIPInputStream(inputStream))); + final NBTInputStream nbt = new NBTInputStream(stream)) { + if (stream.readByte() != NBTConstants.TYPE_COMPOUND) { + return false; + } + NamedTag namedTag = nbt.readNamedTag(); + if (!namedTag.getName().isEmpty()) { + return false; + } + + // 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_LIST && name.equals("size")) { + return true; + } + nbt.readTagPayloadLazy(type, 0); + } while (true); + } catch (IOException ignored) { + } + return false; + } + + @Override + public boolean isFormat(final File file) { + return file.getName().toLowerCase(Locale.ROOT).endsWith(".nbt") && super.isFormat(file); } }, @@ -265,6 +394,53 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { }; //FAWE end + private static boolean detectOldSpongeSchematic(InputStream inputStream, int version) { + //FAWE start - dont utilize linbus - WorldEdit approach is not really streamed + try (final DataInputStream stream = new DataInputStream(new FastBufferedInputStream(new GZIPInputStream(inputStream))); + 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() == version; + } + nbt.readTagPayloadLazy(type, 0); + } while (true); + } catch (IOException ignored) { + } + return false; + } + + /** + * For backwards compatibility, this points to the Sponge Schematic Specification (Version 2) + * format. This should not be used going forwards. + * + * @deprecated Use {@link #SPONGE_V2_SCHEMATIC} or {@link #SPONGE_V3_SCHEMATIC} + */ + @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/ClipboardFormat.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/ClipboardFormat.java index cb5cd1b7a..7b4b1bf3b 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 @@ -35,6 +35,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URL; +import java.nio.file.Files; import java.util.Set; import static com.google.common.base.Preconditions.checkNotNull; @@ -82,7 +83,29 @@ public interface ClipboardFormat { * @param file the file * @return true if the given file is of this format */ - boolean isFormat(File file); + default boolean isFormat(File file) { + try (InputStream stream = Files.newInputStream(file.toPath())) { + return isFormat(stream); + } catch (IOException e) { + return false; + } + } + + /** + * Return whether the given stream is of this format. + * + * @apiNote The caller is responsible for the following: + *

    + *
  • Closing the input stream
  • + *
+ * + * @param inputStream The stream + * @return true if the given stream is of this format + * @since TODO + */ + default boolean isFormat(InputStream inputStream) { + return false; + } /** * Get the file extension this format primarily uses. diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/NBTSchematicReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/NBTSchematicReader.java index a6b9c1400..017f07e9c 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/NBTSchematicReader.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/NBTSchematicReader.java @@ -20,6 +20,7 @@ package com.sk89q.worldedit.extent.clipboard.io; import com.sk89q.jnbt.Tag; +import org.enginehub.linbus.tree.LinCompoundTag; import javax.annotation.Nullable; import java.io.IOException; @@ -27,7 +28,10 @@ import java.util.Map; /** * Base class for NBT schematic readers. + * + * @deprecated These utility methods are provided by {@link LinCompoundTag} now. */ +@Deprecated public abstract class NBTSchematicReader implements ClipboardReader { protected static > T requireTag(Map> items, String key, Class expected) throws IOException { diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicNbtUtil.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicNbtUtil.java new file mode 100644 index 000000000..7da0aa164 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicNbtUtil.java @@ -0,0 +1,61 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io; + +import com.sk89q.jnbt.Tag; + +import java.io.IOException; +import java.util.Map; +import javax.annotation.Nullable; + +// note, when clearing deprecations these methods don't need to remain -- they're introduced in 7.3.0 +public class SchematicNbtUtil { + public static T requireTag(Map items, String key, Class expected) throws IOException { + if (!items.containsKey(key)) { + throw new IOException("Schematic file is missing a \"" + key + "\" tag of type " + + expected.getName()); + } + + Tag tag = items.get(key); + if (!expected.isInstance(tag)) { + throw new IOException(key + " tag is not of tag type " + expected.getName() + ", got " + + tag.getClass().getName() + " instead"); + } + + return expected.cast(tag); + } + + @Nullable + public static T getTag(Map items, String key, Class expected) { + if (!items.containsKey(key)) { + return null; + } + + Tag test = items.get(key); + if (!expected.isInstance(test)) { + return null; + } + + return expected.cast(test); + } + + private SchematicNbtUtil() { + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java deleted file mode 100644 index ec82be228..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java +++ /dev/null @@ -1,453 +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 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sk89q.worldedit.extent.clipboard.io; - -import com.fastasyncworldedit.core.configuration.Caption; -import com.google.common.collect.Maps; -import com.sk89q.jnbt.LinBusConverter; -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.StringTag; -import com.sk89q.jnbt.Tag; -import com.sk89q.worldedit.WorldEdit; -import com.sk89q.worldedit.WorldEditException; -import com.sk89q.worldedit.entity.BaseEntity; -import com.sk89q.worldedit.extension.input.InputParseException; -import com.sk89q.worldedit.extension.input.ParserContext; -import com.sk89q.worldedit.extension.platform.Capability; -import com.sk89q.worldedit.extension.platform.Platform; -import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; -import com.sk89q.worldedit.extent.clipboard.Clipboard; -import com.sk89q.worldedit.internal.Constants; -import com.sk89q.worldedit.internal.util.LogManagerCompat; -import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.regions.CuboidRegion; -import com.sk89q.worldedit.regions.Region; -import com.sk89q.worldedit.util.Location; -import com.sk89q.worldedit.util.formatting.text.TextComponent; -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.entity.EntityType; -import com.sk89q.worldedit.world.entity.EntityTypes; -import com.sk89q.worldedit.world.storage.NBTConversions; -import org.apache.logging.log4j.Logger; - -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.OptionalInt; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Reads schematic files using the Sponge Schematic Specification. - * - * @deprecated Slow, resource intensive, but sometimes safer than using the recommended - * {@link com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicReader}. - * Avoid reading large schematics with this reader. - */ -@Deprecated -public class SpongeSchematicReader extends NBTSchematicReader { - - private static final Logger LOGGER = LogManagerCompat.getLogger(); - private final NBTInputStream inputStream; - private DataFixer fixer = null; - private int schematicVersion = -1; - private int dataVersion = -1; - - /** - * 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 { - CompoundTag schematicTag = getBaseTag(); - Map> schematic = schematicTag.getValue(); - - final Platform platform = WorldEdit.getInstance().getPlatformManager() - .queryCapability(Capability.WORLD_EDITING); - int liveDataVersion = platform.getDataVersion(); - - if (schematicVersion == 1) { - dataVersion = Constants.DATA_VERSION_MC_1_13_2; // this is a relatively safe assumption unless someone imports a schematic from 1.12, e.g. sponge 7.1- - fixer = platform.getDataFixer(); - return readVersion1(schematicTag); - } else if (schematicVersion == 2) { - dataVersion = requireTag(schematic, "DataVersion", IntTag.class).getValue(); - if (dataVersion < 0) { - LOGGER.warn( - "Schematic has an unknown data version ({}). Data may be incompatible.", - dataVersion - ); - // Do not DFU unknown data - dataVersion = liveDataVersion; - } - if (dataVersion > liveDataVersion) { - LOGGER.warn("Schematic was made in a newer Minecraft version ({} > {}). Data may be incompatible.", - dataVersion, liveDataVersion - ); - } else if (dataVersion < liveDataVersion) { - fixer = platform.getDataFixer(); - if (fixer != null) { - LOGGER.info("Schematic was made in an older Minecraft version ({} < {}), will attempt DFU.", - dataVersion, liveDataVersion - ); - } else { - LOGGER.info( - "Schematic was made in an older Minecraft version ({} < {}), but DFU is not available. Data may be incompatible.", - dataVersion, - liveDataVersion - ); - } - } - - BlockArrayClipboard clip = readVersion1(schematicTag); - return readVersion2(clip, schematicTag); - } - throw new SchematicLoadException(Caption.of("worldedit.schematic.load.unsupported-version", - TextComponent.of(schematicVersion))); - } - - @Override - public OptionalInt getDataVersion() { - try { - CompoundTag schematicTag = getBaseTag(); - Map> schematic = schematicTag.getValue(); - if (schematicVersion == 1) { - return OptionalInt.of(Constants.DATA_VERSION_MC_1_13_2); - } else if (schematicVersion == 2) { - int dataVersion = requireTag(schematic, "DataVersion", IntTag.class).getValue(); - if (dataVersion < 0) { - return OptionalInt.empty(); - } - return OptionalInt.of(dataVersion); - } - return OptionalInt.empty(); - } catch (IOException e) { - return OptionalInt.empty(); - } - } - - private CompoundTag getBaseTag() throws IOException { - NamedTag rootTag = inputStream.readNamedTag(); - CompoundTag schematicTag = (CompoundTag) rootTag.getTag(); - - // Check - Map> schematic = schematicTag.getValue(); - - // Be lenient about the specific nesting level of the Schematic tag - // Also allows checking the version from newer versions of the specification - if (schematic.size() == 1 && schematic.containsKey("Schematic")) { - schematicTag = requireTag(schematic, "Schematic", CompoundTag.class); - schematic = schematicTag.getValue(); - } - - schematicVersion = requireTag(schematic, "Version", IntTag.class).getValue(); - return schematicTag; - } - - private BlockArrayClipboard readVersion1(CompoundTag schematicTag) throws IOException { - BlockVector3 origin; - Region region; - Map> schematic = schematicTag.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(); - - IntArrayTag offsetTag = getTag(schematic, "Offset", IntArrayTag.class); - int[] offsetParts; - if (offsetTag != null) { - offsetParts = offsetTag.getValue(); - if (offsetParts.length != 3) { - throw new IOException("Invalid offset specified in schematic."); - } - } else { - offsetParts = new int[]{0, 0, 0}; - } - - BlockVector3 min = BlockVector3.at(offsetParts[0], offsetParts[1], offsetParts[2]); - - CompoundTag metadataTag = getTag(schematic, "Metadata", CompoundTag.class); - if (metadataTag != null && metadataTag.containsKey("WEOffsetX")) { - // We appear to have WorldEdit Metadata - Map> metadata = metadataTag.getValue(); - int offsetX = requireTag(metadata, "WEOffsetX", IntTag.class).getValue(); - int offsetY = requireTag(metadata, "WEOffsetY", IntTag.class).getValue(); - int offsetZ = requireTag(metadata, "WEOffsetZ", IntTag.class).getValue(); - BlockVector3 offset = BlockVector3.at(offsetX, offsetY, offsetZ); - origin = min.subtract(offset); - region = new CuboidRegion(min, min.add(width, height, length).subtract(BlockVector3.ONE)); - } else { - origin = min; - region = new CuboidRegion(origin, origin.add(width, height, length).subtract(BlockVector3.ONE)); - } - - IntTag paletteMaxTag = getTag(schematic, "PaletteMax", IntTag.class); - Map> paletteObject = requireTag(schematic, "Palette", CompoundTag.class).getValue(); - if (paletteMaxTag != null && paletteObject.size() != paletteMaxTag.getValue()) { - throw new IOException("Block palette size does not match expected size."); - } - - Map palette = new HashMap<>(); - - ParserContext parserContext = new ParserContext(); - parserContext.setRestricted(false); - parserContext.setTryLegacy(false); - parserContext.setPreferringWildcard(false); - - for (String palettePart : paletteObject.keySet()) { - int id = requireTag(paletteObject, palettePart, IntTag.class).getValue(); - if (fixer != null) { - palettePart = fixer.fixUp(DataFixer.FixTypes.BLOCK_STATE, palettePart, dataVersion); - } - BlockState state; - try { - state = WorldEdit.getInstance().getBlockFactory().parseFromInput(palettePart, parserContext).toImmutableState(); - } catch (InputParseException e) { - LOGGER.warn("Invalid BlockState in palette: " + palettePart + ". Block will be replaced with air."); - state = BlockTypes.AIR.getDefaultState(); - } - palette.put(id, state); - } - - byte[] blocks = requireTag(schematic, "BlockData", ByteArrayTag.class).getValue(); - - Map>> tileEntitiesMap = new HashMap<>(); - ListTag tileEntities = getTag(schematic, "BlockEntities", ListTag.class); - if (tileEntities == null) { - tileEntities = getTag(schematic, "TileEntities", ListTag.class); - } - if (tileEntities != null) { - for (Object tileTag : tileEntities.getValue()) { - Map> tileEntity = ((CompoundTag) tileTag).getValue(); - int[] pos = requireTag(tileEntity, "Pos", IntArrayTag.class).getValue(); - final BlockVector3 pt = BlockVector3.at(pos[0], pos[1], pos[2]); - Map> values = Maps.newHashMap(tileEntity); - values.put("x", new IntTag(pt.x())); - values.put("y", new IntTag(pt.y())); - values.put("z", new IntTag(pt.z())); - //FAWE start - support old, corrupt schematics - Tag id = values.get("Id"); - if (id == null) { - id = values.get("id"); - } - if (id == null) { - continue; - } - values.put("id", id); - //FAWE end - values.remove("Id"); - values.remove("Pos"); - if (fixer != null) { - //FAWE start - LinTag - tileEntity = ((CompoundTag) LinBusConverter.fromLinBus(fixer.fixUp( - DataFixer.FixTypes.BLOCK_ENTITY, - new CompoundTag(values).toLinTag(), - dataVersion - ))).getValue(); - //FAWE end - } else { - tileEntity = values; - } - tileEntitiesMap.put(pt, tileEntity); - } - } - - BlockArrayClipboard clipboard = new BlockArrayClipboard(region); - clipboard.setOrigin(origin); - - int index = 0; - int i = 0; - int value; - int varintLength; - while (i < blocks.length) { - value = 0; - varintLength = 0; - - while (true) { - value |= (blocks[i] & 127) << (varintLength++ * 7); - if (varintLength > 5) { - throw new IOException("VarInt too big (probably corrupted data)"); - } - if ((blocks[i] & 128) != 128) { - i++; - break; - } - i++; - } - // index = (y * length * width) + (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); - BlockVector3 pt = BlockVector3.at(x, y, z); - try { - if (tileEntitiesMap.containsKey(pt)) { - clipboard.setBlock( - clipboard.getMinimumPoint().add(pt), - state.toBaseBlock(new CompoundTag(tileEntitiesMap.get(pt))) - ); - } else { - clipboard.setBlock(clipboard.getMinimumPoint().add(pt), state); - } - } catch (WorldEditException e) { - throw new IOException("Failed to load a block in the schematic"); - } - - index++; - } - - return clipboard; - } - - private Clipboard readVersion2(BlockArrayClipboard version1, CompoundTag schematicTag) throws IOException { - Map> schematic = schematicTag.getValue(); - if (schematic.containsKey("BiomeData")) { - readBiomes(version1, schematic); - } - if (schematic.containsKey("Entities")) { - readEntities(version1, schematic); - } - return version1; - } - - private void readBiomes(BlockArrayClipboard clipboard, Map> schematic) throws IOException { - ByteArrayTag dataTag = requireTag(schematic, "BiomeData", ByteArrayTag.class); - IntTag maxTag = requireTag(schematic, "BiomePaletteMax", IntTag.class); - CompoundTag paletteTag = requireTag(schematic, "BiomePalette", CompoundTag.class); - - Map palette = new HashMap<>(); - if (maxTag.getValue() != paletteTag.getValue().size()) { - throw new IOException("Biome palette size does not match expected size."); - } - - for (Entry> palettePart : paletteTag.getValue().entrySet()) { - String key = palettePart.getKey(); - if (fixer != null) { - key = fixer.fixUp(DataFixer.FixTypes.BIOME, key, dataVersion); - } - BiomeType biome = BiomeTypes.get(key); - if (biome == null) { - LOGGER.warn("Unknown biome type :" + key - + " in palette. Are you missing a mod or using a schematic made in a newer version of Minecraft?"); - } - Tag idTag = palettePart.getValue(); - if (!(idTag instanceof IntTag)) { - throw new IOException("Biome mapped to non-Int tag."); - } - palette.put(((IntTag) idTag).getValue(), biome); - } - - int width = clipboard.getDimensions().x(); - - byte[] biomes = dataTag.getValue(); - int biomeIndex = 0; - int biomeJ = 0; - int bVal; - int varIntLength; - BlockVector3 min = clipboard.getMinimumPoint(); - while (biomeJ < biomes.length) { - bVal = 0; - varIntLength = 0; - - while (true) { - bVal |= (biomes[biomeJ] & 127) << (varIntLength++ * 7); - if (varIntLength > 5) { - throw new IOException("VarInt too big (probably corrupted data)"); - } - if (((biomes[biomeJ] & 128) != 128)) { - biomeJ++; - break; - } - biomeJ++; - } - int z = biomeIndex / width; - int x = biomeIndex % width; - BiomeType type = palette.get(bVal); - for (int y = 0; y < clipboard.getRegion().getHeight(); y++) { - clipboard.setBiome(min.add(x, y, z), type); - } - biomeIndex++; - } - } - - private void readEntities(BlockArrayClipboard clipboard, Map> schematic) throws IOException { - List entList = requireTag(schematic, "Entities", ListTag.class).getValue(); - if (entList.isEmpty()) { - return; - } - for (Tag et : entList) { - if (!(et instanceof CompoundTag)) { - continue; - } - CompoundTag entityTag = (CompoundTag) et; - Map> tags = entityTag.getValue(); - String id = requireTag(tags, "Id", StringTag.class).getValue(); - entityTag = entityTag.createBuilder().putString("id", id).remove("Id").build(); - - if (fixer != null) { - //FAWE start - LinTag - entityTag = (CompoundTag) LinBusConverter.fromLinBus(fixer.fixUp( - DataFixer.FixTypes.ENTITY, - entityTag.toLinTag(), - dataVersion - )); - //FAWE end - } - - EntityType entityType = EntityTypes.get(id); - if (entityType != null) { - Location location = NBTConversions.toLocation( - clipboard, - requireTag(tags, "Pos", ListTag.class), - requireTag(tags, "Rotation", ListTag.class) - ); - BaseEntity state = new BaseEntity(entityType, entityTag); - clipboard.createEntity(location, state); - } else { - LOGGER.warn("Unknown entity when pasting schematic: " + id); - } - } - } - - @Override - public void close() throws IOException { - inputStream.close(); - } - -} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/share/BuiltInClipboardShareDestinations.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/share/BuiltInClipboardShareDestinations.java index 981debe4b..551ac5dd7 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/share/BuiltInClipboardShareDestinations.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/share/BuiltInClipboardShareDestinations.java @@ -61,7 +61,7 @@ public enum BuiltInClipboardShareDestinations implements ClipboardShareDestinati PasteMetadata pasteMetadata = new PasteMetadata(); pasteMetadata.author = metadata.author(); - pasteMetadata.extension = "schem"; + pasteMetadata.extension = metadata.format().getPrimaryFileExtension(); pasteMetadata.name = metadata.name(); EngineHubPaste pasteService = new EngineHubPaste(); @@ -75,7 +75,7 @@ public enum BuiltInClipboardShareDestinations implements ClipboardShareDestinati @Override public ClipboardFormat getDefaultFormat() { - return BuiltInClipboardFormat.SPONGE_SCHEMATIC; + return BuiltInClipboardFormat.SPONGE_V2_SCHEMATIC; } @Override @@ -119,13 +119,14 @@ public enum BuiltInClipboardShareDestinations implements ClipboardShareDestinati @Override public ClipboardFormat getDefaultFormat() { - return BuiltInClipboardFormat.FAST; + return BuiltInClipboardFormat.FAST_V3; } @Override public boolean supportsFormat(final ClipboardFormat format) { - return format == BuiltInClipboardFormat.SPONGE_SCHEMATIC || - format == BuiltInClipboardFormat.FAST || + return format == BuiltInClipboardFormat.SPONGE_V2_SCHEMATIC || + format == BuiltInClipboardFormat.FAST_V3 || + format == BuiltInClipboardFormat.FAST_V2 || format == BuiltInClipboardFormat.MCEDIT_SCHEMATIC; } }; 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 new file mode 100644 index 000000000..5c4e90264 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/ReaderUtil.java @@ -0,0 +1,283 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io.sponge; + + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.WorldEditException; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.extension.input.InputParseException; +import com.sk89q.worldedit.extension.input.ParserContext; +import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.internal.util.LogManagerCompat; +import com.sk89q.worldedit.internal.util.VarIntIterator; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.util.concurrency.LazyReference; +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.entity.EntityType; +import com.sk89q.worldedit.world.entity.EntityTypes; +import com.sk89q.worldedit.world.storage.NBTConversions; +import it.unimi.dsi.fastutil.ints.Int2ObjectLinkedOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import org.apache.logging.log4j.Logger; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinIntArrayTag; +import org.enginehub.linbus.tree.LinIntTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinTagType; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkState; + +/** + * Common code shared between schematic readers. + */ +public class ReaderUtil { //FAWE - make public + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + static void checkSchematicVersion(int version, LinCompoundTag schematicTag) throws IOException { + int schematicVersion = getSchematicVersion(schematicTag); + + checkState( + version == schematicVersion, + "Schematic is not version %s, but %s", version, schematicVersion + ); + } + + public static int getSchematicVersion(LinCompoundTag schematicTag) throws IOException { + return schematicTag.getTag("Version", LinTagType.intTag()).valueAsInt(); + } + + static VersionedDataFixer getVersionedDataFixer( + LinCompoundTag schematic, Platform platform, + int liveDataVersion + ) { + //FAWE start - call fawe method without NBT component requirement + return getVersionedDataFixer(schematic.getTag("DataVersion", LinTagType.intTag()).valueAsInt(), platform, + liveDataVersion + ); + //FAWE end + } + + //FAWE start - make getVersionedDataFixer without schematic compound + public + public static VersionedDataFixer getVersionedDataFixer(int schematicDataVersion, Platform platform, int liveDataVersion) { + DataFixer fixer = null; + if (schematicDataVersion < 0) { + LOGGER.warn( + "Schematic has an unknown data version ({}). Data may be incompatible.", + schematicDataVersion + ); + } else if (schematicDataVersion > liveDataVersion) { + LOGGER.warn( + "Schematic was made in a newer Minecraft version ({} > {})." + + " Data may be incompatible.", + schematicDataVersion, liveDataVersion + ); + } else if (schematicDataVersion < liveDataVersion) { + fixer = platform.getDataFixer(); + if (fixer != null) { + LOGGER.debug( + "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.", + schematicDataVersion, liveDataVersion + ); + } + } + return new VersionedDataFixer(schematicDataVersion, fixer); + } + //FAWE end + + static Map decodePalette( + LinCompoundTag paletteObject, VersionedDataFixer fixer + ) throws IOException { + Map palette = new HashMap<>(); + + ParserContext parserContext = new ParserContext(); + parserContext.setRestricted(false); + parserContext.setTryLegacy(false); + parserContext.setPreferringWildcard(false); + + for (var palettePart : paletteObject.value().entrySet()) { + if (!(palettePart.getValue() instanceof LinIntTag idTag)) { + throw new IOException("Invalid palette entry: " + palettePart); + } + int id = idTag.valueAsInt(); + String paletteName = fixer.fixUp(DataFixer.FixTypes.BLOCK_STATE, palettePart.getKey()); + BlockState state; + try { + state = WorldEdit.getInstance().getBlockFactory().parseFromInput(paletteName, parserContext).toImmutableState(); + } catch (InputParseException e) { + LOGGER.warn("Invalid BlockState in palette: " + palettePart + ". Block will be replaced with air."); + state = BlockTypes.AIR.getDefaultState(); + } + palette.put(id, state); + } + return palette; + } + + static void initializeClipboardFromBlocks( + Clipboard clipboard, Map palette, byte[] blocks, LinListTag tileEntities, + VersionedDataFixer fixer, boolean dataIsNested + ) throws IOException { + Map tileEntitiesMap = new HashMap<>(); + if (tileEntities != null) { + for (LinCompoundTag tileEntity : tileEntities.value()) { + final BlockVector3 pt = clipboard.getMinimumPoint().add( + decodeBlockVector3(tileEntity.getTag("Pos", LinTagType.intArrayTag())) + ); + LinCompoundTag.Builder values = extractData(dataIsNested, tileEntity); + values.putInt("x", pt.x()); + values.putInt("y", pt.y()); + values.putInt("z", pt.z()); + values.put("id", tileEntity.value().get("Id")); + if (fixer.isActive()) { + tileEntity = fixer.fixUp(DataFixer.FixTypes.BLOCK_ENTITY, values.build()); + } else { + tileEntity = values.build(); + } + tileEntitiesMap.put(pt, tileEntity); + } + } + + int width = clipboard.getRegion().getWidth(); + int length = clipboard.getRegion().getLength(); + + int index = 0; + for (VarIntIterator iter = new VarIntIterator(blocks); iter.hasNext(); index++) { + int nextBlockId = iter.nextInt(); + BlockState state = palette.get(nextBlockId); + BlockVector3 rawPos = decodePositionFromDataIndex(width, length, index); + try { + BlockVector3 offsetPos = clipboard.getMinimumPoint().add(rawPos); + LinCompoundTag tileEntity = tileEntitiesMap.get(offsetPos); + clipboard.setBlock(offsetPos, state.toBaseBlock(tileEntity)); + } catch (WorldEditException e) { + throw new IOException("Failed to load a block in the schematic", e); + } + } + } + + private static LinCompoundTag.Builder extractData(boolean dataIsNested, LinCompoundTag tag) { + if (dataIsNested) { + LinCompoundTag dataTag = tag.findTag("Data", LinTagType.compoundTag()); + return dataTag != null ? dataTag.toBuilder() : LinCompoundTag.builder(); + } else { + LinCompoundTag.Builder values = tag.toBuilder(); + values.remove("Id"); + values.remove("Pos"); + return values; + } + } + + static BlockVector3 decodePositionFromDataIndex(int width, int length, int index) { + // index = (y * width * length) + (z * width) + x + int y = index / (width * length); + int remainder = index - (y * width * length); + int z = remainder / width; + int x = remainder - z * width; + return BlockVector3.at(x, y, z); + } + + static BlockVector3 decodeBlockVector3(@Nullable LinIntArrayTag tag) throws IOException { + if (tag == null) { + return BlockVector3.ZERO; + } + int[] parts = tag.value(); + if (parts.length != 3) { + throw new IOException("Invalid block vector specified in schematic."); + } + return BlockVector3.at(parts[0], parts[1], parts[2]); + } + + static void readEntities( + BlockArrayClipboard clipboard, List entList, + VersionedDataFixer fixer, boolean positionIsRelative + ) { + if (entList.isEmpty()) { + return; + } + for (LinCompoundTag entityTag : entList) { + String id = entityTag.getTag("Id", LinTagType.stringTag()).value(); + LinCompoundTag.Builder values = extractData(positionIsRelative, entityTag); + LinCompoundTag dataTag = values.putString("id", id).build(); + dataTag = fixer.fixUp(DataFixer.FixTypes.ENTITY, dataTag); + + EntityType entityType = EntityTypes.get(id); + if (entityType != null) { + Location location = NBTConversions.toLocation( + clipboard, + entityTag.getListTag("Pos", LinTagType.doubleTag()), + dataTag.getListTag("Rotation", LinTagType.floatTag()) + ); + BaseEntity state = new BaseEntity(entityType, LazyReference.computed(dataTag)); + if (positionIsRelative) { + location = location.setPosition( + location.toVector().add(clipboard.getMinimumPoint().toVector3()) + ); + } + clipboard.createEntity(location, state); + } else { + LOGGER.warn("Unknown entity when pasting schematic: " + id); + } + } + } + + static Int2ObjectMap readBiomePalette(VersionedDataFixer fixer, LinCompoundTag paletteTag, Logger logger) throws + IOException { + Int2ObjectMap palette = new Int2ObjectLinkedOpenHashMap<>(paletteTag.value().size()); + for (var palettePart : paletteTag.value().entrySet()) { + String key = palettePart.getKey(); + key = fixer.fixUp(DataFixer.FixTypes.BIOME, key); + BiomeType biome = BiomeTypes.get(key); + if (biome == null) { + logger.warn("Unknown biome type :" + key + + " in palette. Are you missing a mod or using a schematic made in a newer version of Minecraft?"); + } + if (!(palettePart.getValue() instanceof LinIntTag idTag)) { + throw new IOException("Biome mapped to non-Int tag."); + } + palette.put(idTag.valueAsInt(), biome); + } + return palette; + } + + private ReaderUtil() { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV1Reader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV1Reader.java new file mode 100644 index 000000000..7574bfe4c --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV1Reader.java @@ -0,0 +1,134 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io.sponge; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +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.internal.Constants; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.block.BlockState; +import org.enginehub.linbus.stream.LinStream; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinIntTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinRootEntry; +import org.enginehub.linbus.tree.LinTagType; + +import java.io.IOException; +import java.util.Map; +import java.util.OptionalInt; + +/** + * Reads schematic files using the Sponge Schematic Specification (Version 1). + */ +public class SpongeSchematicV1Reader implements ClipboardReader { + + private final LinStream rootStream; + + public SpongeSchematicV1Reader(LinStream rootStream) { + this.rootStream = rootStream; + } + + @Override + public Clipboard read() throws IOException { + LinCompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(1, schematicTag); + + return doRead(schematicTag); + } + + // For legacy SpongeSchematicReader, can be inlined in WorldEdit 8 + public static BlockArrayClipboard doRead(LinCompoundTag schematicTag) throws IOException { + final Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); + + // this is a relatively safe assumption unless someone imports a schematic from 1.12 + // e.g. sponge 7.1- + VersionedDataFixer fixer = new VersionedDataFixer(Constants.DATA_VERSION_MC_1_13_2, platform.getDataFixer()); + return readVersion1(schematicTag, fixer); + } + + @Override + public OptionalInt getDataVersion() { + try { + // Validate schematic version to be sure + ReaderUtil.checkSchematicVersion(1, getBaseTag()); + return OptionalInt.of(Constants.DATA_VERSION_MC_1_13_2); + } catch (IOException e) { + return OptionalInt.empty(); + } + } + + private LinCompoundTag getBaseTag() throws IOException { + return LinRootEntry.readFrom(rootStream).value(); + } + + static BlockArrayClipboard readVersion1(LinCompoundTag schematicTag, VersionedDataFixer fixer) throws IOException { + int width = schematicTag.getTag("Width", LinTagType.shortTag()).valueAsShort() & 0xFFFF; + int height = schematicTag.getTag("Height", LinTagType.shortTag()).valueAsShort() & 0xFFFF; + int length = schematicTag.getTag("Length", LinTagType.shortTag()).valueAsShort() & 0xFFFF; + + BlockVector3 min = ReaderUtil.decodeBlockVector3(schematicTag.findTag("Offset", LinTagType.intArrayTag())); + + BlockVector3 offset = BlockVector3.ZERO; + LinCompoundTag metadataTag = schematicTag.findTag("Metadata", LinTagType.compoundTag()); + if (metadataTag != null) { + LinIntTag offsetX = metadataTag.findTag("WEOffsetX", LinTagType.intTag()); + if (offsetX != null) { + int offsetY = metadataTag.getTag("WEOffsetY", LinTagType.intTag()).valueAsInt(); + int offsetZ = metadataTag.getTag("WEOffsetZ", LinTagType.intTag()).valueAsInt(); + offset = BlockVector3.at(offsetX.valueAsInt(), offsetY, offsetZ); + } + } + + BlockVector3 origin = min.subtract(offset); + Region region = new CuboidRegion(min, min.add(width, height, length).subtract(BlockVector3.ONE)); + + LinIntTag paletteMaxTag = schematicTag.findTag("PaletteMax", LinTagType.intTag()); + LinCompoundTag paletteObject = schematicTag.getTag("Palette", LinTagType.compoundTag()); + if (paletteMaxTag != null && paletteObject.value().size() != paletteMaxTag.valueAsInt()) { + throw new IOException("Block palette size does not match expected size."); + } + + Map palette = ReaderUtil.decodePalette(paletteObject, fixer); + + byte[] blocks = schematicTag.getTag("BlockData", LinTagType.byteArrayTag()).value(); + + LinListTag tileEntities = schematicTag.findListTag("BlockEntities", LinTagType.compoundTag()); + if (tileEntities == null) { + tileEntities = schematicTag.findListTag("TileEntities", LinTagType.compoundTag()); + } + + BlockArrayClipboard clipboard = new BlockArrayClipboard(region); + clipboard.setOrigin(origin); + ReaderUtil.initializeClipboardFromBlocks(clipboard, palette, blocks, tileEntities, fixer, false); + return clipboard; + } + + @Override + public void close() throws IOException { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Reader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Reader.java new file mode 100644 index 000000000..2cd7bb1e1 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Reader.java @@ -0,0 +1,144 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io.sponge; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +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.internal.util.LogManagerCompat; +import com.sk89q.worldedit.internal.util.VarIntIterator; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.world.biome.BiomeType; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import org.apache.logging.log4j.Logger; +import org.enginehub.linbus.stream.LinStream; +import org.enginehub.linbus.tree.LinByteArrayTag; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinIntTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinRootEntry; +import org.enginehub.linbus.tree.LinTagType; + +import java.io.IOException; +import java.util.OptionalInt; + +/** + * Reads schematic files using the Sponge Schematic Specification (Version 2). + */ +public class SpongeSchematicV2Reader implements ClipboardReader { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private final LinStream rootStream; + + public SpongeSchematicV2Reader(LinStream rootStream) { + this.rootStream = rootStream; + } + + @Override + public Clipboard read() throws IOException { + LinCompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(2, schematicTag); + + return doRead(schematicTag); + } + + // For legacy SpongeSchematicReader, can be inlined in WorldEdit 8 + public static Clipboard doRead(LinCompoundTag schematicTag) throws IOException { + final Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); + int liveDataVersion = platform.getDataVersion(); + + VersionedDataFixer fixer = ReaderUtil.getVersionedDataFixer(schematicTag, platform, liveDataVersion); + BlockArrayClipboard clip = SpongeSchematicV1Reader.readVersion1(schematicTag, fixer); + return readVersion2(clip, schematicTag, fixer); + } + + @Override + public OptionalInt getDataVersion() { + try { + LinCompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(2, schematicTag); + + int dataVersion = schematicTag.getTag("DataVersion", LinTagType.intTag()).valueAsInt(); + if (dataVersion < 0) { + return OptionalInt.empty(); + } + return OptionalInt.of(dataVersion); + } catch (IOException e) { + return OptionalInt.empty(); + } + } + + private LinCompoundTag getBaseTag() throws IOException { + return LinRootEntry.readFrom(rootStream).value(); + } + + private static Clipboard readVersion2( + BlockArrayClipboard version1, LinCompoundTag schematicTag, VersionedDataFixer fixer + ) throws IOException { + if (schematicTag.value().containsKey("BiomeData")) { + readBiomes2(version1, schematicTag, fixer); + } + LinListTag entities = schematicTag.findListTag("Entities", LinTagType.compoundTag()); + if (entities != null) { + ReaderUtil.readEntities(version1, entities.value(), fixer, false); + } + return version1; + } + + private static void readBiomes2( + BlockArrayClipboard clipboard, LinCompoundTag schematic, VersionedDataFixer fixer + ) throws IOException { + LinByteArrayTag dataTag = schematic.getTag("BiomeData", LinTagType.byteArrayTag()); + LinIntTag maxTag = schematic.getTag("BiomePaletteMax", LinTagType.intTag()); + LinCompoundTag paletteTag = schematic.getTag("BiomePalette", LinTagType.compoundTag()); + + if (maxTag.valueAsInt() != paletteTag.value().size()) { + throw new IOException("Biome palette size does not match expected size."); + } + + Int2ObjectMap palette = ReaderUtil.readBiomePalette(fixer, paletteTag, LOGGER); + + int width = clipboard.getDimensions().x(); + + byte[] biomes = dataTag.value(); + BlockVector3 min = clipboard.getMinimumPoint(); + int index = 0; + for (VarIntIterator iter = new VarIntIterator(biomes); iter.hasNext(); index++) { + int nextBiomeId = iter.nextInt(); + BiomeType type = palette.get(nextBiomeId); + // hack -- the x and y values from the 3d decode with length == 1 are equivalent + BlockVector3 hackDecode = ReaderUtil.decodePositionFromDataIndex(width, 1, index); + int x = hackDecode.x(); + int z = hackDecode.y(); + for (int y = 0; y < clipboard.getRegion().getHeight(); y++) { + clipboard.setBiome(min.add(x, y, z), type); + } + } + } + + @Override + public void close() throws IOException { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicWriter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Writer.java similarity index 51% rename from worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicWriter.java rename to worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Writer.java index 38c0115e5..8504f0fe8 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicWriter.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Writer.java @@ -17,62 +17,41 @@ * along with this program. If not, see . */ -package com.sk89q.worldedit.extent.clipboard.io; +package com.sk89q.worldedit.extent.clipboard.io.sponge; -import com.fastasyncworldedit.core.Fawe; -import com.google.common.collect.Maps; -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.WorldEdit; -import com.sk89q.worldedit.entity.BaseEntity; import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; 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.block.BaseBlock; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import org.enginehub.linbus.stream.LinBinaryIO; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinRootEntry; +import org.enginehub.linbus.tree.LinTagType; import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; import java.io.IOException; -import java.util.ArrayList; -import java.util.HashMap; -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; /** - * Writes schematic files using the Sponge schematic format. - * - * @deprecated Slow, resource intensive, but sometimes safer than using the recommended - * {@link com.fastasyncworldedit.core.extent.clipboard.io.FastSchematicWriter}. - * Avoid using large clipboards to create schematics with this writer. + * Writes schematic files using the Sponge Schematic Specification (Version 2). */ -@Deprecated -public class SpongeSchematicWriter implements ClipboardWriter { +public class SpongeSchematicV2Writer implements ClipboardWriter { private static final int CURRENT_VERSION = 2; private static final int MAX_SIZE = Short.MAX_VALUE - Short.MIN_VALUE; - private final NBTOutputStream outputStream; + private final DataOutputStream outputStream; - /** - * Create a new schematic writer. - * - * @param outputStream the output stream to write to - */ - public SpongeSchematicWriter(NBTOutputStream outputStream) { - checkNotNull(outputStream); + public SpongeSchematicV2Writer(DataOutputStream outputStream) { this.outputStream = outputStream; } @@ -82,17 +61,16 @@ public class SpongeSchematicWriter implements ClipboardWriter { // between upstream and FAWE clipboard.flush(); //FAWE end - // For now always write the latest version. Maybe provide support for earlier if more appear. - outputStream.writeNamedTag("Schematic", new CompoundTag(write2(clipboard))); + LinBinaryIO.write(outputStream, new LinRootEntry("Schematic", write2(clipboard))); } /** * Writes a version 2 schematic file. * * @param clipboard The clipboard - * @return The schematic map + * @return the schematic tag */ - private Map> write2(Clipboard clipboard) { + private LinCompoundTag write2(Clipboard clipboard) { Region region = clipboard.getRegion(); BlockVector3 origin = clipboard.getOrigin(); BlockVector3 min = region.getMinimumPoint(); @@ -111,34 +89,49 @@ public class SpongeSchematicWriter implements ClipboardWriter { throw new IllegalArgumentException("Length of region too large for a .schematic"); } - Map> schematic = new HashMap<>(); - schematic.put("Version", new IntTag(CURRENT_VERSION)); - schematic.put("DataVersion", new IntTag( - WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion())); + LinCompoundTag.Builder schematic = LinCompoundTag.builder(); + schematic.putInt("Version", CURRENT_VERSION); + schematic.putInt("DataVersion", + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion() + ); - Map> metadata = new HashMap<>(); - metadata.put("WEOffsetX", new IntTag(offset.x())); - metadata.put("WEOffsetY", new IntTag(offset.y())); - metadata.put("WEOffsetZ", new IntTag(offset.z())); - metadata.put("FAWEVersion", new IntTag(Fawe.instance().getVersion().build)); + LinCompoundTag.Builder metadata = LinCompoundTag.builder(); + metadata.putInt("WEOffsetX", offset.x()); + metadata.putInt("WEOffsetY", offset.y()); + metadata.putInt("WEOffsetZ", offset.z()); - schematic.put("Metadata", new CompoundTag(metadata)); + LinCompoundTag.Builder worldEditSection = LinCompoundTag.builder(); + worldEditSection.putString("Version", WorldEdit.getVersion()); + worldEditSection.putString("EditingPlatform", + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).id() + ); + worldEditSection.putIntArray("Offset", new int[]{offset.x(), offset.y(), offset.z()}); - schematic.put("Width", new ShortTag((short) width)); - schematic.put("Height", new ShortTag((short) height)); - schematic.put("Length", new ShortTag((short) length)); + LinCompoundTag.Builder platformsSection = LinCompoundTag.builder(); + for (Platform platform : WorldEdit.getInstance().getPlatformManager().getPlatforms()) { + platformsSection.put(platform.id(), LinCompoundTag + .builder() + .putString("Name", platform.getPlatformName()) + .putString("Version", platform.getPlatformVersion()) + .build()); + } + worldEditSection.put("Platforms", platformsSection.build()); + + metadata.put("WorldEdit", worldEditSection.build()); + + schematic.put("Metadata", metadata.build()); + + schematic.putShort("Width", (short) width); + schematic.putShort("Height", (short) height); + schematic.putShort("Length", (short) length); // The Sponge format Offset refers to the 'min' points location in the world. That's our 'Origin' - schematic.put("Offset", new IntArrayTag(new int[]{ - min.x(), - min.y(), - min.z(), - })); + schematic.putIntArray("Offset", new int[]{min.x(), min.y(), min.z(),}); int paletteMax = 0; - Map palette = new HashMap<>(); + Object2IntMap palette = new Object2IntLinkedOpenHashMap<>(); - List tileEntities = new ArrayList<>(); + LinListTag.Builder tileEntities = LinListTag.builder(LinTagType.compoundTag()); ByteArrayOutputStream buffer = new ByteArrayOutputStream(width * height * length); @@ -150,8 +143,9 @@ public class SpongeSchematicWriter implements ClipboardWriter { int x0 = min.x() + x; BlockVector3 point = BlockVector3.at(x0, y0, z0); BaseBlock block = clipboard.getFullBlock(point); - if (block.getNbtData() != null) { - Map> values = new HashMap<>(block.getNbtData().getValue()); + LinCompoundTag nbt = block.getNbt(); + if (nbt != null) { + LinCompoundTag.Builder values = nbt.toBuilder(); values.remove("id"); // Remove 'id' if it exists. We want 'Id' @@ -160,16 +154,16 @@ public class SpongeSchematicWriter implements ClipboardWriter { values.remove("y"); values.remove("z"); - values.put("Id", new StringTag(block.getNbtId())); - values.put("Pos", new IntArrayTag(new int[]{x, y, z})); + values.putString("Id", block.getNbtId()); + values.putIntArray("Pos", new int[]{x, y, z}); - tileEntities.add(new CompoundTag(values)); + tileEntities.add(values.build()); } String blockKey = block.toImmutableState().getAsString(); int blockId; if (palette.containsKey(blockKey)) { - blockId = palette.get(blockKey); + blockId = palette.getInt(blockKey); } else { blockId = paletteMax; palette.put(blockKey, blockId); @@ -185,14 +179,14 @@ public class SpongeSchematicWriter implements ClipboardWriter { } } - schematic.put("PaletteMax", new IntTag(paletteMax)); + schematic.putInt("PaletteMax", paletteMax); - Map> paletteTag = new HashMap<>(); - palette.forEach((key, value) -> paletteTag.put(key, new IntTag(value))); + LinCompoundTag.Builder paletteTag = LinCompoundTag.builder(); + Object2IntMaps.fastForEach(palette, e -> paletteTag.putInt(e.getKey(), e.getIntValue())); - schematic.put("Palette", new CompoundTag(paletteTag)); - schematic.put("BlockData", new ByteArrayTag(buffer.toByteArray())); - schematic.put("BlockEntities", new ListTag(CompoundTag.class, tileEntities)); + schematic.put("Palette", paletteTag.build()); + schematic.putByteArray("BlockData", buffer.toByteArray()); + schematic.put("BlockEntities", tileEntities.build()); // version 2 stuff if (clipboard.hasBiomes()) { @@ -200,13 +194,16 @@ public class SpongeSchematicWriter implements ClipboardWriter { } if (!clipboard.getEntities().isEmpty()) { - writeEntities(clipboard, schematic); + LinListTag value = WriterUtil.encodeEntities(clipboard, false); + if (value != null) { + schematic.put("Entities", value); + } } - return schematic; + return schematic.build(); } - private void writeBiomes(Clipboard clipboard, Map> schematic) { + private void writeBiomes(Clipboard clipboard, LinCompoundTag.Builder schematic) { BlockVector3 min = clipboard.getMinimumPoint(); int width = clipboard.getRegion().getWidth(); int length = clipboard.getRegion().getLength(); @@ -214,7 +211,7 @@ public class SpongeSchematicWriter implements ClipboardWriter { ByteArrayOutputStream buffer = new ByteArrayOutputStream(width * length); int paletteMax = 0; - Map palette = new HashMap<>(); + Object2IntMap palette = new Object2IntLinkedOpenHashMap<>(); for (int z = 0; z < length; z++) { int z0 = min.z() + z; @@ -226,7 +223,7 @@ public class SpongeSchematicWriter implements ClipboardWriter { String biomeKey = biome.id(); int biomeId; if (palette.containsKey(biomeKey)) { - biomeId = palette.get(biomeKey); + biomeId = palette.getInt(biomeKey); } else { biomeId = paletteMax; palette.put(biomeKey, biomeId); @@ -241,38 +238,13 @@ public class SpongeSchematicWriter implements ClipboardWriter { } } - schematic.put("BiomePaletteMax", new IntTag(paletteMax)); + schematic.putInt("BiomePaletteMax", paletteMax); - Map> paletteTag = new HashMap<>(); - palette.forEach((key, value) -> paletteTag.put(key, new IntTag(value))); + LinCompoundTag.Builder paletteTag = LinCompoundTag.builder(); + Object2IntMaps.fastForEach(palette, e -> paletteTag.putInt(e.getKey(), e.getIntValue())); - schematic.put("BiomePalette", new CompoundTag(paletteTag)); - schematic.put("BiomeData", new ByteArrayTag(buffer.toByteArray())); - } - - private void writeEntities(Clipboard clipboard, Map> schematic) { - List entities = clipboard.getEntities().stream().map(e -> { - BaseEntity state = e.getState(); - if (state == null) { - return null; - } - Map> values = Maps.newHashMap(); - CompoundTag rawData = state.getNbtData(); - if (rawData != null) { - values.putAll(rawData.getValue()); - } - values.remove("id"); - values.put("Id", new StringTag(state.getType().id())); - final Location location = e.getLocation(); - values.put("Pos", writeVector(location.toVector())); - values.put("Rotation", writeRotation(location)); - - return new CompoundTag(values); - }).filter(Objects::nonNull).collect(Collectors.toList()); - if (entities.isEmpty()) { - return; - } - schematic.put("Entities", new ListTag(CompoundTag.class, entities)); + schematic.put("BiomePalette", paletteTag.build()); + schematic.putByteArray("BiomeData", buffer.toByteArray()); } @Override diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Reader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Reader.java new file mode 100644 index 000000000..978cb8ba5 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Reader.java @@ -0,0 +1,168 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io.sponge; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +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.internal.util.LogManagerCompat; +import com.sk89q.worldedit.internal.util.VarIntIterator; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.block.BlockState; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import org.apache.logging.log4j.Logger; +import org.enginehub.linbus.stream.LinStream; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinRootEntry; +import org.enginehub.linbus.tree.LinTagType; + +import java.io.IOException; +import java.util.Map; +import java.util.OptionalInt; + +/** + * Reads schematic files using the Sponge Schematic Specification. + */ +public class SpongeSchematicV3Reader implements ClipboardReader { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private final LinStream rootStream; + + public SpongeSchematicV3Reader(LinStream rootStream) { + this.rootStream = rootStream; + } + + @Override + public Clipboard read() throws IOException { + LinCompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(3, schematicTag); + + final Platform platform = WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING); + int liveDataVersion = platform.getDataVersion(); + + VersionedDataFixer fixer = ReaderUtil.getVersionedDataFixer(schematicTag, platform, liveDataVersion); + return readVersion3(schematicTag, fixer); + } + + @Override + public OptionalInt getDataVersion() { + try { + LinCompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(3, schematicTag); + + int dataVersion = schematicTag.getTag("DataVersion", LinTagType.intTag()).valueAsInt(); + if (dataVersion < 0) { + return OptionalInt.empty(); + } + return OptionalInt.of(dataVersion); + } catch (IOException e) { + return OptionalInt.empty(); + } + } + + private LinCompoundTag getBaseTag() throws IOException { + return LinRootEntry.readFrom(rootStream).value().getTag("Schematic", LinTagType.compoundTag()); + } + + private Clipboard readVersion3(LinCompoundTag schematicTag, VersionedDataFixer fixer) throws IOException { + int width = schematicTag.getTag("Width", LinTagType.shortTag()).valueAsShort() & 0xFFFF; + int height = schematicTag.getTag("Height", LinTagType.shortTag()).valueAsShort() & 0xFFFF; + int length = schematicTag.getTag("Length", LinTagType.shortTag()).valueAsShort() & 0xFFFF; + + BlockVector3 offset = ReaderUtil.decodeBlockVector3(schematicTag.findTag("Offset", LinTagType.intArrayTag())); + + BlockVector3 origin = BlockVector3.ZERO; + LinCompoundTag metadataTag = schematicTag.findTag("Metadata", LinTagType.compoundTag()); + if (metadataTag != null) { + LinCompoundTag worldeditMeta = metadataTag.findTag("WorldEdit", LinTagType.compoundTag()); + if (worldeditMeta != null) { + origin = ReaderUtil.decodeBlockVector3(worldeditMeta.findTag("Origin", LinTagType.intArrayTag())); + } + } + BlockVector3 min = offset.add(origin); + + BlockArrayClipboard clipboard = new BlockArrayClipboard(new CuboidRegion( + min, + min.add(width, height, length).subtract(BlockVector3.ONE) + )); + clipboard.setOrigin(origin); + + decodeBlocksIntoClipboard(fixer, schematicTag, clipboard); + + LinCompoundTag biomeContainer = schematicTag.findTag("Biomes", LinTagType.compoundTag()); + if (biomeContainer != null) { + readBiomes3(clipboard, biomeContainer, fixer); + } + + LinListTag entities = schematicTag.findListTag("Entities", LinTagType.compoundTag()); + if (entities != null) { + ReaderUtil.readEntities(clipboard, entities.value(), fixer, true); + } + + return clipboard; + } + + private void decodeBlocksIntoClipboard( + VersionedDataFixer fixer, LinCompoundTag schematic, BlockArrayClipboard clipboard + ) throws IOException { + LinCompoundTag blockContainer = schematic.getTag("Blocks", LinTagType.compoundTag()); + + LinCompoundTag paletteObject = blockContainer.getTag("Palette", LinTagType.compoundTag()); + Map palette = ReaderUtil.decodePalette(paletteObject, fixer); + + byte[] blocks = blockContainer.getTag("Data", LinTagType.byteArrayTag()).value(); + LinListTag blockEntities = blockContainer.getListTag("BlockEntities", LinTagType.compoundTag()); + + ReaderUtil.initializeClipboardFromBlocks(clipboard, palette, blocks, blockEntities, fixer, true); + } + + private void readBiomes3( + BlockArrayClipboard clipboard, LinCompoundTag biomeContainer, VersionedDataFixer fixer + ) throws IOException { + LinCompoundTag paletteTag = biomeContainer.getTag("Palette", LinTagType.compoundTag()); + + Int2ObjectMap palette = ReaderUtil.readBiomePalette(fixer, paletteTag, LOGGER); + + int width = clipboard.getRegion().getWidth(); + int length = clipboard.getRegion().getLength(); + + byte[] biomes = biomeContainer.getTag("Data", LinTagType.byteArrayTag()).value(); + BlockVector3 min = clipboard.getMinimumPoint(); + int index = 0; + for (VarIntIterator iter = new VarIntIterator(biomes); iter.hasNext(); index++) { + int nextBiomeId = iter.nextInt(); + BiomeType type = palette.get(nextBiomeId); + BlockVector3 pos = ReaderUtil.decodePositionFromDataIndex(width, length, index); + clipboard.setBiome(min.add(pos), type); + } + } + + @Override + public void close() throws IOException { + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Writer.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Writer.java new file mode 100644 index 000000000..4030c2f90 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Writer.java @@ -0,0 +1,233 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io.sponge; + +import com.sk89q.worldedit.WorldEdit; +import com.sk89q.worldedit.extension.platform.Capability; +import com.sk89q.worldedit.extension.platform.Platform; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.ClipboardWriter; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.Region; +import com.sk89q.worldedit.world.block.BaseBlock; +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntMaps; +import org.enginehub.linbus.stream.LinBinaryIO; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinRootEntry; +import org.enginehub.linbus.tree.LinTagType; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.function.Function; + +/** + * Writes schematic files using the Sponge Schematic Specification (Version 3). + */ +public class SpongeSchematicV3Writer implements ClipboardWriter { + + private static final int CURRENT_VERSION = 3; + + private static final int MAX_SIZE = Short.MAX_VALUE - Short.MIN_VALUE; + private final DataOutputStream outputStream; + + public SpongeSchematicV3Writer(DataOutputStream outputStream) { + this.outputStream = outputStream; + } + + @Override + public void write(Clipboard clipboard) throws IOException { + //FAWE start - ensure clipboard is flushed in case of using clipboard-on-disk. Maintains allowing of the same code + // between upstream and FAWE + clipboard.flush(); + //FAWE end + LinBinaryIO.write(outputStream, + new LinRootEntry("", LinCompoundTag.builder().put("Schematic", write3(clipboard)).build()) + ); + } + + /** + * Writes a version 3 schematic file. + * + * @param clipboard The clipboard + * @return The schematic map + */ + private LinCompoundTag write3(Clipboard clipboard) { + Region region = clipboard.getRegion(); + BlockVector3 origin = clipboard.getOrigin(); + BlockVector3 min = region.getMinimumPoint(); + BlockVector3 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"); + } + + LinCompoundTag.Builder schematic = LinCompoundTag.builder(); + schematic.putInt("Version", CURRENT_VERSION); + schematic.putInt("DataVersion", + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion() + ); + + LinCompoundTag.Builder metadata = LinCompoundTag.builder(); + metadata.putLong("Date", System.currentTimeMillis()); + + LinCompoundTag.Builder worldEditSection = LinCompoundTag.builder(); + worldEditSection.putString("Version", WorldEdit.getVersion()); + worldEditSection.putString("EditingPlatform", + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).id() + ); + worldEditSection.putIntArray("Origin", new int[]{origin.x(), origin.y(), origin.z()}); + + LinCompoundTag.Builder platformsSection = LinCompoundTag.builder(); + for (Platform platform : WorldEdit.getInstance().getPlatformManager().getPlatforms()) { + platformsSection.put(platform.id(), LinCompoundTag + .builder() + .putString("Name", platform.getPlatformName()) + .putString("Version", platform.getPlatformVersion()) + .build()); + } + worldEditSection.put("Platforms", platformsSection.build()); + + metadata.put("WorldEdit", worldEditSection.build()); + + schematic.put("Metadata", metadata.build()); + + schematic.putShort("Width", (short) width); + schematic.putShort("Height", (short) height); + schematic.putShort("Length", (short) length); + + schematic.putIntArray("Offset", new int[]{offset.x(), offset.y(), offset.z(),}); + + schematic.put("Blocks", encodeBlocks(clipboard)); + + if (clipboard.hasBiomes()) { + schematic.put("Biomes", encodeBiomes(clipboard)); + } + + if (!clipboard.getEntities().isEmpty()) { + LinListTag value = WriterUtil.encodeEntities(clipboard, true); + if (value != null) { + schematic.put("Entities", value); + } + } + + return schematic.build(); + } + + private static final class PaletteMap { + + private final Object2IntMap contents = new Object2IntLinkedOpenHashMap<>(); + private int nextId = 0; + + public int getId(String key) { + int result = contents.getOrDefault(key, -1); + if (result != -1) { + return result; + } + int newValue = nextId; + nextId++; + contents.put(key, newValue); + return newValue; + } + + public LinCompoundTag toNbt() { + LinCompoundTag.Builder result = LinCompoundTag.builder(); + Object2IntMaps.fastForEach(contents, e -> result.putInt(e.getKey(), e.getIntValue())); + return result.build(); + } + + } + + private LinCompoundTag encodeBlocks(Clipboard clipboard) { + LinListTag.Builder blockEntities = LinListTag.builder(LinTagType.compoundTag()); + LinCompoundTag.Builder result = encodePalettedData(clipboard, point -> { + BaseBlock block = clipboard.getFullBlock(point); + // Also compute block entity side-effect here + LinCompoundTag nbt = block.getNbt(); + if (nbt != null) { + LinCompoundTag.Builder builder = LinCompoundTag.builder(); + + builder.putString("Id", block.getNbtId()); + BlockVector3 adjustedPos = point.subtract(clipboard.getMinimumPoint()); + builder.putIntArray("Pos", new int[]{adjustedPos.x(), adjustedPos.y(), adjustedPos.z()}); + builder.put("Data", nbt); + + blockEntities.add(builder.build()); + } + return block.toImmutableState().getAsString(); + }); + + return result.put("BlockEntities", blockEntities.build()).build(); + } + + private LinCompoundTag encodeBiomes(Clipboard clipboard) { + return encodePalettedData(clipboard, point -> clipboard.getBiome(point).id()).build(); + } + + private LinCompoundTag.Builder encodePalettedData( + Clipboard clipboard, Function keyFunction + ) { + BlockVector3 min = clipboard.getMinimumPoint(); + int width = clipboard.getRegion().getWidth(); + int height = clipboard.getRegion().getHeight(); + int length = clipboard.getRegion().getLength(); + + PaletteMap paletteMap = new PaletteMap(); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(width * height * length); + + for (int y = 0; y < height; y++) { + for (int z = 0; z < length; z++) { + for (int x = 0; x < width; x++) { + BlockVector3 point = min.add(x, y, z); + + String key = keyFunction.apply(point); + int id = paletteMap.getId(key); + + while ((id & -128) != 0) { + buffer.write(id & 127 | 128); + id >>>= 7; + } + buffer.write(id); + } + } + } + + return LinCompoundTag.builder().put("Palette", paletteMap.toNbt()).putByteArray("Data", buffer.toByteArray()); + } + + @Override + public void close() throws IOException { + outputStream.close(); + } + +} 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 new file mode 100644 index 000000000..35f6d58bc --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/VersionedDataFixer.java @@ -0,0 +1,47 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io.sponge; + + +import com.sk89q.worldedit.world.DataFixer; + +import javax.annotation.Nullable; + +public final class VersionedDataFixer { //FAWE - public + private final int dataVersion; + @Nullable + private final DataFixer fixer; + + public VersionedDataFixer(int dataVersion, @Nullable DataFixer fixer) { //FAWE - public + this.dataVersion = dataVersion; + this.fixer = fixer; + } + + public boolean isActive() { + return fixer != null; + } + + public T fixUp(DataFixer.FixType type, T original) { + if (!isActive()) { + return original; + } + return fixer.fixUp(type, original, dataVersion); + } +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/WriterUtil.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/WriterUtil.java new file mode 100644 index 000000000..17ff5004d --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/WriterUtil.java @@ -0,0 +1,91 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.extent.clipboard.io.sponge; + +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.entity.Entity; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.math.Vector3; +import com.sk89q.worldedit.util.Location; +import org.enginehub.linbus.tree.LinCompoundTag; +import org.enginehub.linbus.tree.LinDoubleTag; +import org.enginehub.linbus.tree.LinFloatTag; +import org.enginehub.linbus.tree.LinListTag; +import org.enginehub.linbus.tree.LinTagType; + +class WriterUtil { + + static LinListTag encodeEntities(Clipboard clipboard, boolean positionIsRelative) { + LinListTag.Builder entities = LinListTag.builder(LinTagType.compoundTag()); + for (Entity entity : clipboard.getEntities()) { + LinCompoundTag encoded = encodeEntity(clipboard, positionIsRelative, entity); + if (encoded != null) { + entities.add(encoded); + } + } + var result = entities.build(); + if (result.value().isEmpty()) { + return null; + } + return result; + } + + private static LinCompoundTag encodeEntity(Clipboard clipboard, boolean positionIsRelative, Entity e) { + BaseEntity state = e.getState(); + if (state == null) { + return null; + } + LinCompoundTag.Builder fullTagBuilder = LinCompoundTag.builder(); + LinCompoundTag.Builder dataTagBuilder = LinCompoundTag.builder(); + LinCompoundTag rawData = state.getNbt(); + if (rawData != null) { + dataTagBuilder.putAll(rawData.value()); + dataTagBuilder.remove("id"); + } + final Location location = e.getLocation(); + Vector3 pos = location.toVector(); + dataTagBuilder.put("Rotation", encodeRotation(location)); + if (positionIsRelative) { + pos = pos.subtract(clipboard.getMinimumPoint().toVector3()); + + fullTagBuilder.put("Data", dataTagBuilder.build()); + } else { + fullTagBuilder.putAll(dataTagBuilder.build().value()); + } + fullTagBuilder.putString("Id", state.getType().id()); + fullTagBuilder.put("Pos", encodeVector(pos)); + + return fullTagBuilder.build(); + } + + static LinListTag encodeVector(Vector3 vector) { + return LinListTag.builder(LinTagType.doubleTag()).add(LinDoubleTag.of(vector.x())).add(LinDoubleTag.of(vector.y())).add( + LinDoubleTag.of(vector.z())).build(); + } + + static LinListTag encodeRotation(Location location) { + return LinListTag + .builder(LinTagType.floatTag()) + .add(LinFloatTag.of(location.getYaw())) + .add(LinFloatTag.of(location.getPitch())) + .build(); + } + +} diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/package-info.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/package-info.java new file mode 100644 index 000000000..930ae5efb --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/package-info.java @@ -0,0 +1,26 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/** + * This package is internal, containing implementation details of the Sponge Schematic + * Specification. Use the {@link com.sk89q.worldedit.extent.clipboard.io.ClipboardFormats} or + * {@link com.sk89q.worldedit.extent.clipboard.io.BuiltInClipboardFormat} classes to + * acquire readers and writers instead. + */ +package com.sk89q.worldedit.extent.clipboard.io.sponge; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/VarIntIterator.java b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/VarIntIterator.java new file mode 100644 index 000000000..ac91c8365 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/internal/util/VarIntIterator.java @@ -0,0 +1,81 @@ +/* + * 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 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sk89q.worldedit.internal.util; + +import it.unimi.dsi.fastutil.ints.IntIterator; + +import java.util.NoSuchElementException; +import java.util.PrimitiveIterator; + +/** + * Simple class to transform a {@code byte[]} into an iterator of the VarInts encoded in it. + */ +public class VarIntIterator implements PrimitiveIterator.OfInt { + + private final byte[] source; + private int index; + private boolean hasNextInt; + private int nextInt; + + public VarIntIterator(byte[] source) { + this.source = source; + } + + @Override + public boolean hasNext() { + if (hasNextInt) { + return true; + } + if (index >= source.length) { + return false; + } + + nextInt = readNextInt(); + return hasNextInt = true; + } + + private int readNextInt() { + int value = 0; + for (int bitsRead = 0; ; bitsRead += 7) { + if (index >= source.length) { + throw new IllegalStateException("Ran out of bytes while reading VarInt (probably corrupted data)"); + } + byte next = source[index]; + 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; + } + + @Override + public int nextInt() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + hasNextInt = false; + return nextInt; + } +}