Resource New 1.16.2 MultiBlockChange Guide (ProtocolLib)

Discussion in 'Spigot Plugin Development' started by joehot200, Sep 5, 2020.

  1. joehot200

    Supporter

    Did the new 1.16.2 changes to the MultiBlockChange packet have you banging your head against a wall? Me too! As there were 4+ issues on ProtocolLib's GitHub about the change, here is a basic how-to guide for 1.16.2+ MultiBlockChange.

    Make a new PacketContainer of type MULTI_BLOCK_CHANGE.
    Code (Java):

    PacketType type =
                PacketType.Play.Server.MULTI_BLOCK_CHANGE;
    PacketContainer blChPack;
    public void createPacket(){ //Maybe instead in a new class / class constructor, such as PacketWrapper does?
    blChPack = new PacketContainer(type);
    }
     
    Once we created the packet, we then need to set the chunk that this packet will change. In 1.16.2, multi block change covers a 16x16x16 area - the old 1.16.1 and below multi block change covered a 16x256x16 area - this means that if you're converting from old code, you will have to send more change packets than before.
    Code (Java):

    public void setChunkModified(Vector location) { //Takes a *real world coordinate*.
            //Converts to chunk coordinate
            int x = location.getBlockX() >> 4;
            int y = location.getBlockY() >> 4;
            int z = location.getBlockZ() >> 4;
            //Creates a new BlockPosition at chunk coordinate
            BlockPosition value = new BlockPosition(x, y, z);
            //Writes the chunk position
            blChPack.getSectionPositions().write(0, value); //blChPack is our PacketContainer from before
        }
     
    Now we have a chunk, we need to prepare some fake blocks! To do this, we create a WrappedBlockData of any block type we want.
    Code (Java):

    ArrayList<WrappedBlockData> blockArray; //Initialised somewhere
    public WrappedBlockData getGlowstoneData(){
    return WrappedBlockData.createData(Material.GLOWSTONE); //Let's fake some glowstone!
    }
     
    And we also need to be able to set the position. In the 1.16.2 packet, the position is a short calculated from the X, Y and Z coordinates of the block - code below.
    Code (Java):

    public short setShortLocation(Location loc) { //Takes a real-world location.
            return (setShortLocation(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ()));
        }
     
        public short setShortLocation(int x, int y, int z) {
             //Convert to location within chunk.
             x = x & 0xF;
             y = y & 0xF;
             z = z & 0xF;
             //Creates position from location within chunk
             return (short) (x << 8 | z << 4 | y << 0);
        }
     
    Next, we get to the data of the packet. In 1.16.2, the new packet requires an array of WrappedBlockData (basically, the block type) and an array of short (the block location), both of which we just created above! Now, we simply add them to the packet. There are many ways to do this, however I've currently used ArrayLists and then converted them to arrays using ArrayUtils┬╣.

    Code (Java):

    public void setChangeData(ArrayList<WrappedBlockData> blockDat, ArrayList<Short> blockPositions) {
     
            WrappedBlockData[] blockData = blockDat.toArray(new WrappedBlockData[0]);
            Short[] blockLocsShort = blockPositions.toArray(new Short[0]);
            short[] blockLocations = ArrayUtils.toPrimitive(blockLocsShort);
     
     
            blChPack.getBlockDataArrays().writeSafely(0, blockData);
            blChPack.getShortArrays().writeSafely(0, blockLocations);
        }
     
    ┬╣ There are more efficient ways than converting from an ArrayList to an array, as done in the above example, *every* time you want to send a block change, especially if the data you send isn't changing and so doesn't need to be converted every time.

    Finally, we have the usual code to send the packet:

    Code (Java):

    public void sendPacket(Player p) {
                ProtocolLibrary.getProtocolManager().sendServerPacket(p, blChPack);
        }
     
    To use these methods, you would have something like this:
    Code (Java):

    ArrayList<WrappedBlockData> blockArray; //Our fake blocks, initialized somewhere
    ArrayList<Short> locationArray; //Locations of said fake blocks, initialized somewhere

    Block b = new Location(0, 1, 0).getBlock(); //Any block we want to change.
    Location blockLoc = b.getLocation(); //Location of block

    createPacket(); //Create the packet
    setChunk(new Vector(blockLoc.getX(), blockLoc.getY(), blockLoc.getZ())); //Converted to chunk coords automatically

    //Set block 'b' to be changed into Glowstone by adding it to the ArrayLists.
    blockArray.add(getGlowstoneData()); //Add 1 glowstone to blockArray.
    locationArray.add(setShortLocation(blockLoc)); //Adds position of glowstone to array

    //Set the data of the packet.
    setChangeData(blockArray, LocationArray);

    //Send the packet
    sendPacket(p); //p = player to send packet to
     
    Keep in mind that the order of both arrays must match. For example, locationArray.get(0) must return the position for blockArray.get(0).

    And that's it! Thanks to @dmulloy2 for pointing me in the right direction, and this issue on GitHub was useful too.

    If you're used to using PacketWrapper for ProtocolLib, you can simply replace setChunk() with the setChunkModified() code above, and replace setRecords() with the setChangeData() code above.
    If you're used to NMS, it may be helpful for you to see how BetterPortals by @_TheProff_ does it. Code pasted below as BetterPortals is now switching to ProtocolLib.

    package com.lauriethefish.betterportals.bukkit.multiblockchange;
    import com.lauriethefish.betterportals.bukkit.ReflectUtils;
    import org.bukkit.Location;
    import org.bukkit.entity.Player;
    import org.bukkit.util.Vector;
    // Interface to represent both implementations of MultiblockChangeManager
    public interface MultiBlockChangeManager {
    // Instantiates the correct implementation of MultiBlockChangeManager depending on the underlying server
    static MultiBlockChangeManager createInstance(Player player) {
    return (MultiBlockChangeManager) ReflectUtils.newInstance(ReflectUtils.multiBlockChangeImpl,
    new Class[]{Player.class}, new Object[]{player});
    }
    void addChange(Vector location, Object newType);
    default void addChange(Location location, Object newType) {
    addChange(location.toVector(), newType);
    }
    void sendChanges();
    }


    package com.lauriethefish.betterportals.bukkit.multiblockchange;
    import java.lang.reflect.Array;
    import java.util.HashMap;
    import java.util.Map;
    import com.lauriethefish.betterportals.bukkit.ReflectUtils;
    import org.bukkit.entity.Player;
    import org.bukkit.util.Vector;
    // Allows you to add blocks to a HashMap that will then be divided to be sent to the player
    public class MultiBlockChangeManager_1_16_2 implements MultiBlockChangeManager {
    private Object playerConnection;
    // Stores the changes, separated out into chunk sections
    private HashMap<SectionPosition, HashMap<Vector, Object>> changes = new HashMap<>();
    public MultiBlockChangeManager_1_16_2(Player player) {
    Object craftPlayer = ReflectUtils.runMethod(player, "getHandle");
    playerConnection = ReflectUtils.getField(craftPlayer, "playerConnection");
    }
    // Adds a new block to the HashMap
    public void addChange(Vector location, Object newType) {
    // Add a hashmap for this chunk if one doesn't already exist
    SectionPosition section = new SectionPosition(location);
    if(changes.get(section) == null) {
    changes.put(section, new HashMap<>());
    }
    changes.get(section).put(location, newType);
    }
    // Sends all the queued changes
    public void sendChanges() {
    for(Map.Entry<SectionPosition, HashMap<Vector, Object>> entry : changes.entrySet()) {
    sendMultiBlockChange(entry.getValue(), entry.getKey());
    }
    }
    // Constructs a multiple block change packet from the given blocks, and sends it to the player
    // All the blocks MUST be in the same chunk section
    private void sendMultiBlockChange(Map<Vector, Object> blocks, SectionPosition section) {
    // Make a new PacketPlayOutMultiBlockChange
    Class<?> packetClass = ReflectUtils.getMcClass("PacketPlayOutMultiBlockChange");
    Object packet = ReflectUtils.newInstance(packetClass);
    // Set the SectionPosition
    ReflectUtils.setField(packet, "a", section.toNMS());
    Object dataArray = Array.newInstance(ReflectUtils.getMcClass("IBlockData"), blocks.size());
    Object shortArray = Array.newInstance(short.class, blocks.size());
    int i = 0;
    for(Map.Entry<Vector, Object> entry : blocks.entrySet()) {
    Vector loc = entry.getKey();
    // Find the chunk section relative position
    int x = loc.getBlockX() & 0xF;
    int y = loc.getBlockY() & 0xF;
    int z = loc.getBlockZ() & 0xF;
    // Set the correct IBlockData and relative position as a short
    Array.set(dataArray, i, entry.getValue());
    Array.set(shortArray, i, (short) (x << 8 | z << 4 | y << 0));
    i++;
    }
    // Set it in the packet
    ReflectUtils.setField(packet, "b", shortArray);
    ReflectUtils.setField(packet, "c", dataArray);
    // Send the packet using more reflection stuff
    ReflectUtils.runMethod(playerConnection, "sendPacket", new Class[]{ReflectUtils.getMcClass("Packet")}, new Object[]{packet});
    }
    }


    package com.lauriethefish.betterportals.bukkit.multiblockchange;
    import java.util.Objects;
    import com.lauriethefish.betterportals.bukkit.ReflectUtils;
    import org.bukkit.util.Vector;
    import lombok.Getter;
    import lombok.Setter;
    @Getter
    @Setter
    public class SectionPosition {
    public int x;
    public int y;
    public int z;
    public SectionPosition(Vector location) {
    x = location.getBlockX() >> 4;
    y = location.getBlockY() >> 4;
    z = location.getBlockZ() >> 4;
    }
    // Make an NMS SectionPosition from this object
    public Object toNMS() {
    return ReflectUtils.newInstance("SectionPosition", new Class[]{int.class, int.class, int.class}, new Object[]{x, y, z});
    }
    // Automatically generated
    @Override
    public boolean equals(Object o) {
    if (o == this)
    return true;
    if (!(o instanceof SectionPosition)) {
    return false;
    }
    SectionPosition sectionPosition = (SectionPosition) o;
    return x == sectionPosition.x && y == sectionPosition.y && z == sectionPosition.z;
    }
    @Override
    public int hashCode() {
    return Objects.hash(x, y, z);
    }
    }

    The primary reason I made this was just to make life easier for everyone else transitioning to the new 1.16.2 multi block change code. Additionally, new users wishing to change blocks in 1.16.2+ would currently come across a lot of threads with the <1.16.1 code if they googled it.
     
    #1 joehot200, Sep 5, 2020
    Last edited: Jan 12, 2021 at 3:38 PM
    • Like Like x 4
    • Useful Useful x 4
  2. Great guide on the new system, very thoroughly explained with the new method. It might be worth adding a part about how you could use an interface to abstract the differences between versions and add a check for the version.

    Don't worry about linking my GitHub, that's what open source code is for :)
     
    • Friendly Friendly x 1
  3. @joehot200 Heads up that I refactored various parts of the plugin and the new link is here.
     
  4. joehot200

    Supporter

    Beautiful, edited the link in my post. Cheers! <3
     
  5. Hey I'm trying to follow this but it seems that

    PacketContainer#getBlockDataArray()
    and
    PacketContainer#getSectionPositions()

    no longer exist... Would you be able to help me out here? I might be missing something...
     
    • Like Like x 1
  6. joehot200

    Supporter

    I believe it's the other way around, you need to update your ProtocolLib from the Jenkins page.

    Make sure your users are updated too, I have a bunch of people use the wrong version and then report errors
     
  7. I'll check this out when I get home and report back.
     
  8. Ok it looks like its working but does multi block change only work in one chunk? do i have to send a new packet per chunk? Or is that just how you coded it.
     
  9. joehot200

    Supporter

    You send one multi block change packet per 16x16x16 chunk. If you need to modify two chunks, you need to send two packets, one for each chunk.

    That's not how I programmed it, that's how the Minecraft client works.
     
    • Useful Useful x 1
  10. Ah alright, cheers for the clarification, do you know what would be the most efficient and clean way of doing it per chunk section (on a programming level)
     
  11. Also how would I get the latest ProtocolLib dev build (1.16) via Maven? i've tried everything...
     
  12. joehot200

    Supporter

    For a 16x256x16, to turn all stone into diamond ore:
    Code (Java):

    HashMap<Integer, ArrayList<WrappedBlockData>> dat = new HashMap<Integer, ArrayList<WrappedBlockData>>();
    HashMap<Integer, ArrayList<Short>> blockPositions = new HashMap<Integer, ArrayList<Short>>();
            for (int i = 0; i <= 16; i++) {
                blockData.put(i, new ArrayList<WrappedBlockData>());
                blockPositions.put(i, new ArrayList<Short>());
            }
            BlockState[] blockStates = LIST_OF_BLOCKS_YOU_WANT_TO_CHANGE_IN_CHUNK; //A chunk of 16x256x16, that is.


            for (BlockState state : blockStates) {
          WrappedBlockData data;
                if (state.getType() == Material.STONE){
                data = WrappedBlockData.createData(Material.DIAMOND_ORE);
                } else{
                data = null;
                }
       
                if (data != null) {
                        short shortLoc = setShortLocation(state.getLocation());
                        int y = (int) (state.getY() / 16);
                        blockData.get(y).add(data);
                        blockPositions.get(y).add(shortLoc);
                   
                }
            }
     
    The above is not the most efficient code, but it is some test code I cobbled together a while ago. To make it more efficient I would replace the HashMap with an array.

    Code (Java):

    HashMap<Integer, ArrayList<WrappedBlockData>> dat = new HashMap<Integer, ArrayList<WrappedBlockData>>(); //old
         
    ArrayList<WrappedBlockData>[] dat = new ArrayList[16]; //new
    dat[0] = .....  //Assign your ArrayLists of data
     
    You could possibly even have list of lists, but I personally found ArrayLists easier to work with.

    What problems are you encountering specifically?
     
    • Useful Useful x 1
  13. Don't do ArrayList<...> ... = new ArrayList<...>() or HashMap<...> ... = new HashMap<...>() instead do List<...> ... = new ArrayList<>() and Map<...> ... = new HashMap<>() see here why
     
    • Agree Agree x 1
  14. Alright thanks, that helps point me in the right direction, as for the problems I'm encountering. I use IntelliJ, I can't find the maven stuff I need to put in my pom.xml to use the version of ProtocolLib that includes the #getBlockArray and #getSectionPositions etc, when I try adding the ProtocolLib as a library dependency, it fixes all the red lines including the imports but when I actually try to compile it says

    "package com.comphenix.protocol.wrappers does not exist"

    I added the jar to my module dependies, I also tried library depencies etc...
     
  15. Make sure the scope for your ProtocolLib dependency is set to "provided", as it's a runtime dependency.
     
  16. I fixed it, I had to add it to maven even though it was a local jar, I wasn't aware of that but thank you.
     
  17. joehot200

    Supporter

    A good point to be sure.