Switch Theme

Packet Handlers in NMS

minecraft dev nms

5/4/2024

13 min. read

NMS, better known as Net-Minecraft-Server, is a complicated and undocumented set of internals used in Minecraft. This post will go over the use of Packet Handlers, and why they are important in the context of NMS. I use Packet Handlers in many of my Minecraft plugins, especially the ones that are more complex. But what are they?

Introduction

The netty library is the backbone of Minecraft: Java Edition’s multiplayer system. It often looks something like this:

Channel ch = ...;

// Manage Pipeline
ch.pipeline().addLast("decoder", new PacketDecoder());
ch.pipeline().addLast("encoder", new PacketEncoder());

// Fire Events
ch.fireChannelActive();

// Write and Flush
if (ch.isActive()) {
    ch.writeAndFlush("gmitch215");
}

Packet Handlers heavily utilize the netty library to manage incoming and outgoing packets. They are used to intercept packets the player sends to the server to process by your plugin, instead of being thrown away from the server. This is useful for advanced functionality, like Sign GUIs and WASD-Rideable Entities.

Design

This tutorial will be using the official Mojang Mappings for the NMS classes, for simplicity sake, and for the fact that I don’t like the other mappings.

Part 1: PacketHandler Class

The PacketHandler class is the main class that you register to a player’s ServerCommonPacketListenerImpl class on the NMS side. Let’s first design the PacketHandler class:

Write your class statement, extending ChannelDuplexHandler

import io.netty.channel.ChannelDuplexHandler;

final class PacketHandler extends ChannelDuplexHandler {

For the purposes of a tutorial, we’re going to have a global map of player UUIDs to Predicates containing the packet. The purpose of this map is to map a function to a player, so that we can intercept packets from the player and process them.

import net.minecraft.network.protocol.Packet;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Predicate;

final class PacketHandler extends ChannelDuplexHandler {

    // Map of Player ID to Function
    private static final Map<UUID, Predicate<Packet<?>>> packetHandlers = new HashMap<>();

Next, create your constructor to accept a Player. Each player is going to have their own packet handler, so we want to be able to reference it later.

    private final Player p;

    public PacketHandler(Player p) {
        this.p = p;
    }

Last, we need to implement the channelRead function, which reads the packet from the handler context, tests the function that handled the packet, and then performs the usualy netty operations necessary so the client doesn’t disconnect.

import io.netty.channel.ChannelHandlerContext;
import org.bukkit.scheduler.BukkitRunnable;

import java.util.function.Predicate;

// ...

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (!(packetO instanceof Packet<?> packet)) { // Ensure the object is a Packet
            super.channelRead(ctx, packetO);
            return;
        }

        Predicate<Packet<?>> handler = PACKET_HANDLERS.get(p.getUniqueId()); // Get the registered function
        if (handler != null) new BukkitRunnable() {
            @Override
            public void run() {
                boolean success = handler.test(packet); // Runs the function
                if (success) PACKET_HANDLERS.remove(p.getUniqueId()); // Removes the function if it succeeds
            }
        }.runTask(plugin); // Run the action on the synchronous thread

        super.channelRead(ctx, packetO);
    }
}

Your final class should look something like this:

package mypackage;

import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import net.minecraft.network.protocol.Packet;
import org.bukkit.entity.Player;
import org.bukkit.scheduler.BukkitRunnable;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.function.Predicate;

final class PacketHandler extends ChannelDuplexHandler {

    public static final Map<UUID, Predicate<Packet<?>>> PACKET_HANDLERS = new HashMap<>();

    private final Player p; 

    public PacketHandler(Player p) {
        this.p = p;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (!(packetO instanceof Packet<?> packet)) {
            super.channelRead(ctx, packetO);
            return;
        }

        Predicate<Packet<?>> handler = PACKET_HANDLERS.get(p.getUniqueId());
        if (handler != null) new BukkitRunnable() {
            @Override
            public void run() {
                boolean success = handler.test(packet); // Runs the function
                if (success) PACKET_HANDLERS.remove(p.getUniqueId());
            }
        }.runTask(plugin);

        super.channelRead(ctx, packetO);
    }

}

Part 2: Registering the PacketHandler

Now that we have a PacketHandler class, we need to register it to our target player. This is done by getting the player’s ServerCommonPacketListenerImpl class, getting the netty channel object through a couple of fields (and reflection), and then injecting the object onto the Channel Pipeline. To do this, we need to declare a PACKET_INJECTOR_ID constant to put it on, then set it after the server decodes the packet, and therefore before the player’s natural packet handler.

Keep in mind that internal field names are subject to change, and you should always check the mappings before using them in your plugin. These names are compatible with Minecraft v1.20.4.

import io.netty.channel.Channel;
import net.minecraft.network.Connection;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerCommonPacketListenerImpl;
import org.bukkit.craftbukkit.{V}.entity.CraftPlayer;

import java.lang.reflect.Field;

// ...

public static final String PACKET_INJECTOR_ID = "mypacketinjectorid"

public void addPacketInjector(Player p) {
    ServerPlayer sp = ((CraftPlayer) p).getHandle();

    try {
        Field connection = ServerCommonPacketListenerImpl.class.getDeclaredField("c"); // Check your field name is correct!
        connection.setAccessible(true);
        Channel ch = ((Connection) connection.get(sp.connection)).channel;

        if (ch.pipeline().get(PACKET_INJECTOR_ID) != null) return; // Don't duplicate the handler
        ch.pipeline().addAfter("decoder", PACKET_INJECTOR_ID, new PacketHandler(p));
    } catch (ReflectiveOperationException e) {
        // handle errors
    }
}

You can call this function when the player joins the server:

@EventHandler
public void onJoin(PlayerJoinEvent e) {
    this.addPacketInjector(e.getPlayer());
}

Optional: Remove the Packet Handler

It’s best practice to remove the handler manually, and not have Minecraft deal with your custom stuff when a player leaves the server. Plus, if you ever need to turn it off, you can do so easily.

import io.netty.channel.Channel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.network.Connection;
import org.bukkit.craftbukkit.{V}.entity.CraftPlayer;

public void removePacketInjector(Player p) {
    ServerPlayer sp = ((CraftPlayer) p).getHandle();

    try {
        Field connection = ServerCommonPacketListenerImpl.class.getDeclaredField("c");
        connection.setAccessible(true);
        Channel ch = ((Connection) connection.get(sp.connection)).channel;

        if (ch.pipeline().get(PACKET_INJECTOR_ID) == null) return;
        ch.pipeline().remove(PACKET_INJECTOR_ID);
    } catch (ReflectiveOperationException e) {
        // handle errors
    }
}

You can call this function when the player leaves the server:

@EventHandler
public void onLeave(PlayerQuitEvent e) {
    this.removePacketInjector(e.getPlayer());
}

Part 3: Using the Packet Handler

Now let’s define some custom functionality, now that we can intercept packets that the player sends to the server.

Sign GUI

Many of my plugins require player text input from Signs. This function uses a Consumer<String[]> to handle the input from the player.

import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket;
import net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket;
import net.minecraft.network.protocol.game.ServerboundSignUpdatePacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.state.BlockState;

import java.util.function.Consumer;

// ...

public void sendSign(Player p, Consumer<String[]> lines) {
    addPacketInjector(p); // Ensure the injector is present (will do nothing if it already is)

    Location l = p.getLocation();
    BlockPos pos = new BlockPos(l.getBlockX(), l.getBlockY(), l.getBlockZ()); // Take note of where we want to place a sign
    BlockState old = ((CraftWorld) l.getWorld()).getHandle().getBlockState(pos);

    ClientboundBlockUpdatePacket sent1 = new ClientboundBlockUpdatePacket(pos, Blocks.OAK_SIGN.defaultBlockState()); // Place the sign
    ((CraftPlayer) p).getHandle().connection.send(sent1);

    ClientboundOpenSignEditorPacket sent2 = new ClientboundOpenSignEditorPacket(pos, true); // Open the sign editor
    ((CraftPlayer) p).getHandle().connection.send(sent2);

    PacketHandler.PACKET_HANDLERS.put(p.getUniqueId(), packetO -> { // Register the listener for the sign editor
        if (!(packetO instanceof ServerboundSignUpdatePacket packet)) return false; // Ensure the player sent a sign editor packet

        ClientboundBlockUpdatePacket sent3 = new ClientboundBlockUpdatePacket(pos, old); // Reset the block state to the original where we placed the sign
        ((CraftPlayer) p).getHandle().connection.send(sent3);

        lines.accept(packet.getLines()); // Handle the input from the signs
        return true;
    });
}

Sign input always has an array of 4 string, regardless of whether they are empty, so you can handle the input like this:

public void getInput(Player p) {
    this.sendSign(p, lines -> {
        p.sendMessage("You wrote this on the sign:");
        for (String line : lines) {
            p.sendMessage(line);
        }
    });
}

WASD-Rideable Entities

My BattleCards uses this feature to allow players to ride entities with WASD controls.

public void addPacketInjector(Player p) {
    // Previous handle injector code above

    PacketHandler.PACKET_HANDLERS.put(p.getUniqueID(), packetO -> {
        if (!(packetO instanceof ServerboundPlayerInputPacket packet)) return false;

        Entity vehicle = p.getVehicle();
        if (!(vehicle instanceof CraftCreature)) return false;

        CraftCreature creature = (CraftCreature) vehicle;
        creature.setRotation(p.getLocation().getYaw(), creature.getLocation().getPitch());
        Vector vector = p.getLocation().setPitch(0).getDirection().multiply(packet.getZ()).add(new Vector(0, 1, 0).crossProduct(p.getLocation().setPitch(0).getDirection()).multiply(packet.getX())).multiply(1.1);
        creature.getHandle().move(MoverType.SELF, new Vec3(vector.getX(), vector.getY(), vector.getZ()));
        return true;
    });
}

The original code in Kotlin:

override fun addPacketInjector(p: Player) {
    // Previous handle injector code above

    PacketHandler.PACKET_HANDLERS[p.uniqueId] = { packet ->
        if (packet is ServerboundPlayerInputPacket) {
            val vehicle = p.vehicle as? CraftCreature ?: return

            vehicle.setRotation(p.location.yaw, vehicle.location.pitch)
            val vector = (p.location.apply { pitch = 0F }.direction * packet.zza).plus(Vector(0, 1, 0).crossProduct(p.location.apply { pitch = 0F }.direction) * packet.xxa) * 1.1
            vehicle.handle.move(MoverType.SELF, Vec3(vector.x, vector.y, vector.z))
        }
    }
}

This code intercepts a ServerboundPlayerInputPacket packet, which is sent by the client when the player moves. It then moves the vehicle in the direction the player is facing, and uses the native move function based on a vector calculated from the player’s input.

Conclusion

Packet Handlers are a powerful tool in the NMS developer’s toolkit. They allow you to intercept packets sent by the player to the server, and process them in your plugin. This demo shows how to create a PacketHandler class, register it to a player, and use it to create custom functionality like Sign GUIs and WASD-Rideable Entities, using examples that I personally use in my plugins. Thank you for reading!