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.

Link to their wiki

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 context

  • Floating 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 to 0x80) and if there 1, 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 contains 0. Serialization happens exactly the same, but in the opposite direction

    Byte structure in VarInt

    Byte structure in VarInt

  • Strings 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.

  1. Package length – VarInt field that indicates the size of the rest of the packet (packet ID + packet body) in bytes

  2. 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 data

  3. Package 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:

  1. 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

  2. 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.

  3. 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

  4. 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

  5. A game – the active game stage, when the player spawns in the game world

    Diagram of connection stages

    Diagram of connection stages

    Server selection window

    Server selection window

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 packageClient -> 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 localhost or 127.0.0.1

Server port

16 bit unsigned integer

Server port

Next stage of connection

VarInt

There are only two possible values ​​in this field (1 or 2), which define the purpose for which the client connects. This field also determines which next stage the server will switch to:
1 – if the client requests information about the server. Next connection step Status
2 – if the client connects to the server to log in and then play on the server. The next stage of connection is Login

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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *