Introduction to Packet Listeners
Interception
Section titled “Interception”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.
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;
@NullMarkedpublic 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:
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:
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;
@NullMarkedpublic 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:
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;
@NullMarkedpublic 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.
What’s Next?
Section titled “What’s Next?”We’ve successfully written a packet listener! What’s next? We need to register the listener:
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); }}import com.github.retrooper.packetevents.PacketEvents;import com.github.retrooper.packetevents.event.PacketListenerPriority;import io.github.retrooper.packetevents.factory.spigot.SpigotPacketEventsBuilder;import org.bukkit.plugin.java.JavaPlugin;
public class YourBukkitPlugin extends JavaPlugin {
@Override public void onLoad() { // Building, loading, and initializing the library is necessary when bundling (or shading). PacketEvents.setAPI(SpigotPacketEventsBuilder.build(this)); PacketEvents.getAPI().load();
// Register our listener PacketEvents.getAPI().getEventManager().registerListener(new ExampleListener(), PacketListenerPriority.NORMAL); }
@Override public void onEnable() { // Initialize the library! PacketEvents.getAPI().init(); }
@Override public void onDisable() { // Clean-up process PacketEvents.getAPI().terminate(); }}