package com.comphenix.packetwrapper; import org.bukkit.Location; import org.bukkit.World; import org.bukkit.World.Environment; import com.comphenix.protocol.PacketType; import com.comphenix.protocol.events.PacketContainer; import com.comphenix.protocol.reflect.StructureModifier; /** * Used to process a chunk. * * @author Kristian */ public class ChunkPacketProcessor { /** * Contains the offset of the different block data in a chunk packet. * @author Kristian */ public static class ChunkOffsets { private int blockIdOffset; private int dataOffset; private int lightOffset; private int skylightOffset; private int extraOffset; private ChunkOffsets(int blockIdOffset, int dataOffset, int lightOffset, int skylightOffset, int extraOffset) { this.blockIdOffset = blockIdOffset; this.dataOffset = dataOffset; this.lightOffset = lightOffset; this.skylightOffset = skylightOffset; this.extraOffset = extraOffset; } private void incrementIdIndex() { blockIdOffset += ChunkPacketProcessor.BLOCK_ID_LENGHT; dataOffset += ChunkPacketProcessor.BYTES_PER_NIBBLE_PART; dataOffset += ChunkPacketProcessor.BYTES_PER_NIBBLE_PART; if (skylightOffset >= 0) { skylightOffset += ChunkPacketProcessor.BYTES_PER_NIBBLE_PART; } } private void incrementExtraIndex() { if (extraOffset >= 0) { extraOffset += ChunkPacketProcessor.BYTES_PER_NIBBLE_PART; } } /** * Retrieve the starting index of the block ID data. * <p> * This will be 4096 bytes in lenght, one byte for each block in the 16x16x16 chunklet. * @return The starting location of the block ID data. */ public int getBlockIdOffset() { return blockIdOffset; } /** * Retrieve the starting index of the meta data (4 bit per block). * <p> * This will be 2048 bytes in lenght, one nibblet for each block in the 16x16x16 chunklet. * @return The starting location of the block meta data. */ public int getDataOffset() { return dataOffset; } /** * Retrieve the starting index of the torch light data (4 bit per block). * <p> * This will be 2048 bytes in lenght, one nibblet for each block in the 16x16x16 chunklet. * @return The starting location of the torch light data. */ public int getLightOffset() { return lightOffset; } /** * Retrieve the starting index of the skylight data (4 bit per block). * <p> * This will be 2048 bytes in lenght if the skylight data exists (see {@link #hasSkylightOffset()}), * no bytes if not. * @return The starting location of the skylight data. */ public int getSkylightOffset() { return skylightOffset; } /** * Determine if the current chunklet contains skylight data. * @return TRUE if it does, FALSE otherwise. */ public boolean hasSkylightOffset() { return skylightOffset >= 0; } /** * Retrieve the extra 4 bits in each block ID, if necessary. * <p> * This will be 2048 bytes in lenght if the extra data exists, no bytes if not. * @return The starting location of the extra data. */ public int getExtraOffset() { return extraOffset; } /** * Determine if the current chunklet contains any extra block ID data. * @return TRUE if it does, FALSE otherwise. */ public boolean hasExtraOffset() { return extraOffset > 0; } } /** * Process the content of a single 16x16x16 chunklet in a 16x256x16 chunk. * @author Kristian */ public interface ChunkletProcessor { /** * Process a given chunklet (16x16x16). * @param origin - the block with the lowest x, y and z coordinate in the chunklet. * @param data - the data array. * @param offsets - the offsets with the data for the given chunklet. */ public void processChunklet(Location origin, byte[] data, ChunkOffsets offsets); /** * Process the biome array for a chunk (16x256x16). * <p> * This method will not be called if the chunk is missing biome information. * @param origin - the block with the lowest x, y and z coordinate in the chunk. * @param data - the data array. * @param biomeIndex - the starting index of the biome data (256 bytes in lenght). */ public void processBiomeArray(Location origin, byte[] data, int biomeIndex); } // Useful Minecraft constants protected static final int BYTES_PER_NIBBLE_PART = 2048; protected static final int CHUNK_SEGMENTS = 16; protected static final int NIBBLES_REQUIRED = 4; public static final int BLOCK_ID_LENGHT = 4096; public static final int DATA_LENGHT = 2048; public static final int BIOME_ARRAY_LENGTH = 256; private int chunkX; private int chunkZ; private int chunkMask; private int extraMask; private int chunkSectionNumber; private int extraSectionNumber; private boolean hasContinous = true; private int startIndex; private int size; private byte[] data; private World world; private ChunkPacketProcessor() { // Use factory methods } /** * Construct a chunk packet processor from a givne MAP_CHUNK packet. * @param packet - the map chunk packet. * @return The chunk packet processor. */ public static ChunkPacketProcessor fromMapPacket(PacketContainer packet, World world) { if (!packet.getType().equals(PacketType.Play.Server.MAP_CHUNK)) throw new IllegalArgumentException(packet + " must be a MAP_CHUNK packet."); StructureModifier<Integer> ints = packet.getIntegers(); StructureModifier<byte[]> byteArray = packet.getByteArrays(); // Create an info objects ChunkPacketProcessor processor = new ChunkPacketProcessor(); processor.world = world; processor.chunkX = ints.read(0); // packet.a; processor.chunkZ = ints.read(1); // packet.b; processor.chunkMask = ints.read(2); // packet.c; processor.extraMask = ints.read(3); // packet.d; processor.data = byteArray.read(1); // packet.inflatedBuffer; processor.startIndex = 0; if (packet.getBooleans().size() > 0) { processor.hasContinous = packet.getBooleans().read(0); } return processor; } /** * Construct an array of chunk packet processors from a given MAP_CHUNK_BULK packet. * @param packet - the map chunk bulk packet. * @return The chunk packet processors. */ public static ChunkPacketProcessor[] fromMapBulkPacket(PacketContainer packet, World world) { if (!packet.getType().equals(PacketType.Play.Server.MAP_CHUNK_BULK)) throw new IllegalArgumentException(packet + " must be a MAP_CHUNK_BULK packet."); StructureModifier<int[]> intArrays = packet.getIntegerArrays(); StructureModifier<byte[]> byteArrays = packet.getByteArrays(); int[] x = intArrays.read(0); // packet.c; int[] z = intArrays.read(1); // packet.d; ChunkPacketProcessor[] processors = new ChunkPacketProcessor[x.length]; int[] chunkMask = intArrays.read(2); // packet.a; int[] extraMask = intArrays.read(3); // packet.b; int dataStartIndex = 0; for (int chunkNum = 0; chunkNum < processors.length; chunkNum++) { // Create an info objects ChunkPacketProcessor processor = new ChunkPacketProcessor(); processors[chunkNum] = processor; processor.world = world; processor.chunkX = x[chunkNum]; processor.chunkZ = z[chunkNum]; processor.chunkMask = chunkMask[chunkNum]; processor.extraMask = extraMask[chunkNum]; processor.hasContinous = true; // Always true processor.data = byteArrays.read(1); //packet.buildBuffer; // Check for Spigot if (processor.data == null || processor.data.length == 0) { processor.data = packet.getSpecificModifier(byte[][].class).read(0)[chunkNum]; } else { processor.startIndex = dataStartIndex; } dataStartIndex += processor.size; } return processors; } /** * Begin processing the current chunk with the provided processor. * @param processor - the processor that will process the chunk. */ public void process(ChunkletProcessor processor) { // Compute chunk number for (int i = 0; i < CHUNK_SEGMENTS; i++) { if ((chunkMask & (1 << i)) > 0) { chunkSectionNumber++; } if ((extraMask & (1 << i)) > 0) { extraSectionNumber++; } } int skylightCount = getSkylightCount(); // The total size of a chunk is the number of blocks sent (depends on the number of sections) multiplied by the // amount of bytes per block. This last figure can be calculated by adding together all the data parts: // For any block: // * Block ID - 8 bits per block (byte) // * Block metadata - 4 bits per block (nibble) // * Block light array - 4 bits per block // If 'worldProvider.skylight' is TRUE // * Sky light array - 4 bits per block // If the segment has extra data: // * Add array - 4 bits per block // Biome array - only if the entire chunk (has continous) is sent: // * Biome array - 256 bytes // // A section has 16 * 16 * 16 = 4096 blocks. size = BYTES_PER_NIBBLE_PART * ( (NIBBLES_REQUIRED + skylightCount) * chunkSectionNumber + extraSectionNumber) + (hasContinous ? BIOME_ARRAY_LENGTH : 0); if ((getOffset(2) - startIndex) > data.length) { return; } // Make sure the chunk is loaded if (isChunkLoaded(world, chunkX, chunkZ)) { translate(processor); } } /** * Retrieve the number of 2048 byte segments per chunklet. * <p< * This is usually one for The Overworld, and zero for both The End and The Nether. * @return Number of skylight byte segments. */ protected int getSkylightCount() { // There's no sun/moon in the end or in the nether, so Minecraft doesn't sent any skylight information // This optimization was added in 1.4.6. Note that ideally you should get this from the "f" (skylight) field. return world.getEnvironment() == Environment.NORMAL ? 1 : 0; } private int getOffset(int nibbles) { return startIndex + (nibbles * chunkSectionNumber * ChunkPacketProcessor.BYTES_PER_NIBBLE_PART); } private void translate(ChunkletProcessor processor) { // Loop over 16x16x16 chunks in the 16x256x16 column int current = 4; ChunkOffsets offsets = new ChunkOffsets( getOffset(0), getOffset(2), getOffset(3), getSkylightCount() > 0 ? getOffset(current++) : -1, extraSectionNumber > 0 ? getOffset(current++) : -1 ); for (int i = 0; i < 16; i++) { // If the bitmask indicates this chunk is sent if ((chunkMask & 1 << i) > 0) { // The lowest block (in x, y, z) in this chunklet Location origin = new Location(world, chunkX << 4, i * 16, chunkZ << 4); processor.processChunklet(origin, data, offsets); offsets.incrementIdIndex(); } if ((extraMask & 1 << i) > 0) { offsets.incrementExtraIndex(); } } if (hasContinous) { processor.processBiomeArray(new Location(world, chunkX << 4, 0, chunkZ << 4), data, startIndex + size - BIOME_ARRAY_LENGTH); } } private boolean isChunkLoaded(World world, int x, int z) { return world.isChunkLoaded(x, z); } }