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
}
}
}

As shown, packet modification is also simple to implement with PacketEvents. Let’s code a (slightly) more useful task using PacketEvents. The following code intercepts the ‘Interact Entity’ packet, which is sent whenever the client interacts with or attacks an entity, and then sends a message to the client 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.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();
// Whenever the player sends an entity interaction packet.
if (event.getPacketType() == PacketType.Play.Client.INTERACT_ENTITY) {
WrapperPlayClientInteractEntity packet = new WrapperPlayClientInteractEntity(event);
WrapperPlayClientInteractEntity.InteractAction action = packet.getAction();
if (action == WrapperPlayClientInteractEntity.InteractAction.ATTACK) {
int entityID = packet.getEntityId();
// Find the Bukkit entity (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 it to the cross-platform user
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);
}
}