Pumpkin-Docs/docs/en/developer/networking/networking.md
Alexander Medvedev d219a313bc Better formatting
2025-08-11 22:42:58 +02:00

241 lines
6.4 KiB
Markdown

### Networking
Most of the networking code in Pumpkin can be found in the [pumpkin-protocol](https://github.com/Pumpkin-MC/Pumpkin/tree/master/pumpkin-protocol) crate.
Serverbound: Client→Server
Clientbound: Server→Client
### Structure
Packets in the Pumpkin protocol are organized by functionality and state.
`server`: Contains definitions for serverbound packets.
`client`: Contains definitions for clientbound packets.
### States
**Handshake**: Always the first packet being sent from the client. This also determines the next state, usually to indicate if the player wants to perform a status request, join the server, or wants to be transferred.
**Status**: Indicates the client wants to see a status response (MOTD).
**Login**: The login sequence. Indicates the client wants to join the server.
**Config**: A sequence of configuration packets is mostly sent from the server to the client (features, resource pack, server links, etc.).
**Play**: The final state, which indicates the player is now ready to join, is also used to handle all other gameplay packets.
### Minecraft Protocol
You can find all Minecraft Java packets at <https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol>. There you also can see which [state](#states) they are in.
You can also can see all the information the packets have, which we can either read or write depending on whether they are serverbound or clientbound.
### Adding a Packet
1. Adding a packet is easy. First, derive:
```rust
// For clientbound packets:
#[derive(Serialize)]
// For serverbound packets:
#[derive(Deserialize)]
```
2. Next, you have to make it known that your struct represents a packet. This automatically gets the packet ID from the JSON packets file.
```rust
use pumpkin_data::packet::clientbound::PLAY_DISCONNECT;
#[packet(PLAY_DISCONNECT)]
```
3. Now you can create the `struct`.
> [!IMPORTANT]
> Please start the packet name with "C" or "S" for "Clientbound" or "Serverbound".
> Also, if it's a packet that can be sent in multiple [states](#states), please add the state to the name. For example, there are 3 different disconnect packets.
>
> - `CLoginDisconnect`
> - `CConfigDisconnect`
> - `CPlayDisconnect`
Create fields within your packet structure to represent the data that will be sent.
> [!IMPORTANT]
> Use descriptive field names and appropriate data types.
Examples:
```rust
pub struct CPlayDisconnect {
reason: TextComponent,
// more fields...
}
pub struct SPlayerPosition {
pub x: f64,
pub feet_y: f64,
pub z: f64,
pub ground: bool,
}
```
4. (Clientbound packets only) `impl` a `new` function so we can actually create them by putting in the values.
```rust
impl CPlayDisconnect {
pub fn new(reason: TextComponent) -> Self {
Self { reason }
}
}
```
5. In the end, everything should come together.
```rust
#[derive(Serialize)]
#[packet(PLAY_DISCONNECT)]
pub struct CPlayDisconnect {
reason: TextComponent,
}
impl CPlayDisconnect {
pub fn new(reason: TextComponent) -> Self {
Self { reason }
}
}
#[derive(Deserialize)]
#[packet(PLAY_MOVE_PLAYER_POS)]
pub struct SPlayerPosition {
pub x: f64,
pub feet_y: f64,
pub z: f64,
pub ground: bool,
}
```
6. You can also serialize/deserialize the packet manually, which can be useful if the packet is more complex.
```diff
-#[derive(Serialize)]
+ impl ClientPacket for CPlayDisconnect {
+ fn write(&self, bytebuf: &mut BytesMut) {
+ bytebuf.put_slice(&self.reason.encode());
+ }
-#[derive(Deserialize)]
+ impl ServerPacket for SPlayerPosition {
+ fn read(bytebuf: &mut Bytes) -> Result<Self, ReadingError> {
+ Ok(Self {
+ x: bytebuf.try_get_f64()?,
+ feet_y: bytebuf.try_get_f64()?,
+ z: bytebuf.try_get_f64()?,
+ ground: bytebuf.try_get_bool()?,
+ })
+ }
```
7. You can now send the clientbound packet (see [Sending Packets](#sending-packets)) or listen for the serverbound packet (see [Receiving Packets](#receiving-packets)).
### Client
Pumpkin categorizes `Client`s and `Player`s separately. Everything that is not in the play state is a simple `Client`. Here are the differences:
#### Client
- Can only be the states: Status, Login, Transfer, Config
- Is not a living entity
- Has small resource consumption
#### Player
- Can only be in the Play state
- Is a living entity in a world
- Has more data and consumes more resources
#### Sending Packets
Example:
```rust
// Works only in the Status state
client.send_packet(&CStatusResponse::new("{ description: "A Description"}"));
```
#### Receiving Packets
For `Client`s:
`src/client/mod.rs`
```diff
// Put the packet into the right state
fn handle_mystate_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), ReadingError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SStatusRequest::PACKET_ID => {
self.handle_status_request(server, SStatusRequest::read(bytebuf)?)
.await;
}
+ MyPacket::PACKET_ID => {
+ self.handle_my_packet(MyPacket::read(bytebuf)?)
+ .await;
}
_ => {
log::error!(
"Failed to handle packet id {} while in ... state",
packet.id.0
);
}
};
Ok(())
}
```
For `Player`s:
`src/entity/player.rs`
```diff
// Players only have the Play state
fn handle_play_packet(
&self,
server: &Arc<Server>,
packet: &mut RawPacket,
) -> Result<(), ReadingError> {
let bytebuf = &mut packet.bytebuf;
match packet.id.0 {
SChatMessage::PACKET_ID => {
self.handle_chat_message(SChatMessage::read(bytebuf)?).await;
}
MyPacket::PACKET_ID => {
+ self.handle_mypacket(server, MyPacket::read(bytebuf)?).await;
}
_ => {
log::error!(
"Failed to handle packet id {} while in ... state",
packet.id.0
);
}
};
Ok(())
}
```
### Compression
Minecraft packets **can** use ZLib compression for decoding/encoding. There is usually a threshold set when compression is applied; this most often affects chunk packets.
### Porting
To port to a new minecraft version, you can compare differences in the protocol on the [minecraft.wiki Protocol reference](https://minecraft.wiki/w/Java_Edition_protocol).
Also, change the `CURRENT_MC_PROTOCOL` in `src/lib.rs`.