From 56ae44b1e2e7e2afcc48fc31ef5a63ae94ba2e13 Mon Sep 17 00:00:00 2001 From: Octavia Togami Date: Wed, 19 May 2021 22:41:03 +0200 Subject: [PATCH] Update to Sponge Schematic 3 Includes a major refactoring of how schematics are read. (cherry picked from commit bd475b1d4acbcf2a95e5a8f3aee50d2fb2100ae8) --- .../clipboard/io/BuiltInClipboardFormat.java | 123 ++++- .../extent/clipboard/io/SchematicNbtUtil.java | 42 ++ .../clipboard/io/SpongeSchematicReader.java | 458 ------------------ .../BuiltInClipboardShareDestinations.java | 4 +- .../clipboard/io/sponge/ReaderUtil.java | 234 +++++++++ .../io/sponge/SpongeSchematicV1Reader.java | 155 ++++++ .../io/sponge/SpongeSchematicV2Reader.java | 174 +++++++ .../SpongeSchematicV2Writer.java} | 64 ++- .../io/sponge/SpongeSchematicV3Reader.java | 215 ++++++++ .../io/sponge/SpongeSchematicV3Writer.java | 258 ++++++++++ .../io/sponge/VersionedDataFixer.java | 28 ++ .../clipboard/io/sponge/WriterUtil.java | 65 +++ .../clipboard/io/sponge/package-info.java | 7 + .../internal/util/VarIntIterator.java | 81 ++++ 14 files changed, 1402 insertions(+), 506 deletions(-) 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} (85%) 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-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 5aafa220e..a112433d9 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 @@ -26,10 +26,16 @@ 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.IntTag; 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 org.anarres.parallelgzip.ParallelGZIPOutputStream; import java.io.BufferedInputStream; @@ -118,14 +124,8 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { return name.endsWith(".schematic") || name.endsWith(".mcedit") || name.endsWith(".mce"); } }, + SPONGE_V1_SCHEMATIC("sponge.1") { - /** - * @deprecated Slow, resource intensive, but sometimes safer than using the recommended - * {@link BuiltInClipboardFormat#FAST}. - * Avoid using with any large schematics/clipboards for reading/writing. - */ - @Deprecated - SPONGE_SCHEMATIC("slow", "safe") { @Override public String getPrimaryFileExtension() { return "schem"; @@ -134,13 +134,12 @@ 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 SpongeSchematicV1Reader(nbtStream); } @Override public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { - NBTOutputStream nbtStream = new NBTOutputStream(new GZIPOutputStream(outputStream)); - return new SpongeSchematicWriter(nbtStream); + throw new IOException("This format does not support saving"); } @Override @@ -154,7 +153,8 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { // Check Map schematic = schematicTag.getValue(); - if (!schematic.containsKey("Version")) { + Tag versionTag = schematic.get("Version"); + if (!(versionTag instanceof IntTag) || ((IntTag) versionTag).getValue() != 1) { return false; } } catch (Exception e) { @@ -165,6 +165,98 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { } }, + /** + * @deprecated Slow, resource intensive, but sometimes safer than using the recommended + * {@link BuiltInClipboardFormat#FAST}. + * Avoid using with any large schematics/clipboards for reading/writing. + */ + @Deprecated + SPONGE_V2_SCHEMATIC("slow", "safe", "sponge.2") { + @Override + public String getPrimaryFileExtension() { + return "schem"; + } + + @Override + public ClipboardReader getReader(InputStream inputStream) throws IOException { + NBTInputStream nbtStream = new NBTInputStream(new GZIPInputStream(inputStream)); + return new SpongeSchematicV2Reader(nbtStream); + } + + @Override + public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { + NBTOutputStream nbtStream = new NBTOutputStream(new GZIPOutputStream(outputStream)); + return new SpongeSchematicV2Writer(nbtStream); + } + + @Override + public boolean isFormat(File file) { + try (NBTInputStream str = new NBTInputStream(new GZIPInputStream(new FileInputStream(file)))) { + NamedTag rootTag = str.readNamedTag(); + if (!rootTag.getName().equals("Schematic")) { + return false; + } + CompoundTag schematicTag = (CompoundTag) rootTag.getTag(); + + // Check + Map schematic = schematicTag.getValue(); + Tag versionTag = schematic.get("Version"); + if (!(versionTag instanceof IntTag) || ((IntTag) versionTag).getValue() != 2) { + return false; + } + } catch (Exception e) { + return false; + } + + return true; + } + }, + SPONGE_V3_SCHEMATIC("sponge.3", "sponge", "schem") { + + @Override + public String getPrimaryFileExtension() { + return "schem"; + } + + @Override + public ClipboardReader getReader(InputStream inputStream) throws IOException { + NBTInputStream nbtStream = new NBTInputStream(new GZIPInputStream(inputStream)); + return new SpongeSchematicV3Reader(nbtStream); + } + + @Override + public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { + NBTOutputStream nbtStream = new NBTOutputStream(new GZIPOutputStream(outputStream)); + return new SpongeSchematicV3Writer(nbtStream); + } + + @Override + public boolean isFormat(File file) { + try (NBTInputStream str = new NBTInputStream(new GZIPInputStream(new FileInputStream(file)))) { + NamedTag rootTag = str.readNamedTag(); + CompoundTag rootCompoundTag = (CompoundTag) rootTag.getTag(); + if (!rootCompoundTag.containsKey("Schematic")) { + return false; + } + Tag schematicTag = rootCompoundTag.getValue() + .get("Schematic"); + if (!(schematicTag instanceof CompoundTag)) { + return false; + } + + // Check + Map schematic = ((CompoundTag) schematicTag).getValue(); + Tag versionTag = schematic.get("Version"); + if (!(versionTag instanceof IntTag) || ((IntTag) versionTag).getValue() != 3) { + return false; + } + } catch (Exception e) { + return false; + } + + return true; + } + }, //FAWE start - recover schematics with bad entity data & register other clipboard formats BROKENENTITY("brokenentity", "legacyentity", "le", "be", "brokenentities", "legacyentities") { @Override @@ -265,6 +357,15 @@ public enum BuiltInClipboardFormat implements ClipboardFormat { }; //FAWE end + /** + * 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; + private final ImmutableSet aliases; BuiltInClipboardFormat(String... aliases) { 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..898678241 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SchematicNbtUtil.java @@ -0,0 +1,42 @@ +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 a140b666a..000000000 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicReader.java +++ /dev/null @@ -1,458 +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.AdventureNBTConverter; -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 java.util.stream.Collectors; - -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) { - List> tileEntityTags = tileEntities.getValue().stream() - .map(tag -> (CompoundTag) tag) - .map(CompoundTag::getValue) - .collect(Collectors.toList()); - - for (Map tileEntity : tileEntityTags) { - 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 - BinaryTag - tileEntity = ((CompoundTag) AdventureNBTConverter.fromAdventure(fixer.fixUp( - DataFixer.FixTypes.BLOCK_ENTITY, - new CompoundTag(values).asBinaryTag(), - 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 - BinaryTag - entityTag = (CompoundTag) AdventureNBTConverter.fromAdventure(fixer.fixUp( - DataFixer.FixTypes.ENTITY, - entityTag.asBinaryTag(), - 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..e6cbfda05 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 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..b4e14b88c --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/ReaderUtil.java @@ -0,0 +1,234 @@ +package com.sk89q.worldedit.extent.clipboard.io.sponge; + +import com.google.common.collect.Maps; +import com.sk89q.jnbt.AdventureNBTConverter; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.IntArrayTag; +import com.sk89q.jnbt.IntTag; +import com.sk89q.jnbt.ListTag; +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.Platform; +import com.sk89q.worldedit.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.extent.clipboard.io.SchematicNbtUtil; +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.world.DataFixer; +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.stream.Collectors; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkState; +import static com.sk89q.worldedit.extent.clipboard.io.SchematicNbtUtil.requireTag; + +/** + * Common code shared between schematic readers. + */ +class ReaderUtil { + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + static void checkSchematicVersion(int version, CompoundTag schematicTag) throws IOException { + int schematicVersion = requireTag(schematicTag.getValue(), "Version", IntTag.class) + .getValue(); + + checkState( + version == schematicVersion, + "Schematic is not version %s, but %s", version, schematicVersion + ); + } + + static VersionedDataFixer getVersionedDataFixer(Map schematic, Platform platform, + int liveDataVersion) throws IOException { + DataFixer fixer = null; + int dataVersion = requireTag(schematic, "DataVersion", IntTag.class).getValue(); + if (dataVersion < 0) { + LOGGER.warn( + "Schematic has an unknown data version ({}). Data may be incompatible.", + dataVersion + ); + } else 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.debug( + "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 + ); + } + } + + return new VersionedDataFixer(dataVersion, fixer); + } + + static Map decodePalette( + Map paletteObject, VersionedDataFixer fixer + ) throws IOException { + 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(); + palettePart = fixer.fixUp(DataFixer.FixTypes.BLOCK_STATE, palettePart); + 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); + } + return palette; + } + + static void initializeClipboardFromBlocks( + Clipboard clipboard, Map palette, byte[] blocks, ListTag tileEntities, + VersionedDataFixer fixer + ) throws IOException { + Map> tileEntitiesMap = new HashMap<>(); + if (tileEntities != null) { + List> tileEntityTags = tileEntities.getValue().stream() + .map(tag -> (CompoundTag) tag) + .map(CompoundTag::getValue) + .collect(Collectors.toList()); + + for (Map tileEntity : tileEntityTags) { + int[] pos = requireTag(tileEntity, "Pos", IntArrayTag.class).getValue(); + final BlockVector3 pt = clipboard.getMinimumPoint().add(pos[0], pos[1], pos[2]); + Map values = Maps.newHashMap(tileEntity); + values.put("x", new IntTag(pt.getBlockX())); + values.put("y", new IntTag(pt.getBlockY())); + values.put("z", new IntTag(pt.getBlockZ())); + values.put("id", values.get("Id")); + values.remove("Id"); + values.remove("Pos"); + if (fixer.isActive()) { + tileEntity = ((CompoundTag) AdventureNBTConverter.fromAdventure(fixer.fixUp( + DataFixer.FixTypes.BLOCK_ENTITY, + new CompoundTag(values).asBinaryTag() + ))).getValue(); + } else { + tileEntity = values; + } + 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); + Map tileEntity = tileEntitiesMap.get(offsetPos); + if (tileEntity != null) { + clipboard.setBlock( + offsetPos, state.toBaseBlock(new CompoundTag(tileEntity)) + ); + } else { + clipboard.setBlock(offsetPos, state); + } + } catch (WorldEditException e) { + throw new IOException("Failed to load a block in the schematic", e); + } + } + } + + 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 IntArrayTag tag) throws IOException { + if (tag == null) { + return BlockVector3.ZERO; + } + int[] parts = tag.getValue(); + 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) throws IOException { + 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(); + entityTag = ((CompoundTag) AdventureNBTConverter.fromAdventure(fixer.fixUp( + DataFixer.FixTypes.ENTITY, + entityTag.asBinaryTag() + ))); + + 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); + if (positionIsRelative) { + location = location.setPosition( + location.toVector().add(clipboard.getMinimumPoint().toVector3()) + ); + } + clipboard.createEntity(location, state); + } else { + LOGGER.warn("Unknown entity when pasting schematic: " + id); + } + } + } + + 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..e3668a5f5 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV1Reader.java @@ -0,0 +1,155 @@ +/* + * 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.jnbt.ByteArrayTag; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.IntArrayTag; +import com.sk89q.jnbt.IntTag; +import com.sk89q.jnbt.ListTag; +import com.sk89q.jnbt.NBTInputStream; +import com.sk89q.jnbt.NamedTag; +import com.sk89q.jnbt.ShortTag; +import com.sk89q.jnbt.Tag; +import com.sk89q.worldedit.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.NBTSchematicReader; +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.world.block.BlockState; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.util.Map; +import java.util.OptionalInt; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Reads schematic files using the Sponge Schematic Specification (Version 1). + */ +public class SpongeSchematicV1Reader extends NBTSchematicReader { + + private final NBTInputStream inputStream; + + /** + * Create a new instance. + * + * @param inputStream the input stream to read from + */ + public SpongeSchematicV1Reader(NBTInputStream inputStream) { + checkNotNull(inputStream); + this.inputStream = inputStream; + } + + @Override + public Clipboard read() throws IOException { + CompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(1, getBaseTag()); + + 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 CompoundTag getBaseTag() throws IOException { + NamedTag rootTag = inputStream.readNamedTag(); + + return (CompoundTag) rootTag.getTag(); + } + + static BlockArrayClipboard readVersion1(CompoundTag schematicTag, VersionedDataFixer fixer) throws IOException { + Map schematic = schematicTag.getValue(); + + int width = requireTag(schematic, "Width", ShortTag.class).getValue() & 0xFFFF; + int height = requireTag(schematic, "Height", ShortTag.class).getValue() & 0xFFFF; + int length = requireTag(schematic, "Length", ShortTag.class).getValue() & 0xFFFF; + + BlockVector3 min = ReaderUtil.decodeBlockVector3( + getTag(schematic, "Offset", IntArrayTag.class) + ); + + BlockVector3 offset = BlockVector3.ZERO; + 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(); + offset = BlockVector3.at(offsetX, offsetY, offsetZ); + } + + BlockVector3 origin = min.subtract(offset); + Region region = new CuboidRegion(min, min.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 = ReaderUtil.decodePalette( + paletteObject, fixer + ); + + byte[] blocks = requireTag(schematic, "BlockData", ByteArrayTag.class).getValue(); + + ListTag tileEntities = getTag(schematic, "BlockEntities", ListTag.class); + if (tileEntities == null) { + tileEntities = getTag(schematic, "TileEntities", ListTag.class); + } + + BlockArrayClipboard clipboard = new BlockArrayClipboard(region); + clipboard.setOrigin(origin); + ReaderUtil.initializeClipboardFromBlocks( + clipboard, palette, blocks, tileEntities, fixer + ); + return clipboard; + } + + @Override + public void close() throws IOException { + inputStream.close(); + } +} 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..df20edd69 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV2Reader.java @@ -0,0 +1,174 @@ +/* + * 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.jnbt.ByteArrayTag; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.IntTag; +import com.sk89q.jnbt.ListTag; +import com.sk89q.jnbt.NBTInputStream; +import com.sk89q.jnbt.NamedTag; +import com.sk89q.jnbt.Tag; +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.NBTSchematicReader; +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.DataFixer; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.biome.BiomeTypes; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.util.HashMap; +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 (Version 2). + */ +public class SpongeSchematicV2Reader extends NBTSchematicReader { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private final NBTInputStream inputStream; + + /** + * Create a new instance. + * + * @param inputStream the input stream to read from + */ + public SpongeSchematicV2Reader(NBTInputStream inputStream) { + checkNotNull(inputStream); + this.inputStream = inputStream; + } + + @Override + public Clipboard read() throws IOException { + CompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(2, schematicTag); + + final Platform platform = WorldEdit.getInstance().getPlatformManager() + .queryCapability(Capability.WORLD_EDITING); + int liveDataVersion = platform.getDataVersion(); + + VersionedDataFixer fixer = ReaderUtil.getVersionedDataFixer( + schematicTag.getValue(), platform, liveDataVersion + ); + BlockArrayClipboard clip = SpongeSchematicV1Reader.readVersion1(schematicTag, fixer); + return readVersion2(clip, schematicTag, fixer); + } + + @Override + public OptionalInt getDataVersion() { + try { + CompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(2, schematicTag); + + int dataVersion = requireTag(schematicTag.getValue(), "DataVersion", IntTag.class) + .getValue(); + if (dataVersion < 0) { + return OptionalInt.empty(); + } + return OptionalInt.of(dataVersion); + } catch (IOException e) { + return OptionalInt.empty(); + } + } + + private CompoundTag getBaseTag() throws IOException { + NamedTag rootTag = inputStream.readNamedTag(); + + return (CompoundTag) rootTag.getTag(); + } + + private Clipboard readVersion2(BlockArrayClipboard version1, CompoundTag schematicTag, + VersionedDataFixer fixer) throws IOException { + Map schematic = schematicTag.getValue(); + if (schematic.containsKey("BiomeData")) { + readBiomes2(version1, schematic, fixer); + } + ListTag entities = getTag(schematic, "Entities", ListTag.class); + if (entities != null) { + ReaderUtil.readEntities( + version1, entities.getValue(), fixer, false + ); + } + return version1; + } + + private void readBiomes2(BlockArrayClipboard clipboard, Map schematic, + VersionedDataFixer fixer) 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(); + 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?"); + } + 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().getX(); + + byte[] biomes = dataTag.getValue(); + 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.getX(); + int z = hackDecode.getY(); + for (int y = 0; y < clipboard.getRegion().getHeight(); y++) { + clipboard.setBiome(min.add(x, y, z), type); + } + } + } + + @Override + public void close() throws IOException { + inputStream.close(); + } +} 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 85% 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 24860dd3c..3a85af5cf 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,10 +17,10 @@ * 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.google.common.collect.ImmutableMap; import com.sk89q.jnbt.ByteArrayTag; import com.sk89q.jnbt.CompoundTag; import com.sk89q.jnbt.IntArrayTag; @@ -31,12 +31,12 @@ 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; @@ -46,20 +46,18 @@ 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. + * Writes schematic files using the Sponge Schematic Specification (Version 2). * * @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. */ @Deprecated -public class SpongeSchematicWriter implements ClipboardWriter { +public class SpongeSchematicV2Writer implements ClipboardWriter { private static final int CURRENT_VERSION = 2; @@ -71,7 +69,7 @@ public class SpongeSchematicWriter implements ClipboardWriter { * * @param outputStream the output stream to write to */ - public SpongeSchematicWriter(NBTOutputStream outputStream) { + public SpongeSchematicV2Writer(NBTOutputStream outputStream) { checkNotNull(outputStream); this.outputStream = outputStream; } @@ -122,6 +120,24 @@ public class SpongeSchematicWriter implements ClipboardWriter { metadata.put("WEOffsetZ", new IntTag(offset.z())); metadata.put("FAWEVersion", new IntTag(Fawe.instance().getVersion().build)); + Map worldEditSection = new HashMap<>(); + worldEditSection.put("Version", new StringTag(WorldEdit.getVersion())); + worldEditSection.put("EditingPlatform", new StringTag(WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getId())); + worldEditSection.put("Offset", new IntArrayTag(new int[] { + offset.getBlockX(), offset.getBlockY(), offset.getBlockZ() + })); + + Map platformsSection = new HashMap<>(); + for (Platform platform : WorldEdit.getInstance().getPlatformManager().getPlatforms()) { + platformsSection.put(platform.getId(), new CompoundTag(ImmutableMap.of( + "Name", new StringTag(platform.getPlatformName()), + "Version", new StringTag(platform.getPlatformVersion()) + ))); + } + worldEditSection.put("Platforms", new CompoundTag(platformsSection)); + + metadata.put("WorldEdit", new CompoundTag(worldEditSection)); + schematic.put("Metadata", new CompoundTag(metadata)); schematic.put("Width", new ShortTag((short) width)); @@ -200,7 +216,10 @@ public class SpongeSchematicWriter implements ClipboardWriter { } if (!clipboard.getEntities().isEmpty()) { - writeEntities(clipboard, schematic); + ListTag value = WriterUtil.encodeEntities(clipboard, false); + if (value != null) { + schematic.put("Entities", value); + } } return schematic; @@ -250,31 +269,6 @@ public class SpongeSchematicWriter implements ClipboardWriter { 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().getId())); - 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)); - } - @Override public void close() throws IOException { outputStream.close(); 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..b4ea21269 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Reader.java @@ -0,0 +1,215 @@ +/* + * 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.jnbt.ByteArrayTag; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.IntArrayTag; +import com.sk89q.jnbt.IntTag; +import com.sk89q.jnbt.ListTag; +import com.sk89q.jnbt.NBTInputStream; +import com.sk89q.jnbt.NamedTag; +import com.sk89q.jnbt.ShortTag; +import com.sk89q.jnbt.Tag; +import com.sk89q.worldedit.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.NBTSchematicReader; +import com.sk89q.worldedit.extent.clipboard.io.SchematicNbtUtil; +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.DataFixer; +import com.sk89q.worldedit.world.biome.BiomeType; +import com.sk89q.worldedit.world.biome.BiomeTypes; +import com.sk89q.worldedit.world.block.BlockState; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.util.HashMap; +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. + */ +public class SpongeSchematicV3Reader extends NBTSchematicReader { + + private static final Logger LOGGER = LogManagerCompat.getLogger(); + + private final NBTInputStream inputStream; + + /** + * Create a new instance. + * + * @param inputStream the input stream to read from + */ + public SpongeSchematicV3Reader(NBTInputStream inputStream) { + checkNotNull(inputStream); + this.inputStream = inputStream; + } + + @Override + public Clipboard read() throws IOException { + CompoundTag 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.getValue(), platform, liveDataVersion + ); + return readVersion3(schematicTag, fixer); + } + + @Override + public OptionalInt getDataVersion() { + try { + CompoundTag schematicTag = getBaseTag(); + ReaderUtil.checkSchematicVersion(3, schematicTag); + + int dataVersion = requireTag(schematicTag.getValue(), "DataVersion", IntTag.class) + .getValue(); + if (dataVersion < 0) { + return OptionalInt.empty(); + } + return OptionalInt.of(dataVersion); + } catch (IOException e) { + return OptionalInt.empty(); + } + } + + private CompoundTag getBaseTag() throws IOException { + NamedTag rootTag = inputStream.readNamedTag(); + CompoundTag schematicTag = (CompoundTag) rootTag.getTag(); + // Nested inside the root tag + return requireTag(schematicTag.getValue(), "Schematic", CompoundTag.class); + } + + private Clipboard readVersion3(CompoundTag schematicTag, VersionedDataFixer fixer) throws IOException { + Map schematic = schematicTag.getValue(); + + int width = requireTag(schematic, "Width", ShortTag.class).getValue() & 0xFFFF; + int height = requireTag(schematic, "Height", ShortTag.class).getValue() & 0xFFFF; + int length = requireTag(schematic, "Length", ShortTag.class).getValue() & 0xFFFF; + + BlockVector3 offset = ReaderUtil.decodeBlockVector3( + SchematicNbtUtil.getTag(schematic, "Offset", IntArrayTag.class) + ); + + BlockVector3 origin = BlockVector3.ZERO; + CompoundTag metadataTag = getTag(schematic, "Metadata", CompoundTag.class); + if (metadataTag != null && metadataTag.containsKey("WorldEdit")) { + // We appear to have WorldEdit Metadata + Map worldedit = + requireTag(metadataTag.getValue(), "WorldEdit", CompoundTag.class).getValue(); + origin = ReaderUtil.decodeBlockVector3( + SchematicNbtUtil.getTag(worldedit, "Origin", IntArrayTag.class) + ); + } + 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, schematic, clipboard); + + CompoundTag biomeContainer = getTag(schematic, "Biomes", CompoundTag.class); + if (biomeContainer != null) { + readBiomes3(clipboard, biomeContainer.getValue(), fixer); + } + + ListTag entities = getTag(schematic, "Entities", ListTag.class); + if (entities != null) { + ReaderUtil.readEntities(clipboard, entities.getValue(), fixer, true); + } + + return clipboard; + } + + private void decodeBlocksIntoClipboard(VersionedDataFixer fixer, Map schematic, + BlockArrayClipboard clipboard) throws IOException { + Map blockContainer = requireTag(schematic, "Blocks", CompoundTag.class).getValue(); + + Map paletteObject = requireTag(blockContainer, "Palette", CompoundTag.class).getValue(); + Map palette = ReaderUtil.decodePalette( + paletteObject, fixer + ); + + byte[] blocks = requireTag(blockContainer, "Data", ByteArrayTag.class).getValue(); + ListTag tileEntities = getTag(blockContainer, "BlockEntities", ListTag.class); + + ReaderUtil.initializeClipboardFromBlocks( + clipboard, palette, blocks, tileEntities, fixer + ); + } + + private void readBiomes3(BlockArrayClipboard clipboard, Map biomeContainer, + VersionedDataFixer fixer) throws IOException { + CompoundTag paletteTag = requireTag(biomeContainer, "Palette", CompoundTag.class); + + Map palette = new HashMap<>(); + + for (Entry palettePart : paletteTag.getValue().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?"); + } + 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.getRegion().getWidth(); + int length = clipboard.getRegion().getLength(); + + byte[] biomes = requireTag(biomeContainer, "Data", ByteArrayTag.class).getValue(); + 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 { + inputStream.close(); + } +} 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..39f4fee39 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/SpongeSchematicV3Writer.java @@ -0,0 +1,258 @@ +/* + * 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.google.common.collect.ImmutableMap; +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.LongTag; +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.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 java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * 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 NBTOutputStream outputStream; + + /** + * Create a new schematic writer. + * + * @param outputStream the output stream to write to + */ + public SpongeSchematicV3Writer(NBTOutputStream outputStream) { + checkNotNull(outputStream); + this.outputStream = outputStream; + } + + @Override + public void write(Clipboard clipboard) throws IOException { + // For now always write the latest version. Maybe provide support for earlier if more appear. + outputStream.writeNamedTag("", + new CompoundTag(ImmutableMap.of("Schematic", new CompoundTag(write3(clipboard)))) + ); + } + + /** + * Writes a version 3 schematic file. + * + * @param clipboard The clipboard + * @return The schematic map + */ + private Map 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"); + } + + Map schematic = new HashMap<>(); + schematic.put("Version", new IntTag(CURRENT_VERSION)); + schematic.put("DataVersion", new IntTag( + WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getDataVersion())); + + Map metadata = new HashMap<>(); + metadata.put("Date", new LongTag(System.currentTimeMillis())); + + Map worldEditSection = new HashMap<>(); + worldEditSection.put("Version", new StringTag(WorldEdit.getVersion())); + worldEditSection.put("EditingPlatform", new StringTag(WorldEdit.getInstance().getPlatformManager().queryCapability(Capability.WORLD_EDITING).getId())); + worldEditSection.put("Origin", new IntArrayTag(new int[] { + origin.getBlockX(), origin.getBlockY(), origin.getBlockZ() + })); + + Map platformsSection = new HashMap<>(); + for (Platform platform : WorldEdit.getInstance().getPlatformManager().getPlatforms()) { + platformsSection.put(platform.getId(), new CompoundTag(ImmutableMap.of( + "Name", new StringTag(platform.getPlatformName()), + "Version", new StringTag(platform.getPlatformVersion()) + ))); + } + worldEditSection.put("Platforms", new CompoundTag(platformsSection)); + + metadata.put("WorldEdit", new CompoundTag(worldEditSection)); + + schematic.put("Metadata", new CompoundTag(metadata)); + + schematic.put("Width", new ShortTag((short) width)); + schematic.put("Height", new ShortTag((short) height)); + schematic.put("Length", new ShortTag((short) length)); + + schematic.put("Offset", new IntArrayTag(new int[] { + offset.getBlockX(), + offset.getBlockY(), + offset.getBlockZ(), + })); + + schematic.put("Blocks", encodeBlocks(clipboard)); + + if (clipboard.hasBiomes()) { + schematic.put("Biomes", encodeBiomes(clipboard)); + } + + if (!clipboard.getEntities().isEmpty()) { + ListTag value = WriterUtil.encodeEntities(clipboard, true); + if (value != null) { + schematic.put("Entities", value); + } + } + + return schematic; + } + + private static final class PaletteMap { + private final Map contents = new LinkedHashMap<>(); + private int nextId = 0; + + public int getId(String key) { + Integer result = contents.get(key); + if (result != null) { + return result; + } + int newValue = nextId; + nextId++; + contents.put(key, newValue); + return newValue; + } + + public CompoundTag toNbt() { + return new CompoundTag(ImmutableMap.copyOf(Maps.transformValues( + contents, IntTag::new + ))); + } + } + + private CompoundTag encodeBlocks(Clipboard clipboard) { + List blockEntities = new ArrayList<>(); + CompoundTag result = encodePalettedData(clipboard, point -> { + BaseBlock block = clipboard.getFullBlock(point); + // Also compute block entity side-effect here + if (block.getNbtData() != null) { + Map values = new HashMap<>(block.getNbtData().getValue()); + + values.remove("id"); // Remove 'id' if it exists. We want 'Id' + + // Positions are kept in NBT, we don't want that. + values.remove("x"); + values.remove("y"); + values.remove("z"); + + values.put("Id", new StringTag(block.getNbtId())); + BlockVector3 adjustedPos = point.subtract(clipboard.getMinimumPoint()); + values.put("Pos", new IntArrayTag(new int[] { + adjustedPos.getBlockX(), + adjustedPos.getBlockY(), + adjustedPos.getBlockZ() + })); + + blockEntities.add(new CompoundTag(values)); + } + return block.toImmutableState().getAsString(); + }); + + return result.createBuilder() + .put("BlockEntities", new ListTag(CompoundTag.class, blockEntities)) + .build(); + } + + private CompoundTag encodeBiomes(Clipboard clipboard) { + return encodePalettedData(clipboard, point -> clipboard.getBiome(point).getId()); + } + + private CompoundTag 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 new CompoundTag(ImmutableMap.of( + "Palette", paletteMap.toNbt(), + "Data", new ByteArrayTag(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..7e6b22683 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/VersionedDataFixer.java @@ -0,0 +1,28 @@ +package com.sk89q.worldedit.extent.clipboard.io.sponge; + + +import com.sk89q.worldedit.world.DataFixer; + +import javax.annotation.Nullable; + +final class VersionedDataFixer { + private final int dataVersion; + @Nullable + private final DataFixer fixer; + + VersionedDataFixer(int dataVersion, @Nullable DataFixer fixer) { + 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..43efc0816 --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/WriterUtil.java @@ -0,0 +1,65 @@ +package com.sk89q.worldedit.extent.clipboard.io.sponge; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Maps; +import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.DoubleTag; +import com.sk89q.jnbt.FloatTag; +import com.sk89q.jnbt.ListTag; +import com.sk89q.jnbt.StringTag; +import com.sk89q.jnbt.Tag; +import com.sk89q.worldedit.entity.BaseEntity; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.math.Vector3; +import com.sk89q.worldedit.util.Location; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +class WriterUtil { + static ListTag encodeEntities(Clipboard clipboard, boolean positionIsRelative) { + 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().getId())); + final Location location = e.getLocation(); + Vector3 pos = location.toVector(); + if (positionIsRelative) { + pos = pos.subtract(clipboard.getMinimumPoint().toVector3()); + } + values.put("Pos", encodeVector(pos)); + values.put("Rotation", encodeRotation(location)); + + return new CompoundTag(values); + }).filter(Objects::nonNull).collect(Collectors.toList()); + if (entities.isEmpty()) { + return null; + } + return new ListTag(CompoundTag.class, entities); + } + + static Tag encodeVector(Vector3 vector) { + return new ListTag(DoubleTag.class, ImmutableList.of( + new DoubleTag(vector.getX()), + new DoubleTag(vector.getY()), + new DoubleTag(vector.getZ()) + )); + } + + static Tag encodeRotation(Location location) { + return new ListTag(FloatTag.class, ImmutableList.of( + new FloatTag(location.getYaw()), + new FloatTag(location.getPitch()) + )); + } +} 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..9f3c0d60b --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/sponge/package-info.java @@ -0,0 +1,7 @@ +/** + * 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; + } +}