Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Disguise a block (like a chest) as an arbitrary block.
package com.comphenix.example;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;
import org.bukkit.Location;
import org.bukkit.World;
/**
* Utility class for creating arrays of block changes.
* <p>
* See also {@link Packet34MultiBlockChange}.
*
* @author Kristian
*/
public class BlockChangeArray {
/**
* Represents a single block change.
* <p>
* Retrieved by {@link BlockChangeArray#getBlockChange(int)}.
*
* @author Kristian
*/
public class BlockChange {
// Index of the block change entry that we may change
private final int index;
private BlockChange(int index) {
this.index = index;
}
/**
* Set the location of the block change.
* <p<
* The coordinates will be correctly converted to relative coordinates, provided that all the blocks
* are from the same chunk (16x16 column of blocks).
* @param loc - location.
* @return This block change, for chaining.
*/
public BlockChange setLocation(Location loc) {
setRelativeX(loc.getBlockX() & 0xF);
setRelativeZ(loc.getBlockZ() & 0xF);
setAbsoluteY(loc.getBlockY());
return this;
}
/**
* Retrieve the location of this block change.
* <p>
* The world and absolute chunk position must be provided.
* @param world - the world the block belongs to.
* @param chunkX - the x position of the origin chunk
* @param chunkZ - the y position of the origin chunk
* @return The location.
*/
public Location getLocation(World world, int chunkX, int chunkZ) {
if (world == null)
throw new IllegalArgumentException("World cannot be NULL.");
return new Location(
world,
(chunkX << 4) + getRelativeX(),
getAbsoluteY(),
(chunkZ << 4) + getRelativeZ()
);
}
/**
* Set the relative x-axis position of current block change in the chunk.
* @param relativeX - relative block change location.
* @return This block change, for chaining.
*/
public BlockChange setRelativeX(int relativeX) {
setValue(relativeX, 28, 0xF0000000);
return this;
}
/**
* Retrieve the relative x-axis position of the current block change.
* @return X-axis position of the block change.
*/
public int getRelativeX() {
return getValue(28, 0xF0000000);
}
/**
* Set the relative z-axis position of current block change in the chunk.
* @param relativeZ - relative block change location.
* @return This block change, for chaining.
*/
public BlockChange setRelativeZ(int relativeX) {
setValue(relativeX, 24, 0xF000000);
return this;
}
/**
* Retrieve the relative z-axis position of the current block change.
* @return Z-axis position of the block change.
*/
public byte getRelativeZ() {
return (byte) getValue(24, 0xF000000);
}
/**
* Set the absolute y-axis position of the current block change.
* @param absoluteY - the absolute y-axis position.
* @return This block change, for chaining.
*/
public BlockChange setAbsoluteY(int absoluteY) {
setValue(absoluteY, 16, 0xFF0000);
return this;
}
/**
* Retrieve the absolute y-axis position of the current block change.
* @return Y-axis position of the block change.
*/
public int getAbsoluteY() {
return getValue(16, 0xFF0000);
}
/**
* Set the block ID of the current block change.
* @param blockID - ID that the changed block will have.
* @return This block change, for chaining.
*/
public BlockChange setBlockID(int blockID) {
setValue(blockID, 4, 0xFFF0);
return this;
}
/**
* Retrieve the block ID of the current block change.
* @return The block ID that the block will change into.
*/
public int getBlockID() {
return getValue(4, 0xFFF0);
}
/**
* Set the block metadata of the current block change.
* @param metadata - metadata that the changed block will have.
* @return This block change, for chaining.
*/
public BlockChange setMetadata(int metadata) {
setValue(metadata, 0, 0xF);
return this;
}
/**
* Retrieve the block metadata of the current block change.
* @return The block metadata that the block will change into.
*/
public int getMetadata() {
return getValue(0, 0xF);
}
/**
* Retrieve the index of the current block change.
* @return Index of the current block change.
*/
public int getIndex() {
return index;
}
/**
* Retrieve the integer representation of this block change.
* @return Integer representation.
*/
private int asInteger() {
return data[index];
}
// Should be inlined
private void setValue(int value, int leftShift, int updateMask) {
data[index] = ((value << leftShift) & updateMask) | (data[index] & ~updateMask);
}
private int getValue(int rightShift, int updateMask) {
return (data[index] & updateMask) >> rightShift;
}
}
/**
* Single of a single block change record in bytes.
*/
private static final int RECORD_SIZE = 4;
/**
* The internally backed array.
*/
private int[] data;
/**
* Construct a new array of block changes.
* @param blockChanges - the number of blocks that have been changed.
*/
public BlockChangeArray(int blockChanges) {
data = new int[blockChanges];
}
/**
* Construct a new block change array from the copy of a given data array.
* @param data - the data array to store internally.
*/
public BlockChangeArray(byte[] input) {
if ((input.length % RECORD_SIZE) != 0)
throw new IllegalArgumentException("The lenght of the input data array should be a multiple of " + RECORD_SIZE + ".");
IntBuffer source = ByteBuffer.wrap(input).asIntBuffer();
IntBuffer destination = IntBuffer.allocate(input.length / RECORD_SIZE);
destination.put(source);
// Get the copied array
data = destination.array();
}
/**
* Retrieve a view of the block change entry at the given index.
* <p>
* Any modification to this view will be stored in the block change array itself.
* @param index - index of the block change to retrieve.
* @return A view of the block change entry.
*/
public BlockChange getBlockChange(int index) {
if (index < 0 || index >= getSize())
throw new IllegalArgumentException("Index is out of bounds.");
return new BlockChange(index);
}
/**
* Set the block change at the specified index to contain the given block.
* @param loc - the location that will be converted.
* @param block - the new content of the block change.
*/
public void setBlockChange(int index, BlockChange change) {
if (change == null)
throw new IllegalArgumentException("Block change cannot be NULL.");
data[index] = change.asInteger();
}
/**
* Retrieve the number of block changes.
* @return The number of block changes.
*/
public int getSize() {
return data.length;
}
/**
* Convert this block change array to a byte array.
* @return The resulting byte array.
*/
public byte[] toByteArray() {
ByteBuffer copy = ByteBuffer.allocate(data.length * RECORD_SIZE);
// Copy in the integer array
copy.asIntBuffer().put(data);
return copy.array();
}
}
package com.comphenix.example;
import java.io.Serializable;
import java.util.Arrays;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.block.Block;
class BlockCoordinate implements Serializable {
private static final long serialVersionUID = 1L;
private final int x;
private final int y;
private final int z;
public BlockCoordinate(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
public BlockCoordinate(Location loc) {
this(loc.getBlockX(), loc.getBlockY(), loc.getBlockZ());
}
@Override
public int hashCode() {
return Arrays.hashCode(new int[] {x, y, z });
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj instanceof BlockCoordinate) {
BlockCoordinate other = (BlockCoordinate) obj;
return x == other.x && y == other.y && z == other.z;
}
return true;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getZ() {
return z;
}
public Block toBlock(World world) {
return world.getBlockAt(x, y, z);
}
@Override
public String toString() {
return "[x: " + x + ", y: " + y + ", z: " + z + "]";
}
}
package com.comphenix.example;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import org.apache.commons.lang.SerializationUtils;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.plugin.Plugin;
import com.comphenix.example.BlockChangeArray.BlockChange;
import com.comphenix.example.ChunkPacketProcessor.ChunkletProcessor;
import com.comphenix.protocol.Packets;
import com.comphenix.protocol.ProtocolLibrary;
import com.comphenix.protocol.events.ConnectionSide;
import com.comphenix.protocol.events.PacketAdapter;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.events.PacketEvent;
import com.comphenix.protocol.reflect.FieldAccessException;
import com.comphenix.protocol.reflect.StructureModifier;
import com.google.common.collect.HashBasedTable;
/**
* Simple class that can be used to alter the apperance of a number of blocks.
* @author Kristian
*/
public class BlockDisguiser {
private HashBasedTable<ChunkCoordinate, BlockCoordinate, Integer> translations = HashBasedTable.create();
// The current listener
private PacketAdapter listener;
/**
* Construct a new block changer.
* @param parent - the owner plugin.
*/
public BlockDisguiser(Plugin parent) {
registerListener(parent);
}
@SuppressWarnings("unchecked")
public void loadState(InputStream stream) {
translations = (HashBasedTable<ChunkCoordinate, BlockCoordinate, Integer>)
SerializationUtils.deserialize(stream);
}
public void loadState(File source) throws IOException {
InputStream io = null;
try {
io = new BufferedInputStream(new FileInputStream(source));
loadState(io);
} finally {
if (io != null) {
io.close();
}
}
}
public void saveState(OutputStream stream) {
SerializationUtils.serialize(translations, stream);
}
public void saveState(File destination) throws IOException {
OutputStream io = null;
try {
io = new BufferedOutputStream(new FileOutputStream(destination));
saveState(io);
} finally {
if (io != null) {
io.close();
}
}
}
/**
* Create a new translated block that have a different block ID on the server and visually for a client.
* @param loc - the location of the block.
* @param serverBlockID - the block ID on the server side.
* @param clientBlockID - the block ID on the client side.
*/
public void setTranslatedBlock(Location loc, int serverBlockID, int clientBlockID) {
// Make this block appear as the client block
translations.put(ChunkCoordinate.fromBlock(loc), new BlockCoordinate(loc), clientBlockID);
// Set the server side block
loc.getBlock().setTypeId(serverBlockID);
}
private void registerListener(Plugin plugin) {
final ChunkletProcessor processor = getChunkletProcessor();
ProtocolLibrary.getProtocolManager().addPacketListener(
listener = new PacketAdapter(plugin, ConnectionSide.SERVER_SIDE,
Packets.Server.BLOCK_CHANGE, Packets.Server.MULTI_BLOCK_CHANGE,
Packets.Server.MAP_CHUNK, Packets.Server.MAP_CHUNK_BULK) {
@Override
public void onPacketSending(PacketEvent event) {
PacketContainer packet = event.getPacket();
World world = event.getPlayer().getWorld();
switch (event.getPacketID()) {
case Packets.Server.BLOCK_CHANGE:
translateBlockChange(packet, world);
break;
case Packets.Server.MULTI_BLOCK_CHANGE:
translateMultiBlockChange(packet, world);
break;
case Packets.Server.MAP_CHUNK:
ChunkPacketProcessor.fromMapPacket(packet, world).process(processor);
break;
case Packets.Server.MAP_CHUNK_BULK:
for (ChunkPacketProcessor chunk : ChunkPacketProcessor.fromMapBulkPacket(packet, world)) {
chunk.process(processor);
}
break;
}
}
});
}
public void close() {
if (listener != null) {
ProtocolLibrary.getProtocolManager().removePacketListener(listener);
listener = null;
}
}
private ChunkletProcessor getChunkletProcessor() {
return new ChunkletProcessor() {
@Override
public void processChunklet(Location origin, byte[] data, int blockIndex, int dataIndex) {
ChunkCoordinate coord = ChunkCoordinate.fromBlock(origin);
World world = origin.getWorld();
int originX = origin.getBlockX();
int originY = origin.getBlockY();
int originZ = origin.getBlockZ();
for (BlockCoordinate position : translations.row(coord).keySet()) {
int posX = position.getX();
int posY = position.getY();
int posZ = position.getZ();
// Make sure we're inside the chunklet
if (posY >= originY && posY - originY < 16) {
int offset = blockIndex + (posX - originX) + (posZ - originZ) * 16 + (posY - originY) * 256;
data[offset] = (byte) translateBlockID(world, posX, posY, posZ, data[offset] & 0xFF);
}
}
}
};
}
private void translateBlockChange(PacketContainer packet, World world) throws FieldAccessException {
StructureModifier<Integer> ints = packet.getIntegers();
int x = ints.read(0);
int y = ints.read(1);
int z = ints.read(2);
int blockID = ints.read(3);
System.out.println("Block change: " + x + ", " + y + ", " + z);
// Convert using the tables
ints.write(3, translateBlockID(world, x, y, z, blockID));
}
private void translateMultiBlockChange(PacketContainer packet, World world) throws FieldAccessException {
StructureModifier<byte[]> byteArrays = packet.getByteArrays();
StructureModifier<Integer> ints = packet.getIntegers();
int baseX = ints.read(0) << 4;
int baseZ = ints.read(1) << 4;
BlockChangeArray data = new BlockChangeArray(byteArrays.read(0));
for (int i = 0; i < data.getSize(); i++) {
BlockChange change = data.getBlockChange(i);
change.setBlockID(translateBlockID(
world,
baseX + change.getRelativeX(),
change.getAbsoluteY(),
baseZ + change.getRelativeZ(),
change.getBlockID()
));
}
byteArrays.write(0, data.toByteArray());
}
private int translateBlockID(World world, int x, int y, int z, int blockID) {
Integer translate = translations.get(
ChunkCoordinate.fromBlock(world, x, z), new BlockCoordinate(x, y, z));
// Use the existing block ID if not found
return translate != null ? translate : blockID;
}
}
package com.comphenix.example;
import java.io.Serializable;
import org.bukkit.Location;
import org.bukkit.World;
import com.google.common.base.Objects;
class ChunkCoordinate implements Serializable {
private static final long serialVersionUID = 1L;
private final String worldID;
private final int chunkX;
private final int chunkZ;
private ChunkCoordinate(World world, int chunkX, int chunkZ) {
this.worldID = world.getName();
this.chunkX = chunkX;
this.chunkZ = chunkZ;
}
public static ChunkCoordinate fromBlock(World world, int x, int z) {
return new ChunkCoordinate(world, x >> 4, z >> 4);
}
public static ChunkCoordinate fromBlock(Location loc) {
return fromBlock(loc.getWorld(), loc.getBlockX(), loc.getBlockZ());
}
@Override
public int hashCode() {
return Objects.hashCode(worldID, chunkX, chunkZ);
}
public int getChunkX() {
return chunkX;
}
public int getChunkZ() {
return chunkZ;
}
public String getWorldID() {
return worldID;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj instanceof ChunkCoordinate) {
ChunkCoordinate other = (ChunkCoordinate) obj;
return worldID == other.worldID && chunkX == other.chunkX && chunkZ == other.chunkZ;
}
return true;
}
@Override
public String toString() {
return "[worldID: " + worldID + ", chunkX: " + chunkX + ", chunkZ: " + chunkZ + "]";
}
}
package com.comphenix.example;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.World.Environment;
import com.comphenix.protocol.Packets;
import com.comphenix.protocol.events.PacketContainer;
import com.comphenix.protocol.reflect.StructureModifier;
/**
* Used to process a chunk.
*
* @author Kristian
*/
public class ChunkPacketProcessor {
/**
* Process the content of a single 16x16x16 chunklet in a 16x256x16 chunk.
* @author Kristian
*/
public interface ChunkletProcessor {
public void processChunklet(Location origin, byte[] data, int blockIndex, int dataIndex);
}
// 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;
protected 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 int blockSize;
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.getID() != Packets.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.getID() != Packets.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;
}
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++;
}
}
// 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.
int skylightCount = world.getEnvironment() == Environment.NORMAL ? 1 : 0;
// 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);
blockSize = 4096 * chunkSectionNumber;
if (startIndex + blockSize > data.length) {
return;
}
// Make sure the chunk is loaded
if (isChunkLoaded(world, chunkX, chunkZ)) {
translate(processor);
}
}
private void translate(ChunkletProcessor processor) {
// Loop over 16x16x16 chunks in the 16x256x16 column
int idIndexModifier = 0;
int idOffset = startIndex;
int dataOffset = idOffset + chunkSectionNumber * 4096;
for (int i = 0; i < 16; i++) {
// If the bitmask indicates this chunk is sent
if ((chunkMask & 1 << i) > 0) {
int relativeIDStart = idIndexModifier * 4096;
int relativeDataStart = idIndexModifier * 2048;
int blockIndex = idOffset + relativeIDStart;
int dataIndex = dataOffset + relativeDataStart;
// 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, blockIndex, dataIndex);
idIndexModifier++;
}
}
}
private boolean isChunkLoaded(World world, int x, int z) {
return world.isChunkLoaded(x, z);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment