Solved Schematic Parser having trouble with certain block types (1.12)

Discussion in 'Spigot Plugin Development' started by CrypticCabub, Jun 27, 2018.

  1. Hi fellow developers! I'm having some trouble with my schematic parser in that it is not pasting certain blocks.

    I have not done a full a comprehensive test against all blockIDs yet but do have some useful information. blocks confirmed to not be working are Prismarine (168), Sea Lantern (169), and Grass Path (208).

    My first assumption would be that the block id's are outside the range supported by byte (since java uses signed bytes) however the .schematic specification here (https://minecraft.gamepedia.com/Schematic_file_format) defines the block information as a byte[] so using a short[] seems out of the question.

    For those of you familiar with this format, is there any advice you could give for getting support for these blocks? I am trying to keep my class compatible with the "official" .schematic format should I ever need to use it for WorldEdit/MCEdit schematic files.

    The Schematic class is below (I am using JNBT for NBT parsing):
    Code (Text):
    package com.gmail.crypticcabub.minigamebase.schematics;


    import com.gmail.crypticcabub.minigamebase.objects.Region;
    import com.gmail.crypticcabub.minigamebase.utils.Validate;
    import org.bukkit.Location;
    import org.bukkit.block.Block;
    import org.bukkit.util.Vector;
    import org.jnbt.*;

    import java.io.File;
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.*;

    public class Schematic implements Iterable<Schematic.SchematicBlock> {
        /*
        .schematic implementation information:
            https://minecraft-el.gamepedia.com/Schematic_file_format


        array coordinate structure is [y][z][x]
         */


        public class SchematicIterator implements Iterator<SchematicBlock> {

            private SchematicBlock last;

            /**
             * Returns {@code true} if the iteration has more elements.
             * (In other words, returns {@code true} if {@link #next} would
             * return an element rather than throwing an exception.)
             *
             * @return {@code true} if the iteration has more elements
             */
            @Override
            public boolean hasNext() {
                if(last == null) {
                    return blocks.length > 0;
                } else {
                    return last.index + 1 < blocks.length;
                }
            }

            /**
             * Returns the next element in the iteration.
             *
             * @return the next element in the iteration
             * @throws NoSuchElementException if the iteration has no more elements
             */
            @Override
            public SchematicBlock next() {
                if(!hasNext()) {
                    throw new NoSuchElementException();
                }

                if(last == null) {
                    last = new SchematicBlock(0, 0, 0, 0);
                } else {
                    int index = last.index + 1;
                    Vector v = positionOf(index);
                    last = new SchematicBlock(index, v.getBlockY(), v.getBlockZ(), v.getBlockX());
                }

                return last;
            }
        }

        public class SchematicBlock {
            protected final int index;
            protected final int y;
            protected final int z;
            protected final int x;

            protected SchematicBlock(int index, int y, int z, int x) {
                this.index = index;
                this.y = y;
                this.z = z;
                this.x = x;
            }

            public byte blockID() {
                return blocks[index];
            }

            public byte data() {
                return data[index];
            }

            public Vector getRelativeLocation() {
                return new Vector(x, y, z);
            }

        }

        //todo: add support for entities and tile entities

        public short height; //y
        public short length; //z
        public short width; //x

        private byte[] blocks;
        private byte[] data;

        public static Schematic from(Region region) {
            Schematic schem = new Schematic();
            schem.schematicize(region);
            return schem;
        }

        public static Schematic load(File schematicFile) throws IOException {
            Schematic schem = new Schematic();
            schem.loadSchematicNative(schematicFile);
            return schem;
        }

        public static Schematic from(CompoundTag nbtTag) {
            Schematic schem = new Schematic();
            schem.loadFrom(nbtTag);
            return schem;
        }

        /**
         * get the index of a given relative location within the byte arrays as defined by the .schematic specification
         */
        public int indexOf(int x, int y, int z) {
            return (y * length + z) * width + x;
        }

        /**
         * @return the index of a given relative location within the byte arrays as defined by the .schematic specification
         */
        public int indexOf(Vector v) {
            return indexOf(v.getBlockX(), v.getBlockY(), v.getBlockZ());
        }

        /**
         * get the relative x,y,z position of a block at the given index within this schematic
         */
        public Vector positionOf(int index) {
            /*
            each y segment contains {length} z segments which each contain {width} x segments thus making the size of
            each y segment (length * width).

            y may be found by determinign which y segment we are currently in using integer math, z may be found likewise
            relative to a given y segment, and x is the exact position within the z segment
             */
            int y = index / (length * width);
            int yMod = index % (length * width); //how far into this y level are we?
            int z = yMod / width;
            int x = yMod % width;

            return new Vector(x, y, z);
        }

        /**
         * read the contents of the given region into this schematic
         */
        private void schematicize(Region region) {
            Location minCorner = region.getMinCorner();
            Location maxCorner = region.getMaxCorner();

            height = (short) (maxCorner.getBlockY() - minCorner.getBlockY() + 1);
            length = (short) (maxCorner.getBlockZ() - minCorner.getBlockZ() + 1);
            width = (short) (maxCorner.getBlockX() - minCorner.getBlockX() + 1);


            Block referenceBlock = minCorner.getBlock();

            int arrayLength = (height) * (length) * (width);
            blocks = new byte[arrayLength];
            data = new byte[arrayLength];

            int i = 0;

            for(int y = 0; y < height; y++) {
                for(int z = 0; z < length; z++) {
                    for(int x = 0; x < width; x++) {
                        Block b = referenceBlock.getRelative(x, y, z);
                        blocks[i] = (byte) b.getTypeId();
                        data[i] = b.getData();
                        i++;
                    }
                }
            }

        }

        private void loadFrom(CompoundTag schematicTag) {
            Map<String, Tag> schematic = schematicTag.getValue();
            if (!schematic.containsKey("Blocks")) {
                throw new IllegalArgumentException("Schematic is missing a \"Blocks\" tag");
            }

            width = getChildTag(schematic, "Width", ShortTag.class).getValue();
            length = getChildTag(schematic, "Length", ShortTag.class).getValue();
            height = getChildTag(schematic, "Height", ShortTag.class).getValue();


            //"classic" schematics are from incredibly old schematics of original minecraft. Alpha is everything after that
            String materials = getChildTag(schematic, "Materials", StringTag.class).getValue();
            if (!materials.equals("Alpha")) {
                throw new IllegalArgumentException("Schematic is not an Alpha schematic");
            }

            blocks = getChildTag(schematic, "Blocks", ByteArrayTag.class).getValue();
            data = getChildTag(schematic, "Data", ByteArrayTag.class).getValue();
            //entities = getChildTag(schematic, "Entities", ListTag.class).getValue();
            //tileEntities = getChildTag(schematic, "tileEntities", ListTag.class).getValue();
        }




        /**
         * save the schematic to the given .schematic file
         * <p>
         *     .schematic suffix is not enforced but should always be included to indicate that the file follows the
         *     .schematic specification
         * </p>
         * @param file the file to save to
         */
        public void save(File file) throws IOException {
            if(!file.exists()) {
                file.createNewFile();
            }
            FileOutputStream stream = new FileOutputStream(file);
            NBTOutputStream out = new NBTOutputStream(stream);
            out.writeTag(toCompoundTag("Schematic"));
            out.close();
        }

        /**
         * load schematic information from a .schematic file
         * @param file the file to load from
         * @throws IOException if an issue exists with the file
         * @throws IllegalArgumentException if the file does not contain the correct information
         */
        private void loadSchematicNative(File file) throws IOException {
            FileInputStream stream = new FileInputStream(file);
            NBTInputStream nbtStream = new NBTInputStream(stream);

            //get the root schematic tag from which all children are the schematic's information
            CompoundTag schematicTag = (CompoundTag) nbtStream.readTag();
            nbtStream.close();
            if (!schematicTag.getName().equals("Schematic")) {
                throw new IllegalArgumentException("Tag \"Schematic\" does not exist or is not first");
            }

            loadFrom(schematicTag);
        }

        /**
         * convert the contects of this schematic into a CompoundTag
         * @param name the name to given the CompoundTag
         * @return a CompoundTag containing a copy of the data within this Schematic
         */
        public CompoundTag toCompoundTag(String name) {
            Map<String, Tag> schemTags = new HashMap<>();

            schemTags.put("Width", new ShortTag("Width", width));
            schemTags.put("Height", new ShortTag("Height", height));
            schemTags.put("Length", new ShortTag("Length", length));
            schemTags.put("Materials", new StringTag("Materials", "Alpha"));
            schemTags.put("Blocks", new ByteArrayTag("Blocks", blocks));
            schemTags.put("Data", new ByteArrayTag("Data", data));

            CompoundTag schematic = new CompoundTag(name, schemTags);

            return schematic;
        }

        private <T extends Tag> T getChildTag(Map<String, Tag> tags, String id, Class<T> clazz) {
            Validate.notNull(tags);
            Validate.notNull(id);
            Validate.notNull(clazz);

            Tag tag = tags.get(id);

            if(tag == null) throw new NullPointerException("Tag "  + id + " does not exist!");

            return clazz.cast(tag);
        }

        /**
         * Returns an iterator over elements of type {@code T}.
         *
         * @return an Iterator.
         */
        @Override
        public Iterator<SchematicBlock> iterator() {
            return new SchematicIterator();
        }
    }
     
    Edit:
    Thanks to @xTrollxDudex for pointing me in the right direction.

    The problem was that Java treats bytes as signed values and the .schematic specification requires unsigned bytes. The solution to this problem is that when reading the byte[] from a schematic we must translate the blockIDs from signed bytes into their unsigned counterparts. The solution I found to this after a little research was the following:

    Code (Java):
    public int blockID() {
                return (blocks[index] & 0xFF);
            }
    This takes the blockID byte at the index and returns it as an integer blockID that represents the unsigned value of the blockID. Thus my schematic pasting code now appears to be working as intended
     
    #1 CrypticCabub, Jun 27, 2018
    Last edited: Jun 28, 2018
  2. Not working as in...?

    Edit: What do you mean not pasting certain blocks? As in certain blocks don't show up? Have you done any debugging at all to confirm the values at these positions? Are the schematics you are loading compliant with the format?

    Edit 2: You also have not shown how you are setting the block type at all, how you are converting from the block information into a Material or type. You can use 0xFF to turn an int into unsigned byte btw
     
    #2 xTrollxDudex, Jun 27, 2018
    Last edited: Jun 27, 2018
  3. not pasting as in it appears to just be pasting air or not pasting anything at all (blocks do not exist after paste). I am pretty sure the pasting code works as it does iterate over all locations and paste as intended, there are just some blocks being left out. I didn't include it as it's spread over several other classes responsible for different things in my project but here is the helper code that is actually setting blocks at each position:

    setBlock()
    Code (Java):
            public void setBlock(Location location, int blockID, byte blockData) {
                if (fastPasteEnabled) {
                    try {
                        NMSBlockChanges.setBlockSuperFast(location.getBlock(), blockID, blockData, false);
                    } catch (IncompatibleServerVersionException e) {
                        fastPasteEnabled = false;
                        Bukkit.getLogger().log(Level.SEVERE, "Incompatible server for rapid pasting! pasting slow instead");

                        //the block didn't paste so make sure it did
                        location.getBlock().setTypeIdAndData(blockID, blockData, false);
                    }
                } else {
                    location.getBlock().setTypeIdAndData(blockID, blockData, false);
                }
            }
    setBlockSuperFast()
    Code (Java):
        private static void setBlockSuperFast1_12(Block b, int blockId, byte data, boolean applyPhysics) {
            net.minecraft.server.v1_12_R1.World w = ((org.bukkit.craftbukkit.v1_12_R1.CraftWorld) b.getWorld()).getHandle();
            net.minecraft.server.v1_12_R1.Chunk chunk = w.getChunkAt(b.getX() >> 4, b.getZ() >> 4);
            net.minecraft.server.v1_12_R1.BlockPosition bp = new net.minecraft.server.v1_12_R1.BlockPosition(b.getX(), b.getY(), b.getZ());
            int combined = blockId + (data << 12);
            net.minecraft.server.v1_12_R1.IBlockData ibd = net.minecraft.server.v1_12_R1.Block.getByCombinedId(combined);
            if (applyPhysics) {
                w.setTypeAndData(bp, ibd, 3);
            } else {
                w.setTypeAndData(bp, ibd, 2);
            }
            chunk.a(bp, ibd);
        }
    Based on behavior I am pretty sure the issue is with the blocks themselves as I am pasting full combat arenas correctly and with all spawn positions correctly aligned. There are just some blocks missing from the arena.
     
  4. print out blockId, I bet you it's negative
     
  5. That was spot on @xTrollxDudex thank you. I will edit the original post with a more detailed explanation of the problem and solution
     
  6. I'd be curious to know how you got it to work. Any luck with my solution above?
     
  7. Solution is now in the op, but please let me know if there are any other details that might be useful clarifying for anybody else looking at this thread in the future.