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:
- Networking Introduction
- Java Edition: Architecture, Adding a Java Packet, and Authentication
- Bedrock Edition: Architecture and Adding a Bedrock Packet
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:
- Handshake: The initial packet sent when a client connects. It specifies the protocol version, server address, and the target next state (
Status,Login, orTransfer). - Status: Lightweight query state used for the multiplayer server list (MOTD, online player count, latency ping).
- Login: Authenticates the player (via Mojang Yggdrasil or offline mode), sets up encryption (AES-128/CFB8), and activates packet compression if enabled.
- Config: Introduced in modern Minecraft versions to configure client settings, negotiation of features, resource packs, and server links prior to gameplay.
- 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:
- Retrieve the packet ID constant from
pumpkin-data. - Annotate the struct with
#[java_packet(PACKET_CONSTANT)]. - Implement
ClientPacket, writing the packet fields into the byte stream usingNetworkWriteExt.
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:
- Retrieve the packet ID constant from
pumpkin-data. - Annotate the struct with
#[java_packet(PACKET_CONSTANT)]. - Implement
ServerPacket<'a>, reading fields from the incoming byte slice withNetworkReadExt.
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:
- Frame Decoding: The Tokio TCP stream decodes the VarInt length prefix and decrypts the frame if encryption is enabled.
- Event Interception: Emits
PacketReceivedEventto registered plugins. If cancelled, the packet is immediately dropped. - Version-Aware Dispatch: Maps the wire packet ID according to the client's negotiated protocol version.
- State Handling: Routes the parsed packet struct to its corresponding handler (e.g.,
handle_play_packet). - 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 inHandshake,Status,Login, orConfigstate. Lightweight, not linked to a world, and has no physical entity. Handled incrates/pumpkin/src/net/java/mod.rs.Player: Represents a living entity in an activeWorldduring thePlaystate. Holds inventory, health, velocity, and chunk subscription queues. Handled incrates/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:
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:
// 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_thresholdis enabled inpumpkin.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.