Skip to content

Networking & Protocol ​

Pumpkin includes a high-performance network stack supporting both Minecraft: Java Edition and Minecraft: Bedrock Edition.

Most protocol definitions, codecs, and serialization logic reside in the pumpkin-protocol crate, while connection handling and gameplay packet dispatching reside in the pumpkin crate.

NOTE

Detailed guides are available in the dedicated subcategories:


Direction & Protocol Terminology ​

  • Serverbound (Client → Server): Packets sent by the game client and received by Pumpkin. Prefixed with S (e.g., SChatMessage, SPlayerPosition).
  • Clientbound (Server → Client): Packets sent by Pumpkin to the game client. Prefixed with C (e.g., CPlayDisconnect, CBlockUpdate).

Protocol States ​

The Minecraft Java protocol operates as a finite state machine with five distinct states:

  1. Handshake: The initial packet sent when a client connects. It specifies the protocol version, server address, and the target next state (Status, Login, or Transfer).
  2. Status: Lightweight query state used for the multiplayer server list (MOTD, online player count, latency ping).
  3. Login: Authenticates the player (via Mojang Yggdrasil or offline mode), sets up encryption (AES-128/CFB8), and activates packet compression if enabled.
  4. Config: Introduced in modern Minecraft versions to configure client settings, negotiation of features, resource packs, and server links prior to gameplay.
  5. Play: The primary gameplay state where entities, world chunks, inventory interactions, chat, and movement are processed.

Defining Packets in pumpkin-protocol ​

Packets in pumpkin-protocol are organized by edition (java, bedrock), direction (client, server), and state (handshake, status, login, config, play).

1. Clientbound Packets (ClientPacket) ​

To create a clientbound packet:

  1. Retrieve the packet ID constant from pumpkin-data.
  2. Annotate the struct with #[java_packet(PACKET_CONSTANT)].
  3. Implement ClientPacket, writing the packet fields into the byte stream using NetworkWriteExt.
rust
use std::io::Write;
use pumpkin_data::packet::clientbound::play::DISCONNECT;
use pumpkin_macros::java_packet;
use pumpkin_protocol::ClientPacket;
use pumpkin_protocol::ser::{NetworkWriteExt, WritingError};
use pumpkin_util::text::TextComponent;
use pumpkin_util::version::JavaMinecraftVersion;

/// Disconnects the player while in the Play state.
#[java_packet(DISCONNECT)]
pub struct CPlayDisconnect<'a> {
    pub reason: &'a TextComponent,
}

impl<'a> CPlayDisconnect<'a> {
    pub const fn new(reason: &'a TextComponent) -> Self {
        Self { reason }
    }
}

impl ClientPacket for CPlayDisconnect<'_> {
    fn write_packet_data(
        &self,
        mut write: impl Write,
        version: &JavaMinecraftVersion,
    ) -> Result<(), WritingError> {
        // Write the text component formatted for the target client version
        write.write_component(self.reason, version)
    }
}

2. Serverbound Packets (ServerPacket) ​

To create a serverbound packet:

  1. Retrieve the packet ID constant from pumpkin-data.
  2. Annotate the struct with #[java_packet(PACKET_CONSTANT)].
  3. Implement ServerPacket<'a>, reading fields from the incoming byte slice with NetworkReadExt.
rust
use pumpkin_data::packet::serverbound::play::MOVE_PLAYER_POS;
use pumpkin_macros::java_packet;
use pumpkin_protocol::ServerPacket;
use pumpkin_protocol::ser::{NetworkReadExt, ReadingError};
use pumpkin_util::math::vector3::Vector3;
use pumpkin_util::version::JavaMinecraftVersion;

#[java_packet(MOVE_PLAYER_POS)]
pub struct SPlayerPosition {
    pub position: Vector3<f64>,
    pub collision: u8,
}

impl<'a> ServerPacket<'a> for SPlayerPosition {
    fn read(bytebuf: &mut &'a [u8], version: &JavaMinecraftVersion) -> Result<Self, ReadingError> {
        let x = bytebuf.get_f64_be()?;
        let y = bytebuf.get_f64_be()?;
        if *version <= JavaMinecraftVersion::V_1_7_6 {
            let _stance = bytebuf.get_f64_be()?;
        }
        let z = bytebuf.get_f64_be()?;
        let collision = bytebuf.get_u8()?;

        Ok(Self {
            position: Vector3::new(x, y, z),
            collision,
        })
    }
}

Useful Serialization Helpers (NetworkWriteExt & NetworkReadExt) ​

Pumpkin provides extension traits on byte streams for standard Minecraft primitives:

  • write_var_int(i32) / get_var_int(): Variable-length 32-bit integers.
  • write_var_long(i64) / get_var_long(): Variable-length 64-bit integers.
  • write_string(&str) / get_string(): UTF-8 string prefixed with a VarInt length.
  • write_uuid(&Uuid) / get_uuid(): 128-bit player or entity UUID.
  • write_component(&TextComponent): Serializes formatted chat/UI text.
  • write_nbt(&NBTTag) / get_nbt(): Serializes/deserializes binary NBT tags.

Packet Flow & Handling in pumpkin ​

Incoming packets pass through several stages before modifying game state:

  1. Frame Decoding: The Tokio TCP stream decodes the VarInt length prefix and decrypts the frame if encryption is enabled.
  2. Event Interception: Emits PacketReceivedEvent to registered plugins. If cancelled, the packet is immediately dropped.
  3. Version-Aware Dispatch: Maps the wire packet ID according to the client's negotiated protocol version.
  4. State Handling: Routes the parsed packet struct to its corresponding handler (e.g., handle_play_packet).
  5. State Mutation: Mutates player session, world chunks, or entity states.

1. Client vs Player ​

Pumpkin maintains a strict distinction between pre-play connections and active players:

  • Client: Represents a connection in Handshake, Status, Login, or Config state. Lightweight, not linked to a world, and has no physical entity. Handled in crates/pumpkin/src/net/java/mod.rs.
  • Player: Represents a living entity in an active World during the Play state. Holds inventory, health, velocity, and chunk subscription queues. Handled in crates/pumpkin/src/entity/player/.

2. Packet Dispatching (handle_play_packet) ​

Inside crates/pumpkin/src/net/java/mod.rs, packets are dispatched by matching against their version-specific packet IDs:

rust
pub fn handle_play_packet(
    &self,
    player: &Arc<Player>,
    server: &Arc<Server>,
    packet: &RawPacket,
) -> Result<(), Box<dyn PumpkinError>> {
    let version = self.version.load();

    // Fire plugin event to allow plugins to observe or cancel packets
    let mut event = PacketReceivedEvent::new(player.clone(), packet.id, packet.payload.clone());
    server.plugin_manager.fire_blocking(server, &mut event);
    if event.cancelled {
        return Ok(());
    }

    let mut payload = &event.payload[..];
    match event.packet_id {
        id if id == SConfirmTeleport::to_id(version) => {
            self.handle_confirm_teleport(player, &SConfirmTeleport::read(&mut payload, &version)?);
        }
        id if id == SChatCommand::to_id(version) => {
            let packet = SChatCommand::read(&mut payload, &version)?;
            self.handle_chat_command(player, server, &packet);
        }
        id if id == SPlayerPosition::to_id(version) => {
            let packet = SPlayerPosition::read(&mut payload, &version)?;
            self.handle_player_position(player, &packet);
        }
        _ => {
            trace!("Unhandled play packet id: {:#04x}", event.packet_id);
        }
    }
    Ok(())
}

3. Sending Packets ​

To send a packet to a client:

rust
// Directly send to a single player
player.client.send_packet(&CPlayDisconnect::new(&reason));

// Broadcast to nearby players in the world
world.broadcast_packet_except(
    &[player.gameprofile.id],
    &CBlockUpdate::new(block_pos, block_id),
).await;

Compression & Encryption ​

  • Compression: Once a client passes the Login state, if compression_threshold is enabled in pumpkin.toml, packets exceeding the threshold size are compressed using ZLib/fast compression.
  • Encryption: In online mode, 128-bit AES/CFB8 encryption is activated immediately after shared secret exchange in the Login state.

Released under the MIT License.