I'm writing Minecraft servers from scratch. Part 1. Ping
This series of articles is about the development of server software compatible with the Minecraft: Java Edition protocol.
I will not give many code examples here in the article; I will try to describe everything in words and diagrams. If you are interested in the result in the form of code, I have attached below a link to the repository on Github.
In this part I focus on the main things: data types, packet structure and how the client obtains information about the server
Links
All information about the Minecraft protocol is taken from the community of people who are engaged in reverse engineering and documenting the protocol.
mine-rs – My server repository is growing
mclib – Utilities for the Minecraft protocol, which I moved to a separate repository
The basis, so to speak, the base
People who have played Minecraft, and even more so who have at least set up a server to play with friends, will not learn anything new from this chapter, but for everyone else I will explain, perhaps it will be useful.
Minecraft has two independent versions of the game, developed separately within Mojang, a subsidiary of Microsoft:
Minecraft: Java Edition – the “Classic” version of the game, developed back in 2011 by Marcus “Notch” Person. It is developed, not surprisingly, in the Java programming language. It is for this version of the game that most mods and plugins are written. If you ever played Minecraft in the early days, you played this version.
Minecraft: Bedrock Edition (formerly Pocket Edition) – A newer version of the game, originally developed for smartphones, hence the name Pocket Edition, then it was renamed and ported to consoles and PCs.
Bedrock Edition is not at all interesting to us in the context of this article, since the server is being developed for compatibility with the Java Edition. These versions have different protocols. That is, the Java client will not be able to play on the same server with the Bedrock client.
Protocol
Before I talk about the protocol more specifically, I need to briefly explain such basic things as serialization of data types and the structure of sent packets.
Data types
Let's start with data types. In principle, there is nothing complicated here, let’s list the main ones:
Integer types data is serialized in order from the most significant to the least significant byte (big-endian). That is, an unsigned eight-bit unit will be serialized as
0000_0001
. The size in bytes and the presence of the “-” sign are determined by the contextFloating point numbers There are two types: float32 (float) and float64 (double). Just like with integer types, serialization occurs in little byte order, that is, from high order to low order
VarInt is an integer data type with variable length in bytes. That is, smaller numbers take up fewer bytes, but the maximum length of this type is limited to 5 bytes. In this data type, the 7 least significant bits are responsible for the value of the number, and the 8th bit indicates whether there is data in the next byte. For deserialization VarInt the first byte is taken from it, the 7 least significant bits are read (for example, using the bitwise AND operation
0x7F
) we write the resulting number into the result. Then we check the remaining eighth byte (for example, also using bitwise AND to0x80
) and if there1
, then we read the next byte again, calculate the 7 least significant bits from it and write it to the result, but 7 bits to the left. We repeat until the most significant bit contains0
. Serialization happens exactly the same, but in the opposite directionStrings serialized in UTF-8 with the string size in bytes prefixed as VarInt
VarInt serialization and deserialization example
/// Сериализация
fn pack(&self) -> Vec<u8> {
let mut value = self.0;
let mut result = Vec::new();
for _ in 0..100 {
if (value & !(SEGMENT_BITS as i32)) == 0 {
result.push(value as u8);
break;
}
result.push(((value as u8) & SEGMENT_BITS) | CONTINUE_BIT);
value >>= 7;
value &= i32::MAX >> 6;
}
result
}
/// Десериализация
fn unpack(src: &mut dyn Read) -> Self {
let mut value = 0i32;
for i in 0..5 {
let current_byte = src.read_byte();
value |= ((current_byte & SEGMENT_BITS) as i32) << (i * 7);
if (current_byte & CONTINUE_BIT) == 0 {
break;
}
}
Self(value)
}
Package structure
Let me make a reservation right away that the package structure described below refers to the server operating mode without compression. Packet compression is a separate topic and I have not yet implemented it in my project.
Any uncompressed Minecraft package consists of three main fields. These fields are not separated by special characters, they are simply in byte order.
Package length – VarInt field that indicates the size of the rest of the packet (packet ID + packet body) in bytes
Package ID – VarInt field with a unique identifier of the package structure. Thanks to it, you can determine which structure will be in the next field. This ID is unique for everyone connection stage. For example, in version 1.20.4, when connecting, the client sends a packet with the identifier
0x00
. This is how the server understands how to deserialize subsequent dataPackage body – the structure of the package body, which depends on connection stage and package ID
Connection steps
Package ID unique within each connection step. There are 5 stages in total:
Handshake – always the first stage of connection. In it, the client explains his intention: whether he wants to connect to the server and play or just wants to find out information about players online
Status – the connection enters this stage if the client requests information about the server: a brief description, the number of available places for players, the number of players online, and the like. After receiving this information, the connection is terminated.
Login – this stage is activated after a handshake if the client intends to enter the game; at this stage, information about packet compression and connection encryption is transmitted. After this, the configuration phase is activated
Configuration – at this stage, the server transmits to the client information about the game world, such information as the presence of certain biomes in the game. After this the game stage is activated
A game – the active game stage, when the player spawns in the game world
When the player opens the server selection window or clicks the “Update” button, the client creates a connection to each saved server address requesting a stage Status. In response, the server, if available, sends the client a description field, the number of players online and the maximum number of slots available for players. After receiving this information, the connection is terminated.
If the player presses the “Connect” button, then the client creates a connection requesting the step Login. The server and client exchange the necessary information and the player enters the game online.
Handshake
Any interaction between client and server begins at this stage. The client creates a TCP connection and sends the first packet to the server. At this stage there is only one package.
Handshake package – Client -> Server (Package ID – 0x00
)
Field | Type | Description |
---|---|---|
Protocol version | VarInt | Protocol version, which depends on the game version. Using this field, the client and server understand whether they are version compatible with each other. The current version of the game 1.20.4 has protocol version 765 |
Server address | String (max length 255 characters) | The hostname or IP address of the server to which the client connects. For example |
Server port | 16 bit unsigned integer | Server port |
Next stage of connection | VarInt | There are only two possible values in this field ( |
Status
The client indicated in Handshake package, which queries the server for its status and information about it. We are already at the connection stage Status after the previous package, because client indicated 1
in the last field. All switching between statuses occurs without sending additional packets, this is just an abstraction that separates some packets from others
Immediately after the handshake packet, the client sends another packet to the server.
Request status Client -> Server (Package ID – 0x00
)
This package does not have any payload. The client will only transmit the packet ID and packet length
In response to this, the server responds to the client with the following packet
Status response Server -> Client (Package ID – 0x00
)
Field | Type |
---|---|
Server information | JSON, serialized into a string Server information in JSON format having the structure described below, which has been serialized into a string. |
The server information has the following structure in JSON format
{
"version": {
"name": "1.20.4",
"protocol": 765
},
"players": {
"max": 32,
"online": 0,
"sample": [
{
"name": "player username",
"id": "00000000-0000-0000-0000-000000000000"
}
]
},
"description": {
"text": "Server description"
},
"favicon": "data:image/png;base64,<data>",
"enforcesSecureChat": true,
"previewsChat": true
}
In field version
the version supported by the server is transmitted. The client compares its protocol version with the one sent to version.protocol
and if they do not match, then information about the version mismatch is displayed in the list of servers.
In field players.max
the available number of players on the server is transmitted, and players.online
their current number is online.
As far as I know, the field players.sample
is not processed visually by the game client, so it is not necessary to pass honest data about the presence of players on the server; you can pass an empty array even with the value players.online
different from 0.
Field description
is a special structure that allows you to display formatted text in the server description field.
favicon
is an optional field that can contain a server icon. It should be a 64 by 64 PNG image encoded in base64.
What do the fields do? enforcesSecureChat
And previewsChat
I don't know. I simply did not use them in my code and the package was considered valid. So these fields are optional.
Bottom line
Thanks for reading! In the next part, I will describe in more detail the process of client authorization on the server and what needs to be done for the player to enter the game world.