From 17fba54305770303b5bbbc6539ee895fc818969e Mon Sep 17 00:00:00 2001 From: wizjany Date: Wed, 3 Apr 2019 23:33:10 -0400 Subject: [PATCH] Update SpongeSchematic format to version 2. Allows saving and loading entities and biomes. --- .../bukkit/BukkitServerInterface.java | 6 + .../com/sk89q/jnbt/CompoundTagBuilder.java | 12 ++ .../extension/platform/Platform.java | 7 + .../extent/clipboard/BlockArrayClipboard.java | 5 +- .../clipboard/io/MCEditSchematicReader.java | 2 + .../clipboard/io/SpongeSchematicReader.java | 120 ++++++++++++++++- .../clipboard/io/SpongeSchematicWriter.java | 121 ++++++++++++++++-- .../world/storage/NBTConversions.java | 2 +- .../sk89q/worldedit/forge/ForgePlatform.java | 6 + .../worldedit/sponge/SpongePlatform.java | 6 + 10 files changed, 273 insertions(+), 14 deletions(-) diff --git a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java index adfc64bc0..0123b452b 100644 --- a/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java +++ b/worldedit-bukkit/src/main/java/com/sk89q/worldedit/bukkit/BukkitServerInterface.java @@ -65,6 +65,12 @@ public class BukkitServerInterface implements MultiUserPlatform { return BukkitRegistries.getInstance(); } + @Override + public int getDataVersion() { + // TODO - add to adapter - CraftMagicNumbers#getDataVersion + return 1631; + } + @Override public boolean isValidMobType(String type) { final EntityType entityType = EntityType.fromName(type); diff --git a/worldedit-core/src/main/java/com/sk89q/jnbt/CompoundTagBuilder.java b/worldedit-core/src/main/java/com/sk89q/jnbt/CompoundTagBuilder.java index b0e873c0d..6b5776619 100644 --- a/worldedit-core/src/main/java/com/sk89q/jnbt/CompoundTagBuilder.java +++ b/worldedit-core/src/main/java/com/sk89q/jnbt/CompoundTagBuilder.java @@ -181,6 +181,18 @@ public class CompoundTagBuilder { return put(key, new StringTag(value)); } + /** + * Remove the given key from the compound tag. Does nothing if the key doesn't exist. + * + * @param key the key + * @return this object + */ + public CompoundTagBuilder remove(String key) { + checkNotNull(key); + entries.remove(key); + return this; + } + /** * Put all the entries from the given map into this map. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java index 3b37a98b5..6878235d5 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extension/platform/Platform.java @@ -45,6 +45,13 @@ public interface Platform { */ Registries getRegistries(); + /** + * Gets the Minecraft data version being used by the platform. + * + * @return the data version + */ + int getDataVersion(); + /** * Checks if a mob type is valid. * diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java index e3cea6c89..029d25052 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/BlockArrayClipboard.java @@ -171,7 +171,10 @@ public class BlockArrayClipboard implements Clipboard { if (biomes != null && position.containedWithin(getMinimumPoint().toBlockVector2(), getMaximumPoint().toBlockVector2())) { BlockVector2 v = position.subtract(region.getMinimumPoint().toBlockVector2()); - return biomes[v.getBlockX()][v.getBlockZ()]; + BiomeType biomeType = biomes[v.getBlockX()][v.getBlockZ()]; + if (biomeType != null) { + return biomeType; + } } return BiomeTypes.OCEAN; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MCEditSchematicReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MCEditSchematicReader.java index dad694e66..a5e87c356 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MCEditSchematicReader.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MCEditSchematicReader.java @@ -53,6 +53,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -315,6 +316,7 @@ public class MCEditSchematicReader extends NBTSchematicReader { case "PigZombie": return "zombie_pigman"; default: return id; } + return id; } private String convertBlockEntityId(String id) { 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 index 09594e388..57d8582b2 100644 --- 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 @@ -28,18 +28,27 @@ 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.extent.clipboard.BlockArrayClipboard; import com.sk89q.worldedit.extent.clipboard.Clipboard; import com.sk89q.worldedit.extent.clipboard.io.legacycompat.NBTCompatibilityHandler; 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.world.biome.BiomeType; +import com.sk89q.worldedit.world.biome.BiomeTypes; import com.sk89q.worldedit.world.block.BlockState; +import com.sk89q.worldedit.world.entity.EntityType; +import com.sk89q.worldedit.world.entity.EntityTypes; +import com.sk89q.worldedit.world.storage.NBTConversions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -89,6 +98,15 @@ public class SpongeSchematicReader extends NBTSchematicReader { int version = requireTag(schematic, "Version", IntTag.class).getValue(); if (version == 1) { return readVersion1(schematicTag); + } else if (version == 2) { + int dataVersion = requireTag(schematic, "DataVersion", IntTag.class).getValue(); + if (dataVersion > WorldEdit.getInstance().getPlatformManager() + .queryCapability(Capability.WORLD_EDITING).getDataVersion()) { + // maybe should just warn? simple schematics may be compatible still. + throw new IOException("Schematic was made in a newer Minecraft version. Data may be incompatible."); + } + BlockArrayClipboard clip = readVersion1(schematicTag); + return readVersion2(clip, schematicTag); } throw new IOException("This schematic version is currently not supported"); } @@ -123,10 +141,11 @@ public class SpongeSchematicReader extends NBTSchematicReader { region = new CuboidRegion(origin, origin.add(width, height, length).subtract(BlockVector3.ONE)); } + // Note: these aren't actually required by the spec, but we require them I guess? int paletteMax = requireTag(schematic, "PaletteMax", IntTag.class).getValue(); Map paletteObject = requireTag(schematic, "Palette", CompoundTag.class).getValue(); if (paletteObject.size() != paletteMax) { - throw new IOException("Differing given palette size to actual size"); + throw new IOException("Block palette size does not match expected size."); } Map palette = new HashMap<>(); @@ -168,8 +187,8 @@ public class SpongeSchematicReader extends NBTSchematicReader { int index = 0; int i = 0; - int value = 0; - int varintLength = 0; + int value; + int varintLength; while (i < blocks.length) { value = 0; varintLength = 0; @@ -185,7 +204,7 @@ public class SpongeSchematicReader extends NBTSchematicReader { } i++; } - // index = (y * length + z) * width + x + // index = (y * length * width) + (z * width) + x int y = index / (width * length); int z = (index % (width * length)) / width; int x = (index % (width * length)) % width; @@ -219,6 +238,99 @@ public class SpongeSchematicReader extends NBTSchematicReader { 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); + // TODO for now, we just assume if biomedata is present, palette will be as well. + // atm the spec doesn't actually require palettes + 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."); + } + Map paletteEntries = paletteTag.getValue(); + + for (Map.Entry palettePart : paletteEntries.entrySet()) { + BiomeType biome = BiomeTypes.get(palettePart.getKey()); + if (biome == null) { + log.warn("Unknown biome type '" + palettePart.getKey() + "' 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(); + int biomeIndex = 0; + int biomeJ = 0; + int bVal; + int varIntLength; + while (biomeJ < biomes.length) { + bVal = 0; + varIntLength = 0; + + while (true) { + bVal |= (biomes[biomeJ] & 127) << (varIntLength++ * 7); + if (varIntLength > 5) { + throw new RuntimeException("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); + clipboard.setBiome(clipboard.getMinimumPoint().toBlockVector2().add(x, 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(); + + 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.createBuilder().putString("id", id).remove("Id").build()); + clipboard.createEntity(location, state); + } else { + log.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/SpongeSchematicWriter.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/SpongeSchematicWriter.java index 8237106f6..f9d9248a6 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/SpongeSchematicWriter.java @@ -21,8 +21,12 @@ package com.sk89q.worldedit.extent.clipboard.io; import static com.google.common.base.Preconditions.checkNotNull; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import com.sk89q.jnbt.ByteArrayTag; import com.sk89q.jnbt.CompoundTag; +import com.sk89q.jnbt.DoubleTag; +import com.sk89q.jnbt.FloatTag; import com.sk89q.jnbt.IntArrayTag; import com.sk89q.jnbt.IntTag; import com.sk89q.jnbt.ListTag; @@ -30,9 +34,16 @@ 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.extent.clipboard.Clipboard; +import com.sk89q.worldedit.math.BlockVector2; import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.math.Vector3; 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 java.io.ByteArrayOutputStream; @@ -41,12 +52,15 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * Writes schematic files using the Sponge schematic format. */ public class SpongeSchematicWriter 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; @@ -63,17 +77,17 @@ public class SpongeSchematicWriter implements ClipboardWriter { @Override public void write(Clipboard clipboard) throws IOException { // For now always write the latest version. Maybe provide support for earlier if more appear. - outputStream.writeNamedTag("Schematic", new CompoundTag(write1(clipboard))); + outputStream.writeNamedTag("Schematic", new CompoundTag(write2(clipboard))); } /** - * Writes a version 1 schematic file. + * Writes a version 2 schematic file. * * @param clipboard The clipboard * @return The schematic map * @throws IOException If an error occurs */ - private Map write1(Clipboard clipboard) throws IOException { + private Map write2(Clipboard clipboard) throws IOException { Region region = clipboard.getRegion(); BlockVector3 origin = clipboard.getOrigin(); BlockVector3 min = region.getMinimumPoint(); @@ -93,7 +107,9 @@ public class SpongeSchematicWriter implements ClipboardWriter { } Map schematic = new HashMap<>(); - schematic.put("Version", new IntTag(1)); + 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("WEOffsetX", new IntTag(offset.getBlockX())); @@ -129,10 +145,7 @@ public class SpongeSchematicWriter implements ClipboardWriter { BlockVector3 point = BlockVector3.at(x0, y0, z0); BaseBlock block = clipboard.getFullBlock(point); if (block.getNbtData() != null) { - Map values = new HashMap<>(); - for (Map.Entry entry : block.getNbtData().getValue().entrySet()) { - values.put(entry.getKey(), entry.getValue()); - } + Map values = new HashMap<>(block.getNbtData().getValue()); values.remove("id"); // Remove 'id' if it exists. We want 'Id' @@ -179,9 +192,101 @@ public class SpongeSchematicWriter implements ClipboardWriter { schematic.put("BlockData", new ByteArrayTag(buffer.toByteArray())); schematic.put("TileEntities", new ListTag(CompoundTag.class, tileEntities)); + // version 2 stuff + if (clipboard.hasBiomes()) { + writeBiomes(clipboard, schematic); + } + + if (!clipboard.getEntities().isEmpty()) { + writeEntities(clipboard, schematic); + } + return schematic; } + private void writeBiomes(Clipboard clipboard, Map schematic) { + BlockVector3 min = clipboard.getMinimumPoint(); + int width = clipboard.getRegion().getWidth(); + int length = clipboard.getRegion().getLength(); + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(width * length); + + int paletteMax = 0; + Map palette = new HashMap<>(); + + for (int z = 0; z < length; z++) { + int z0 = min.getBlockZ() + z; + for (int x = 0; x < width; x++) { + int x0 = min.getBlockX() + x; + BlockVector2 pt = BlockVector2.at(x0, z0); + BiomeType biome = clipboard.getBiome(pt); + + String biomeKey = biome.getId(); + int biomeId; + if (palette.containsKey(biomeKey)) { + biomeId = palette.get(biomeKey); + } else { + biomeId = paletteMax; + palette.put(biomeKey, biomeId); + paletteMax++; + } + + while ((biomeId & -128) != 0) { + buffer.write(biomeId & 127 | 128); + biomeId >>>= 7; + } + buffer.write(biomeId); + } + } + + schematic.put("BiomePaletteMax", new IntTag(paletteMax)); + + Map paletteTag = new HashMap<>(); + palette.forEach((key, value) -> paletteTag.put(key, new IntTag(value))); + + 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().getId())); + values.put("Pos", writeVector(e.getLocation().toVector())); + values.put("Rotation", writeRotation(e.getLocation())); + + return new CompoundTag(values); + }).filter(e -> e != null).collect(Collectors.toList()); + if (entities.isEmpty()) { + return; + } + schematic.put("Entities", new ListTag(CompoundTag.class, entities)); + } + + private Tag writeVector(Vector3 vector) { + List list = new ArrayList(); + list.add(new DoubleTag(vector.getX())); + list.add(new DoubleTag(vector.getY())); + list.add(new DoubleTag(vector.getZ())); + return new ListTag(DoubleTag.class, list); + } + + private Tag writeRotation(Location location) { + List list = new ArrayList(); + list.add(new FloatTag(location.getYaw())); + list.add(new FloatTag(location.getPitch())); + return new ListTag(FloatTag.class, list); + } + @Override public void close() throws IOException { outputStream.close(); diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/NBTConversions.java b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/NBTConversions.java index 6b61bddf9..3da5c0047 100644 --- a/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/NBTConversions.java +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/world/storage/NBTConversions.java @@ -52,7 +52,7 @@ public final class NBTConversions { return new Location( extent, positionTag.asDouble(0), positionTag.asDouble(1), positionTag.asDouble(2), - (float) directionTag.asDouble(0), (float) directionTag.asDouble(1)); + directionTag.getFloat(0), directionTag.getFloat(1)); } } diff --git a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java index 0530acc54..d578c3b9f 100644 --- a/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java +++ b/worldedit-forge/src/main/java/com/sk89q/worldedit/forge/ForgePlatform.java @@ -64,6 +64,12 @@ class ForgePlatform extends AbstractPlatform implements MultiUserPlatform { return ForgeRegistries.getInstance(); } + @Override + public int getDataVersion() { + // TODO technically available as WorldInfo#field_209227_p but requires a world ref? + return 1631; + } + @Override public boolean isValidMobType(String type) { return net.minecraftforge.registries.ForgeRegistries.ENTITIES.containsKey(new ResourceLocation(type)); diff --git a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java index 774347645..33a4395e0 100644 --- a/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java +++ b/worldedit-sponge/src/main/java/com/sk89q/worldedit/sponge/SpongePlatform.java @@ -68,6 +68,12 @@ class SpongePlatform extends AbstractPlatform implements MultiUserPlatform { return SpongeRegistries.getInstance(); } + @Override + public int getDataVersion() { + // TODO add to adapter - org.spongepowered.common.data.util.DataUtil#MINECRAFT_DATA_VERSION + return 1631; + } + @Override public boolean isValidMobType(String type) { return Sponge.getRegistry().getType(EntityType.class, type).isPresent();