Skip to content

Introduction to Packet Listeners

In networking, we typically distinguish between packets sent to the client and packets sent to the server. There are certain packets that only the client can parse, and there exist some that can only be parsed by the server. It’s important that we send the right packets to avoid running into issues. If we fail to notice this difference, the worst that could happen is (a) the client gets kicked because it fails to parse a packet or (b) the client crashes, also failing to parse a packet.

All communication between the client and the server is through packets. PacketEvents allows us to read what’s being sent between the client and server. But it doesn’t end there. We also have the opportunity to alter data being sent. We will expand on more interesting features offered by PacketEvents at a later point. Now, are you ready to intercept some packets?

Here’s an example of how we could intercept a health-related Minecraft packet. Take a moment to read it through, and see if you understand what’s going on.

ExampleListener.java
import com.github.retrooper.packetevents.event.PacketListener;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateHealth;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ExampleListener implements PacketListener {
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.UPDATE_HEALTH) {
// health of the connected player was updated!
WrapperPlayServerUpdateHealth packet = new WrapperPlayServerUpdateHealth(event);
float health = packet.getHealth();
}
}
}

As you may have guessed, the packet processor above has a condition that checks whether the ‘Update Health’ (also known as ‘Set Health’) packet was sent. If that condition holds true, then the code parses the packet, obtaining the ‘health.’ At the time of writing, here’s how the Minecraft Protocol Wiki describes this packet:

A screenshot of the Minecraft Protocol Wiki, showcasing the clientbound set_health packet

The packet is sent by the server to the client, and it tells the client what its health should be. If we decided we wanted to trick the client (for educational purposes) and provide it with a false health value (such as 15), we could easily do so like this:

ExampleListener.java
import com.github.retrooper.packetevents.event.PacketListener;
import com.github.retrooper.packetevents.event.PacketSendEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.wrapper.play.server.WrapperPlayServerUpdateHealth;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ExampleListener implements PacketListener {
@Override
public void onPacketSend(PacketSendEvent event) {
if (event.getPacketType() == PacketType.Play.Server.UPDATE_HEALTH) {
// health of the connected player was updated!
WrapperPlayServerUpdateHealth packet = new WrapperPlayServerUpdateHealth(event);
packet.setHealth(15.0F); // range depends on maximum health of entity
event.markForReEncode(true); // tells packetevents to modify the packet
}
}
}

Let’s code a (slightly) more useful task using PacketEvents. The following code intercepts the ‘Attack’ and ‘Interact Entity’ packets. On Minecraft 26.1 and newer, the ‘Attack’ packet is sent by a player whenever they attack an entity, and the “Interact Entity” packet is sent whenever a player right clicks an entity. On Minecraft versions older than 26.1, it works a little differently. The “Interact Entity” packet is sent by a player whenever they left click or right click on an entity. The code in this example will support both legacy and modern versions of Minecraft. Once we detect that an entity has been attacked, we will send a message to the player informing them that they have attacked an entity:

ExampleListener.java
import com.github.retrooper.packetevents.event.PacketListener;
import com.github.retrooper.packetevents.event.PacketReceiveEvent;
import com.github.retrooper.packetevents.protocol.packettype.PacketType;
import com.github.retrooper.packetevents.protocol.player.User;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientAttack;
import com.github.retrooper.packetevents.wrapper.play.client.WrapperPlayClientInteractEntity;
import io.github.retrooper.packetevents.util.SpigotConversionUtil;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextDecoration;
import org.bukkit.entity.Entity;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class ExampleListener implements PacketListener {
@Override
public void onPacketReceive(PacketReceiveEvent event) {
User user = event.getUser();
// This code supports Minecraft 26.1 and newer.
if (event.getPacketType() == PacketType.Play.Client.ATTACK) {
WrapperPlayClientAttack attackPacket = new WrapperPlayClientAttack(event);
// Execute our code here
doSomething(user, attackPacket.getEntityId());
}
// This code works on Minecraft 1.8-1.21.11.
else if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
WrapperPlayClientInteractEntity.InteractAction action = packet.getAction();
// Check if the action is an ATTACK and not a right click
if (action == WrapperPlayClientInteractEntity.InteractAction.ATTACK) {
// Execute our code here
doSomething(user, packet.getEntityId());
}
}
}
public void doSomething(User user, int entityID) {
// Find the Bukkit entity that was attacked (WARNING: unsafe method!)
Entity entity = SpigotConversionUtil.getEntityById(null, entityID);
// Create a chat component with the Adventure API
Component message = Component.text("You attacked an entity.")
.hoverEvent(HoverEvent.hoverEvent(
HoverEvent.Action.SHOW_TEXT,
Component.text("Entity Name: " + entity.getName())
.color(NamedTextColor.GREEN)
.decorate(TextDecoration.BOLD)
.decorate(TextDecoration.ITALIC)));
// Send a message to the user using PacketEvents
user.sendMessage(message);
}
}

As you may have noticed, PacketEvents provided us with an entity ID. We used the conversion system to convert it into a Bukkit entity so we can assess the name of the entity. Lastly, we sent the message using the Adventure API (which is embedded in PacketEvents). The Adventure API is also available on all Paper-based software, you can learn more about it here. If you’re confused about the entity IDs and the conversion system, refer to this.

We’ve successfully written a packet listener! What’s next? We need to register the listener:

YourBukkitPlugin.java
import com.github.retrooper.packetevents.PacketEvents;
import com.github.retrooper.packetevents.event.EventManager;
import com.github.retrooper.packetevents.event.PacketListenerPriority;
import org.bukkit.plugin.java.JavaPlugin;
public class YourBukkitPlugin extends JavaPlugin {
@Override
public void onLoad() {
EventManager events = PacketEvents.getAPI().getEventManager();
events.registerListener(new ExampleListener(), PacketListenerPriority.NORMAL);
}
}