From 3839f2547df422d448d0bbcfd1c9aff821755ab5 Mon Sep 17 00:00:00 2001 From: wizjany Date: Thu, 26 Nov 2020 23:19:26 -0500 Subject: [PATCH] Add structure reading as a clipboard format. --- .../clipboard/io/BuiltInClipboardFormat.java | 33 +++ .../io/MinecraftStructureReader.java | 196 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100644 worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MinecraftStructureReader.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 379c5e5190..03ddb7c992 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 @@ -119,6 +119,39 @@ public boolean isFormat(File file) { return false; } + return true; + } + }, + MINECRAFT_STRUCTURE("structure") { + @Override + public String getPrimaryFileExtension() { + return "nbt"; + } + + @Override + public ClipboardReader getReader(InputStream inputStream) throws IOException { + NBTInputStream nbtStream = new NBTInputStream(new GZIPInputStream(inputStream)); + return new MinecraftStructureReader(nbtStream); + } + + @Override + public ClipboardWriter getWriter(OutputStream outputStream) throws IOException { + throw new IOException("This format does not support saving"); + } + + @Override + public boolean isFormat(File file) { + try (NBTInputStream str = new NBTInputStream(new GZIPInputStream(new FileInputStream(file)))) { + NamedTag rootTag = str.readNamedTag(); + CompoundTag structureTag = (CompoundTag) rootTag.getTag(); + Map structure = structureTag.getValue(); + if (!structure.containsKey("DataVersion")) { + return false; + } + } catch (Exception e) { + return false; + } + return true; } }; diff --git a/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MinecraftStructureReader.java b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MinecraftStructureReader.java new file mode 100644 index 0000000000..be9b31b6fb --- /dev/null +++ b/worldedit-core/src/main/java/com/sk89q/worldedit/extent/clipboard/io/MinecraftStructureReader.java @@ -0,0 +1,196 @@ +/* + * 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.google.common.collect.Maps; +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.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.extent.clipboard.BlockArrayClipboard; +import com.sk89q.worldedit.extent.clipboard.Clipboard; +import com.sk89q.worldedit.math.BlockVector3; +import com.sk89q.worldedit.regions.CuboidRegion; +import com.sk89q.worldedit.util.Location; +import com.sk89q.worldedit.world.block.BaseBlock; +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 java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import static com.google.common.base.Preconditions.checkNotNull; + +/** + * Reads Mincraft structure files. + * {@link "https://minecraft.gamepedia.com/Structure_block_file_format"} + */ +public class MinecraftStructureReader extends NBTSchematicReader { + private static final Logger log = Logger.getLogger(MinecraftStructureReader.class.getCanonicalName()); + private final NBTInputStream inputStream; + + /** + * Create a new instance. + * + * @param inputStream the input stream to read from + */ + public MinecraftStructureReader(NBTInputStream inputStream) { + checkNotNull(inputStream); + this.inputStream = inputStream; + } + + @Override + public Clipboard read() throws IOException { + NamedTag rootTag = inputStream.readNamedTag(); + + // MC structures are all unnamed, but this doesn't seem to be necessary? might remove this later +// if (!rootTag.getName().isEmpty()) { +// throw new IOException("Root tag has name - are you sure this is a structure?"); +// } + + CompoundTag structureTag = (CompoundTag) rootTag.getTag(); + Map structure = structureTag.getValue(); + int version = requireTag(structure, "DataVersion", IntTag.class).getValue(); + + // TODO DFU + + List size = requireTag(structure, "size", ListTag.class).getValue(); + if (size.size() != 3) { + throw new IOException("Invalid size list tag in structure."); + } + int width = ((IntTag) size.get(0)).getValue(); + int height = ((IntTag) size.get(1)).getValue(); + int length = ((IntTag) size.get(2)).getValue(); + + if (structure.containsKey("palettes")) { + throw new IOException("Structures with multiple palettes are not currently supported."); + } + List paletteObject = (List) (List) requireTag(structure, "palette", ListTag.class).getValue(); + Map palette = readPalette(paletteObject); + + return getClipboardWithPalette(structure, palette, width, height, length); + } + + private BlockArrayClipboard getClipboardWithPalette(Map structure, Map palette, + int width, int height, int length) throws IOException { + BlockArrayClipboard clipboard = new BlockArrayClipboard( + new CuboidRegion(BlockVector3.ZERO, BlockVector3.at(width, height, length).subtract(BlockVector3.ONE))); + + for (Tag blockTag : requireTag(structure, "blocks", ListTag.class).getValue()) { + CompoundTag block = (CompoundTag) blockTag; + ListTag pos = requireTag(block.getValue(), "pos", ListTag.class); + BlockVector3 vec = BlockVector3.at(pos.getInt(0), pos.getInt(1), pos.getInt(2)); + int state = requireTag(block.getValue(), "state", IntTag.class).getValue(); + BlockState blockState = palette.get(state); + CompoundTag nbt = getTag(block.getValue(), "nbt", CompoundTag.class); + try { + if (nbt == null) { + clipboard.setBlock(vec, blockState); + } else { + Map values = Maps.newHashMap(nbt.getValue()); + values.put("x", new IntTag(vec.getBlockX())); + values.put("y", new IntTag(vec.getBlockY())); + values.put("z", new IntTag(vec.getBlockZ())); + BaseBlock baseBlock = blockState.toBaseBlock(nbt); + clipboard.setBlock(vec, baseBlock); + } + } catch (WorldEditException e) { + throw new IOException("Failed to load a block in the schematic"); + } + } + + List entityTags = requireTag(structure, "entities", ListTag.class).getValue(); + for (Tag tag : entityTags) { + CompoundTag compound = (CompoundTag) tag; + CompoundTag nbt = requireTag(compound.getValue(), "nbt", CompoundTag.class); + Location location = NBTConversions.toLocation(clipboard, compound.getListTag("pos"), + nbt.getListTag("Rotation")); + String id = requireTag(nbt.getValue(), "id", StringTag.class).getValue(); + + if (!id.isEmpty()) { + EntityType entityType = EntityTypes.get(id.toLowerCase()); + if (entityType != null) { + BaseEntity state = new BaseEntity(entityType, compound); + clipboard.createEntity(location, state); + } else { + log.warning("Unknown entity when loading structure: " + id.toLowerCase()); + } + } + } + return clipboard; + } + + private Map readPalette(List paletteTag) throws IOException { + Map palette = new HashMap<>(); + ParserContext parserContext = new ParserContext(); + parserContext.setRestricted(false); + parserContext.setTryLegacy(false); + parserContext.setPreferringWildcard(false); + + for (int i = 0; i < paletteTag.size(); i++) { + CompoundTag palettePart = paletteTag.get(i); + String blockName = palettePart.getString("Name"); + if (blockName.isEmpty()) { + throw new IOException("Palette block name empty."); + } + StringBuilder stateBuilder = new StringBuilder(blockName); + Tag props = palettePart.getValue().getOrDefault("Properties", null); + if (props instanceof CompoundTag) { + CompoundTag properties = ((CompoundTag) props); + if (!properties.getValue().isEmpty()) { + stateBuilder.append('['); + stateBuilder.append(properties.getValue().entrySet().stream().map(e -> + e.getKey() + "=" + e.getValue().getValue()).collect(Collectors.joining(","))); + stateBuilder.append(']'); + } + } + BlockState state; + String stateString = stateBuilder.toString(); + try { + state = WorldEdit.getInstance().getBlockFactory().parseFromInput(stateString, parserContext).toImmutableState(); + } catch (InputParseException e) { + throw new IOException("Invalid BlockState in structure: " + stateString + + ". Are you missing a mod or using a structure made in a newer version of Minecraft?"); + } + palette.put(i, state); + } + return palette; + } + + @Override + public void close() throws IOException { + inputStream.close(); + } +} \ No newline at end of file