minecraft_protocol: foundation + per-version protocol docs 1.7.10->26.2
8 topical docs (overview, data types, lifecycle, handshake, status/ping, login+encryption, configuration, version-differences) + proxy-forwarding set + 16 per-version release-line docs, sourced from minecraft.wiki, ViaVersion (source + commits), minecraft-data, node-minecraft-protocol, Velocity, BungeeCord. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
/tmp/
|
||||||
|
*.tmp
|
||||||
+268
@@ -0,0 +1,268 @@
|
|||||||
|
# Minecraft Java Edition Protocol — Overview
|
||||||
|
|
||||||
|
> **Scope:** Java Edition only. Protocol details change with each release; version-specific differences are called out explicitly. "Latest" means 1.21.x as of mid-2026 unless noted.
|
||||||
|
>
|
||||||
|
> **Primary sources:**
|
||||||
|
> - `node-minecraft-protocol` v1.66.2 (`/tmp/mcproto-refs/node-minecraft-protocol/`)
|
||||||
|
> - minecraft.wiki/w/Java_Edition_protocol (formerly wiki.vg) — [Packet format](https://minecraft.wiki/w/Java_Edition_protocol) / [Data types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Transport
|
||||||
|
|
||||||
|
- **TCP**, no UDP (voice-chat plugins add their own UDP layer out-of-band).
|
||||||
|
- Default port: **25565** (configurable).
|
||||||
|
- All data is **big-endian** except VarInt/VarLong, which use little-endian 7-bit groups (see `01-data-types.md`).
|
||||||
|
- **Encryption** is applied after the Login handshake (online-mode servers). The cipher is AES-128-CFB8, using the shared secret as both key and IV. Encryption wraps the raw TCP byte stream; packet framing runs on top of the encrypted layer.
|
||||||
|
- Source: `node-minecraft-protocol/src/transforms/encryption.js:6-9` (`aes-128-cfb8`, key=IV=shared secret).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Packet Framing
|
||||||
|
|
||||||
|
Every packet on the wire is length-prefixed. There are two modes: **uncompressed** (before the server sends `Set Compression`) and **compressed** (after).
|
||||||
|
|
||||||
|
### 2.1 Uncompressed packet format
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|------------|---------|------------------------------------------------|
|
||||||
|
| Length | VarInt | Byte count of everything that follows (Packet ID + Data). |
|
||||||
|
| Packet ID | VarInt | State- and direction-scoped identifier. |
|
||||||
|
| Data | bytes | Payload; structure defined per packet. |
|
||||||
|
|
||||||
|
The `Framer` prepends the length:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/transforms/framing.js:16-21
|
||||||
|
const varIntSize = sizeOfVarInt(chunk.length)
|
||||||
|
const buffer = Buffer.alloc(varIntSize + chunk.length)
|
||||||
|
writeVarInt(chunk.length, buffer, 0)
|
||||||
|
chunk.copy(buffer, varIntSize)
|
||||||
|
```
|
||||||
|
|
||||||
|
The `Splitter` reads length, then slices exactly that many bytes — buffering partial reads across TCP segments (framing.js:48-73).
|
||||||
|
|
||||||
|
### 2.2 Compressed packet format
|
||||||
|
|
||||||
|
Enabled by the serverbound `Set Compression` packet (Login state). After that, **every** subsequent packet uses:
|
||||||
|
|
||||||
|
| Field | Type | Notes |
|
||||||
|
|----------------|---------|-----------------------------------------------------------------------------|
|
||||||
|
| Packet Length | VarInt | Byte count of `Data Length` field + compressed payload. |
|
||||||
|
| Data Length | VarInt | Uncompressed size of (Packet ID + Data). **0 = not compressed.** |
|
||||||
|
| Packet ID | VarInt | } zlib-deflated together when `Data Length > 0` |
|
||||||
|
| Data | bytes | } |
|
||||||
|
|
||||||
|
**Threshold rule:** if `len(Packet ID + Data) >= compressionThreshold`, compress and set `Data Length` to the uncompressed size. Otherwise send uncompressed with `Data Length = 0`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/transforms/compression.js:22-40
|
||||||
|
if (chunk.length >= this.compressionThreshold) {
|
||||||
|
const newChunk = zlib.deflateSync(chunk)
|
||||||
|
// prepend VarInt(chunk.length), then compressed bytes
|
||||||
|
} else {
|
||||||
|
// prepend VarInt(0), then raw bytes
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Historical note:** Compression was added in **1.8** (protocol 47). Per `server/login.js:179`, node-minecraft-protocol gates compression on `protocolVersion >= 27` (snapshot 14w28a), which predates the 1.8 release — whole-protocol compression replaced earlier per-packet compression in that snapshot.
|
||||||
|
|
||||||
|
### 2.3 Packet size limits
|
||||||
|
|
||||||
|
- Uncompressed maximum: **2²¹ − 1 = 2,097,151 bytes** (the `Length` VarInt may not exceed 3 bytes on the wire). — [minecraft.wiki](https://minecraft.wiki/w/Java_Edition_protocol)
|
||||||
|
- Serverbound compressed: uncompressed (Packet ID + Data) must be ≤ 2²³ bytes (8,388,608). <!-- VERIFY: wiki states this limit; not reflected in node-minecraft-protocol source -->
|
||||||
|
- Plugin message unrecognised-channel data: vanilla client caps at 1,048,576 bytes. <!-- VERIFY -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Connection State Machine
|
||||||
|
|
||||||
|
The protocol is divided into **states**. Each state has its own independent set of packet IDs. Transitions are triggered by specific packets.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/states.js:3-8
|
||||||
|
const states = {
|
||||||
|
HANDSHAKING: 'handshaking',
|
||||||
|
STATUS: 'status',
|
||||||
|
LOGIN: 'login',
|
||||||
|
CONFIGURATION: 'configuration', // added in 1.20.2 (protocol 764)
|
||||||
|
PLAY: 'play'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### State diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Handshaking : "TCP connect"
|
||||||
|
|
||||||
|
Handshaking --> Status : "Handshake (nextState=1)"
|
||||||
|
Handshaking --> Login : "Handshake (nextState=2)"
|
||||||
|
Handshaking --> Login : "Handshake (nextState=3, Transfer 1.20.5+)"
|
||||||
|
|
||||||
|
Status --> [*] : "Status Response + Ping/Pong"
|
||||||
|
|
||||||
|
Login --> Configuration : "Login Success → Login Acknowledged (1.20.2+)"
|
||||||
|
Login --> Play : "Login Success (pre-1.20.2)"
|
||||||
|
|
||||||
|
Configuration --> Play : "Finish Configuration (both sides)"
|
||||||
|
Play --> Configuration : "Start Configuration (server-initiated, 1.20.2+)"
|
||||||
|
Play --> [*] : "Disconnect"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 Handshaking
|
||||||
|
|
||||||
|
Initial state on connect. Client sends exactly one packet:
|
||||||
|
|
||||||
|
- **Handshake** (0x00, serverbound): carries `protocolVersion`, `serverHost`, `serverPort`, and `nextState` (1 = Status, 2 = Login, 3 = Transfer).
|
||||||
|
|
||||||
|
After this packet the server transitions immediately to the indicated next state.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/server/handshake.js:34-38
|
||||||
|
if (packet.nextState === 1) {
|
||||||
|
client.state = states.STATUS
|
||||||
|
} else if (packet.nextState === 2) {
|
||||||
|
client.state = states.LOGIN
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`nextState = 3` (Transfer) was added in **1.20.5** for server-transfer support. <!-- VERIFY exact protocol number for Transfer -->
|
||||||
|
|
||||||
|
### 3.2 Status
|
||||||
|
|
||||||
|
Server-list ping. Two round-trips:
|
||||||
|
|
||||||
|
1. Client → **Status Request** (0x00)
|
||||||
|
2. Server → **Status Response** (0x00) — JSON payload with MOTD, player count, favicon
|
||||||
|
3. Client → **Ping Request** (0x01) — 8-byte timestamp
|
||||||
|
4. Server → **Pong Response** (0x01) — echoes timestamp
|
||||||
|
|
||||||
|
Connection is then closed by the client. The server does not transition to another state.
|
||||||
|
|
||||||
|
### 3.3 Login
|
||||||
|
|
||||||
|
Authentication and encryption negotiation.
|
||||||
|
|
||||||
|
Typical online-mode sequence:
|
||||||
|
1. C→S: `Login Start` (username, UUID, optional profile-key signature)
|
||||||
|
2. S→C: `Encryption Request` (server ID, RSA public key, verify token)
|
||||||
|
3. C→S: `Encryption Response` (RSA-encrypted shared secret + verify token)
|
||||||
|
4. *(Both sides enable AES-128-CFB8 encryption)*
|
||||||
|
5. Server calls Mojang session-server to verify join
|
||||||
|
6. S→C: `Set Compression` (threshold; gates compressed mode) — added in **1.8** / snapshot 14w28a
|
||||||
|
7. S→C: `Login Success` (UUID, username, properties)
|
||||||
|
8. C→S: `Login Acknowledged` — triggers transition to Configuration (1.20.2+)
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/server/login.js:179
|
||||||
|
if (client.protocolVersion >= 27) {
|
||||||
|
client.write('compress', { threshold: 256 })
|
||||||
|
client.compressionThreshold = 256
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/server/login.js:189-193
|
||||||
|
if (client.supportFeature('hasConfigurationState')) {
|
||||||
|
client.once('login_acknowledged', onClientLoginAck)
|
||||||
|
} else {
|
||||||
|
client.state = states.PLAY
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Configuration (1.20.2+, protocol 764+)
|
||||||
|
|
||||||
|
Introduced in **1.20.2**. Entered after Login Acknowledged; used for:
|
||||||
|
- Registry codec data (dimension types, biomes, etc.)
|
||||||
|
- Feature flags
|
||||||
|
- Known packs negotiation
|
||||||
|
- Server brand plugin channel
|
||||||
|
|
||||||
|
Both client and server exchange `Finish Configuration` to exit this state.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/server/login.js:224-238 (onClientLoginAck)
|
||||||
|
client.state = states.CONFIGURATION
|
||||||
|
// ... send registry_data, then finish_configuration
|
||||||
|
client.once('finish_configuration', () => {
|
||||||
|
client.state = states.PLAY
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The server can re-enter Configuration from Play at any time by sending `Start Configuration` (serverbound: `Configuration Acknowledged` from client acknowledges the switch back).
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/client/play.js:43-44
|
||||||
|
client.on('start_configuration', () => enterConfigState())
|
||||||
|
// ...
|
||||||
|
if (client.state === states.PLAY) {
|
||||||
|
client.write('configuration_acknowledged', {})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.5 Play
|
||||||
|
|
||||||
|
Normal gameplay state. The bulk of all packets are Play-state packets. This state persists until disconnect or a server-initiated re-entry into Configuration.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Packet ID Scoping
|
||||||
|
|
||||||
|
Packet IDs are **not globally unique**. They are scoped by **both state and direction**:
|
||||||
|
|
||||||
|
- **State**: Handshaking / Status / Login / Configuration / Play
|
||||||
|
- **Direction**: serverbound (client → server) or clientbound (server → client)
|
||||||
|
|
||||||
|
A packet ID of `0x00` in Handshaking-serverbound (Handshake packet) is completely unrelated to `0x00` in Status-clientbound (Status Response), and so on. You need all three pieces — state, direction, ID — to unambiguously identify a packet.
|
||||||
|
|
||||||
|
The serializer key in node-minecraft-protocol reflects this:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/transforms/serializer.js:47-51
|
||||||
|
function createSerializer ({ state = states.HANDSHAKING, isServer = false, version, ... }) {
|
||||||
|
return new Serializer(
|
||||||
|
createProtocol(state, !isServer ? 'toServer' : 'toClient', version, ...),
|
||||||
|
'packet'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Concrete packet ID assignments live in `minecraft-data` (a separate package) and change between protocol versions. Always consult the version-specific protocol data, not hard-coded constants.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Encryption Layer Position
|
||||||
|
|
||||||
|
The pipeline (innermost to outermost on send) is:
|
||||||
|
|
||||||
|
```
|
||||||
|
Packet (ID + payload)
|
||||||
|
→ Compression (optional, zlib deflate)
|
||||||
|
→ Framing (VarInt length prefix)
|
||||||
|
→ Encryption (AES-128-CFB8, wraps the byte stream after framing)
|
||||||
|
→ TCP
|
||||||
|
```
|
||||||
|
|
||||||
|
Framing runs before encryption so the receiver can split frames while decrypting. Both directions use the same shared secret but independent cipher/decipher instances.
|
||||||
|
|
||||||
|
Source: `node-minecraft-protocol/src/transforms/encryption.js` — AES-128-CFB8 with `key = IV = sharedSecret`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Legacy Server List Ping
|
||||||
|
|
||||||
|
Clients predating the current framing (pre-1.7) send a `0xFE` byte as the first byte of the TCP stream. node-minecraft-protocol detects this and shims it into the modern VarInt-framed pipeline:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/transforms/framing.js:36-43
|
||||||
|
if (this.recognizeLegacyPing && this.buffer[0] === LEGACY_PING_PACKET_ID) {
|
||||||
|
// Prefix a VarInt-encoded packet ID so the deserializer can handle it
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a compatibility shim only; the modern protocol framing is always VarInt-prefixed as described in §2.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See `01-data-types.md` for wire type encodings (VarInt, Position, NBT, etc.).*
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
# Minecraft Java Edition Protocol — Wire Data Types
|
||||||
|
|
||||||
|
> **Scope:** Java Edition wire types only. Version-specific changes are called out inline.
|
||||||
|
>
|
||||||
|
> **Primary sources:**
|
||||||
|
> - `node-minecraft-protocol` v1.66.2 (`/tmp/mcproto-refs/node-minecraft-protocol/`)
|
||||||
|
> - minecraft.wiki/w/Java_Edition_protocol/[Data_types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. VarInt
|
||||||
|
|
||||||
|
Variable-length encoding for **signed 32-bit integers**. 1–5 bytes on the wire.
|
||||||
|
|
||||||
|
**Encoding rules:**
|
||||||
|
- Take 7 bits of the value (least significant group first).
|
||||||
|
- Set the MSB of each byte to `1` if more bytes follow, `0` on the final byte.
|
||||||
|
- Two's complement for negative numbers — no zigzag encoding — so all negative values use the full 5 bytes.
|
||||||
|
|
||||||
|
**Encoding table:**
|
||||||
|
|
||||||
|
| Decimal value | Hex bytes | Byte count |
|
||||||
|
|-----------------------|-----------------------------------|------------|
|
||||||
|
| 0 | `00` | 1 |
|
||||||
|
| 1 | `01` | 1 |
|
||||||
|
| 127 | `7F` | 1 |
|
||||||
|
| 128 | `80 01` | 2 |
|
||||||
|
| 255 | `FF 01` | 2 |
|
||||||
|
| 300 | `AC 02` | 2 |
|
||||||
|
| 25565 | `DD C7 01` | 3 |
|
||||||
|
| 2097151 | `FF FF 7F` | 3 |
|
||||||
|
| 2147483647 | `FF FF FF FF 07` | 5 |
|
||||||
|
| -1 | `FF FF FF FF 0F` | 5 |
|
||||||
|
| -2147483648 | `80 80 80 80 08` | 5 |
|
||||||
|
|
||||||
|
**Max bytes: 5.** Reading more than 5 bytes for a VarInt is a protocol error.
|
||||||
|
|
||||||
|
VarInt is used for: packet length, packet ID, string prefix, array length, and most integer fields in the protocol.
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Data_types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types); node-minecraft-protocol delegates VarInt to `protodef` (`src/datatypes/minecraft.js:6`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. VarLong
|
||||||
|
|
||||||
|
Variable-length encoding for **signed 64-bit integers**. Same algorithm as VarInt, extended to 10 bytes.
|
||||||
|
|
||||||
|
**Encoding rules:** identical to VarInt but processes all 64 bits.
|
||||||
|
|
||||||
|
| Decimal value | Hex bytes | Byte count |
|
||||||
|
|-----------------------------|---------------------------------------------------|------------|
|
||||||
|
| 0 | `00` | 1 |
|
||||||
|
| 9223372036854775807 | `FF FF FF FF FF FF FF FF 7F` | 9 |
|
||||||
|
| -1 | `FF FF FF FF FF FF FF FF FF 01` | 10 |
|
||||||
|
| -9223372036854775808 | `80 80 80 80 80 80 80 80 80 01` | 10 |
|
||||||
|
|
||||||
|
**Max bytes: 10.** Reading more than 10 bytes for a VarLong is a protocol error.
|
||||||
|
|
||||||
|
node-minecraft-protocol delegates VarLong read/write to the same VarInt routines (which handle BigInt):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/datatypes/minecraft.js:20-29
|
||||||
|
function readVarLong (buffer, offset) {
|
||||||
|
return readVarInt(buffer, offset)
|
||||||
|
}
|
||||||
|
function writeVarLong (value, buffer, offset) {
|
||||||
|
return writeVarInt(value, buffer, offset)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. String
|
||||||
|
|
||||||
|
UTF-8 encoded string, prefixed by its byte length as a **VarInt**.
|
||||||
|
|
||||||
|
```
|
||||||
|
[VarInt: byte length][UTF-8 bytes]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Length caps:**
|
||||||
|
- The spec defines a per-field maximum of `n` characters (UTF-16 code units, not bytes).
|
||||||
|
- The most common cap is **32,767** characters.
|
||||||
|
- Maximum byte capacity for a String(n): `(n × 3) + 3` bytes (worst-case UTF-8 + VarInt overhead).
|
||||||
|
- Characters above U+FFFF (surrogate pairs in UTF-16) count as **2** units toward the character cap, even though they encode as 4 bytes in UTF-8.
|
||||||
|
|
||||||
|
**Common caps by field:**
|
||||||
|
|
||||||
|
| Context | Max chars (UTF-16 units) |
|
||||||
|
|-------------------------------|--------------------------|
|
||||||
|
| General String(n) | up to 32,767 |
|
||||||
|
| Chat message (1.19+) | 256 |
|
||||||
|
| Player username | 16 |
|
||||||
|
| Identifier (Namespace:Path) | 32,767 (see §7) |
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Data_types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. UUID
|
||||||
|
|
||||||
|
128-bit UUID encoded as **two big-endian unsigned 64-bit integers**: most significant 64 bits first, then least significant 64 bits. Always **16 bytes** on the wire.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/datatypes/minecraft.js:32-37
|
||||||
|
function readUUID (buffer, offset) {
|
||||||
|
return {
|
||||||
|
value: UUID.stringify(buffer.slice(offset, 16 + offset)),
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/datatypes/compiler-minecraft.js:8-12
|
||||||
|
UUID: ['native', (buffer, offset) => {
|
||||||
|
return {
|
||||||
|
value: UUID.stringify(buffer.slice(offset, 16 + offset)),
|
||||||
|
size: 16
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
```
|
||||||
|
|
||||||
|
No dashes on the wire — raw 16 bytes. The `SizeOf` for UUID is the constant `16` (`compiler-minecraft.js:143`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Boolean
|
||||||
|
|
||||||
|
Single byte: `0x00` = false, `0x01` = true. Used as the presence flag in `Prefixed Optional` (see §12).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Position (Block Coordinates)
|
||||||
|
|
||||||
|
Encodes a block position (x, y, z) packed into a single **signed 64-bit integer** (8 bytes).
|
||||||
|
|
||||||
|
### Current format (1.14+, protocol 477+)
|
||||||
|
|
||||||
|
Bit layout (MSB to LSB):
|
||||||
|
|
||||||
|
```
|
||||||
|
Bits 63–38 : x (26 bits, signed)
|
||||||
|
Bits 37–12 : z (26 bits, signed)
|
||||||
|
Bits 11–0 : y (12 bits, signed)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Encode:**
|
||||||
|
```
|
||||||
|
value = ((x & 0x3FFFFFF) << 38) | ((z & 0x3FFFFFF) << 12) | (y & 0xFFF)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Decode:**
|
||||||
|
```
|
||||||
|
x = val >> 38
|
||||||
|
z = val << 26 >> 38
|
||||||
|
y = val << 52 >> 52
|
||||||
|
```
|
||||||
|
|
||||||
|
All three components are sign-extended from their field widths.
|
||||||
|
|
||||||
|
Valid ranges:
|
||||||
|
- x: −33,554,432 to 33,554,431
|
||||||
|
- z: −33,554,432 to 33,554,431
|
||||||
|
- y: −2,048 to 2,047
|
||||||
|
|
||||||
|
### Pre-1.14 format (historical, protocol ≤ 476)
|
||||||
|
|
||||||
|
Different bit layout — y occupied the middle 12 bits:
|
||||||
|
|
||||||
|
```
|
||||||
|
Bits 63–38 : x (26 bits)
|
||||||
|
Bits 37–26 : y (12 bits)
|
||||||
|
Bits 25–0 : z (26 bits)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Breaking change in 1.14** — any code parsing Position must branch on the protocol version. <!-- VERIFY exact protocol number 477 for 1.14 -->
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Data_types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Angle
|
||||||
|
|
||||||
|
Single **unsigned byte** representing a rotation angle.
|
||||||
|
|
||||||
|
```
|
||||||
|
angle_degrees = byte_value × (360 / 256)
|
||||||
|
```
|
||||||
|
|
||||||
|
One byte covers a full 360° turn in steps of 1/256 (≈ 1.406°). Used for entity facing direction in many packets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Identifier (Namespaced Location)
|
||||||
|
|
||||||
|
Encoded as a **String** (§3) with a max length of 32,767 characters, but with a constrained format:
|
||||||
|
|
||||||
|
```
|
||||||
|
namespace:path
|
||||||
|
```
|
||||||
|
|
||||||
|
- `namespace`: lowercase alphanumerics, `.`, `-`, `_` — regex `[a-z0-9.\-_]`
|
||||||
|
- `path`: lowercase alphanumerics, `.`, `-`, `_`, `/` — regex `[a-z0-9.\-_/]`
|
||||||
|
- Default namespace if omitted: `minecraft`
|
||||||
|
|
||||||
|
Examples: `minecraft:stone`, `my_mod:custom/block`.
|
||||||
|
|
||||||
|
Invalid Identifiers (wrong characters, missing colon) cause a disconnect.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Fixed-Width Integer Types
|
||||||
|
|
||||||
|
Standard big-endian integers used in many fields:
|
||||||
|
|
||||||
|
| Type | Bytes | Signed | Range |
|
||||||
|
|-------|-------|--------|-------------------------------------------|
|
||||||
|
| Byte | 1 | Yes | −128 to 127 |
|
||||||
|
| UByte | 1 | No | 0 to 255 |
|
||||||
|
| Short | 2 | Yes | −32,768 to 32,767 |
|
||||||
|
| UShort| 2 | No | 0 to 65,535 |
|
||||||
|
| Int | 4 | Yes | −2,147,483,648 to 2,147,483,647 |
|
||||||
|
| Long | 8 | Yes | −9,223,372,036,854,775,808 to max |
|
||||||
|
| Float | 4 | — | IEEE 754 single precision |
|
||||||
|
| Double| 8 | — | IEEE 754 double precision |
|
||||||
|
|
||||||
|
All in big-endian byte order.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. NBT (Named Binary Tag)
|
||||||
|
|
||||||
|
NBT is a typed binary tree format. On the network it appears in two forms:
|
||||||
|
|
||||||
|
### 10.1 Standard network NBT (pre-1.20.2)
|
||||||
|
|
||||||
|
A normal NBT compound, including the root tag's type byte, the compound tag ID (`0x0A`), a length-prefixed name string for the root compound, and then the compound body. Used in item slots, chunk data, etc.
|
||||||
|
|
||||||
|
### 10.2 Network NBT (1.20.2+, protocol 764+)
|
||||||
|
|
||||||
|
**Breaking change in 1.20.2:** the root compound's name string is **omitted** on the wire. The root tag type byte (`0x0A`) is still present, but where the name length + name bytes used to follow, there are now zero bytes before the compound body begins.
|
||||||
|
|
||||||
|
This affects any field typed as `nbt` in the protocol data for 1.20.2+. Pre-1.20.2 parsers that expect a root name will misparse post-1.20.2 data and vice versa.
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Data_types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types) — "Version 1.20.2 changed network NBT format by removing the root compound tag's name field during transmission."
|
||||||
|
|
||||||
|
### 10.3 Compressed NBT (item slots, pre-1.13)
|
||||||
|
|
||||||
|
Some older item slot fields used a length-prefixed gzip-compressed NBT blob. Length was an `Int16BE`; value of `-1` means empty/no NBT:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/datatypes/minecraft.js:51-70
|
||||||
|
function readCompressedNbt (buffer, offset) {
|
||||||
|
const length = buffer.readInt16BE(offset)
|
||||||
|
if (length === -1) return { size: 2 }
|
||||||
|
const compressedNbt = buffer.slice(offset + 2, offset + 2 + length)
|
||||||
|
const nbtBuffer = zlib.gunzipSync(compressedNbt)
|
||||||
|
return { size: length + 2, value: nbt.proto.read(nbtBuffer, 0, 'nbt').value }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This format was replaced by uncompressed NBT in later versions.
|
||||||
|
|
||||||
|
### 10.4 NBT in text components
|
||||||
|
|
||||||
|
Network text components use NBT:
|
||||||
|
- **String Tag** (`0x08`): for components containing only plain text.
|
||||||
|
- **Compound Tag** (`0x0A`): for components with formatting, translations, or other structure.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Array of X
|
||||||
|
|
||||||
|
An undelimited sequence of `X` elements whose count is **known from context** (e.g., a preceding `Length` field or the packet spec). No length prefix is encoded on the wire for a bare array.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Prefixed Array of X
|
||||||
|
|
||||||
|
A **VarInt** count followed by that many elements of type `X`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[VarInt: count][X][X]...[X]
|
||||||
|
```
|
||||||
|
|
||||||
|
Zero-length is valid (count = 0, zero following bytes).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Optional X
|
||||||
|
|
||||||
|
A field that is present or absent based on **context** — typically a flag in an earlier field or a feature of the packet variant. When absent, contributes zero bytes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Prefixed Optional X
|
||||||
|
|
||||||
|
A **Boolean** (§5) followed by an `X` value if the boolean is `true`:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Boolean: present][X if present]
|
||||||
|
```
|
||||||
|
|
||||||
|
If `present = false`, the field is completely absent (no bytes for X).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Enum
|
||||||
|
|
||||||
|
Enums are encoded using an **underlying wire type** (almost always VarInt) with values defined per-packet in the protocol spec. Receiving an undefined enum value typically causes the client or server to disconnect.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. BitSet
|
||||||
|
|
||||||
|
Length-prefixed bit array using 64-bit longs.
|
||||||
|
|
||||||
|
```
|
||||||
|
[VarInt: num_longs][Long][Long]...[Long]
|
||||||
|
```
|
||||||
|
|
||||||
|
Bit `i` is set when:
|
||||||
|
```
|
||||||
|
(Data[i / 64] & (1L << (i % 64))) != 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Longs are big-endian. Bit 0 is the LSB of the first long.
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Data_types](https://minecraft.wiki/w/Java_Edition_protocol/Data_types).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Fixed BitSet(n)
|
||||||
|
|
||||||
|
A bit array of exactly `n` bits, encoded as `⌈n / 8⌉` bytes (no length prefix).
|
||||||
|
|
||||||
|
Bit `i` is set when:
|
||||||
|
```
|
||||||
|
(Data[i / 8] & (1 << (i % 8))) != 0
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** the bit indexing is byte-based here, not long-based as in the prefixed BitSet. The two are **not** interchangeable.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. Miscellaneous Types in node-minecraft-protocol
|
||||||
|
|
||||||
|
| Type name | Encoding | Source |
|
||||||
|
|-------------------------|-------------------------------------------------------------------|--------|
|
||||||
|
| `restBuffer` | Raw bytes from current offset to end of packet buffer | `minecraft.js:100-113` |
|
||||||
|
| `entityMetadataLoop` | Repeated typed entries terminated by a sentinel byte (`endVal`) | `minecraft.js:116-133` |
|
||||||
|
| `topBitSetTerminatedArray` | Array where MSB of the first byte of each element signals continuation (MSB=1) or end (MSB=0) | `minecraft.js:152-169` |
|
||||||
|
| `compressedNbt` | Int16BE length + gzip-compressed NBT; −1 = absent | `minecraft.js:51-97` |
|
||||||
|
| `lpVec3` | Length-prefixed 3D vector (from `lpVec3.js`) | `minecraft.js:7` |
|
||||||
|
| `registryEntryHolder` | VarInt discriminant: 0 = inline entry follows, n>0 = registry ID (n-1) | `compiler-minecraft.js:45-54` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19. Version History Cheat Sheet
|
||||||
|
|
||||||
|
| Version | Protocol | Change |
|
||||||
|
|-------------|----------|--------------------------------------------------------------|
|
||||||
|
| 1.7.x | 4–5 | Baseline modern framing introduced (replaced legacy 0xFE ping) |
|
||||||
|
| 1.8 | 47 | Whole-packet compression (`Set Compression`) added |
|
||||||
|
| 1.9 | 107 | Protocol overhaul; many packet IDs changed |
|
||||||
|
| 1.14 | 477 | `Position` bit layout changed (y and z swapped) <!-- VERIFY exact protocol 477 --> |
|
||||||
|
| 1.20.2 | 764 | `Configuration` state added; NBT root compound name dropped from network NBT |
|
||||||
|
| 1.20.5 | 766 | `Transfer` intent (nextState=3) added to Handshake <!-- VERIFY --> |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*See `00-overview.md` for TCP transport, packet framing pipeline, and the connection state machine.*
|
||||||
@@ -0,0 +1,287 @@
|
|||||||
|
# Connection Lifecycle — Java Edition Protocol State Machine
|
||||||
|
|
||||||
|
> **Scope**: 1.7.10 through current (26.1 / protocol 775). Version-specific divergences are called out inline.
|
||||||
|
> **Connective-tissue doc**: per-phase detail lives in `03-handshake.md`, `04-status.md`, `05-login.md`, `06-configuration.md`, `07-play.md`. This doc covers the state machine, transition triggers, and the ordered exchange that takes a raw TCP socket to in-game.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. States
|
||||||
|
|
||||||
|
The protocol defines five named states. Each state carries its own packet-ID namespace (IDs are reused across states):
|
||||||
|
|
||||||
|
| State | Integer ID | Direction of entry | Purpose |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **Handshaking** | (implicit, initial) | TCP connect | One packet; chooses the next state |
|
||||||
|
| **Status** | 1 | via Handshake intent=1 | Server-list ping/pong; no auth |
|
||||||
|
| **Login** | 2 | via Handshake intent=2 (or 3) | Auth, encryption, compression, identity |
|
||||||
|
| **Configuration** | — (no integer) | after Login Acknowledged (≥1.20.2) | Registry sync, feature flags, resource packs |
|
||||||
|
| **Play** | — | after Finish Configuration (or Login Success <1.20.2) | Gameplay |
|
||||||
|
|
||||||
|
Sources:
|
||||||
|
- State names: `node-minecraft-protocol/src/states.js:3-9`
|
||||||
|
- Velocity enum: `Velocity/proxy/…/StateRegistry.java:140-263` (`HANDSHAKE`, `STATUS`, `CONFIG`, `PLAY`, `LOGIN`)
|
||||||
|
- Integer IDs (STATUS_ID=1, LOGIN_ID=2, TRANSFER_ID=3): `StateRegistry.java:881-883`
|
||||||
|
- Intent=3 (Transfer login, ≥1.20.5): `minecraft.wiki/w/Java_Edition_protocol/Packets`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. State Machine Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
TCP connect
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────┐
|
||||||
|
│ HANDSHAKING │ (one packet: set_protocol / Handshake 0x00)
|
||||||
|
└──────┬───────┘
|
||||||
|
│ intent field
|
||||||
|
├─ 1 ──────────────────────────┐
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
┌─────────┐ ┌─────────┐
|
||||||
|
│ STATUS │ │ LOGIN │
|
||||||
|
└────┬────┘ └────┬────┘
|
||||||
|
│ ping done / disconnect │ Login Success + LoginAcknowledged
|
||||||
|
▼ │ (≥1.20.2 only)
|
||||||
|
[close] ┌───┴──────────────┐
|
||||||
|
│ CONFIGURATION │ (≥1.20.2 / protocol 764)
|
||||||
|
└───┬──────────────┘
|
||||||
|
│ Finish Configuration
|
||||||
|
│ ┌─────────────────────┐
|
||||||
|
▼ ◄─────────────┤ Start Configuration │
|
||||||
|
┌────────┐ │ (Play→Config re-cfg) │
|
||||||
|
│ PLAY ├────────────►┘ │
|
||||||
|
└────────┘ └─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pre-1.20.2 path**: Login Success → PLAY directly (no Configuration state, no Login Acknowledged).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Handshake: How next-state is chosen
|
||||||
|
|
||||||
|
The very first packet sent on a new TCP connection is the serverbound **Handshake** (packet ID `0x00` in the HANDSHAKING state, stable since 1.7.2).
|
||||||
|
|
||||||
|
Fields:
|
||||||
|
- `protocolVersion` — VarInt, the client's protocol number
|
||||||
|
- `serverHost` — String (max 255), the address the client typed
|
||||||
|
- `serverPort` — Unsigned Short
|
||||||
|
- `nextState` — VarInt enum:
|
||||||
|
- `1` → STATUS
|
||||||
|
- `2` → LOGIN
|
||||||
|
- `3` → LOGIN after server transfer (≥1.20.5 / protocol 766) <!-- VERIFY exact version of Transfer -->
|
||||||
|
|
||||||
|
The server reads `nextState` and immediately switches its decoder to the corresponding state. There is no server response in the Handshaking state.
|
||||||
|
|
||||||
|
Source: `node-minecraft-protocol/src/server/handshake.js:34-38`
|
||||||
|
```js
|
||||||
|
if (packet.nextState === 1) {
|
||||||
|
client.state = states.STATUS
|
||||||
|
} else if (packet.nextState === 2) {
|
||||||
|
client.state = states.LOGIN
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The client side always sends `nextState: 2` for normal login:
|
||||||
|
`node-minecraft-protocol/src/client/setProtocol.js:21-27`
|
||||||
|
```js
|
||||||
|
client.write('set_protocol', {
|
||||||
|
protocolVersion: options.protocolVersion,
|
||||||
|
serverHost: taggedHost,
|
||||||
|
serverPort: options.port,
|
||||||
|
nextState: 2
|
||||||
|
})
|
||||||
|
client.state = states.LOGIN
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Status Path (Server List Ping)
|
||||||
|
|
||||||
|
Used by the client (and external tools) to read MOTD, player count, and favicon without authenticating.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>S: "Handshake (0x00, nextState=1)"
|
||||||
|
Note over C,S: Both switch to STATUS state
|
||||||
|
C->>S: "Status Request (0x00)"
|
||||||
|
S->>C: "Status Response (0x00) — JSON payload"
|
||||||
|
C->>S: "Ping Request (0x01) — Long timestamp"
|
||||||
|
S->>C: "Pong Response (0x01) — echo timestamp"
|
||||||
|
Note over C,S: Server closes connection
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- The Ping Request / Pong Response round-trip is optional but used by vanilla to measure latency.
|
||||||
|
- No encryption, no compression, no auth in this path.
|
||||||
|
- Server handler: `node-minecraft-protocol/src/server/ping.js:7-64` — writes `server_info` then echoes `ping` then ends.
|
||||||
|
- Velocity STATUS registration: `StateRegistry.java:148-161` — `StatusRequestPacket` 0x00, `StatusPingPacket` 0x01 (both directions).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Login Path
|
||||||
|
|
||||||
|
### 5a. Pre-1.20.2 (protocol < 764): Login → Play directly
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>S: "Handshake (0x00, nextState=2)"
|
||||||
|
Note over C,S: Both switch to LOGIN state
|
||||||
|
C->>S: "Login Start (0x00) — username + UUID"
|
||||||
|
Note over S: online-mode check
|
||||||
|
S->>C: "Encryption Request (0x01) — serverId, publicKey, verifyToken [online-mode only]"
|
||||||
|
Note over C: Mojang auth (joinServer call)
|
||||||
|
C->>S: "Encryption Response (0x01) — encrypted sharedSecret + verifyToken"
|
||||||
|
Note over C,S: Both enable AES-128-CFB8 encryption immediately
|
||||||
|
Note over S: Yggdrasil hasJoined verification
|
||||||
|
S->>C: "Set Compression (0x03) — threshold [optional, >=1.8]"
|
||||||
|
Note over C,S: All subsequent packets use compressed format if enabled
|
||||||
|
S->>C: "Login Success (0x02) — UUID + username + properties"
|
||||||
|
Note over C,S: Both switch to PLAY state
|
||||||
|
S->>C: "Join Game (Play 0x01/varies) — game state, dimension, etc."
|
||||||
|
```
|
||||||
|
|
||||||
|
Key sequencing rules (pre-1.20.2):
|
||||||
|
- **Set Compression must precede Login Success.** Anything sent after Set Compression (including Login Success itself) uses the compressed packet format. Source: minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol
|
||||||
|
- **Encryption activates immediately** after the server receives Encryption Response — both sides flip the AES cipher before the next byte. Source: `node-minecraft-protocol/src/client/encrypt.js:73` (`client.setEncryption(sharedSecret)` called before returning from handler); `node-minecraft-protocol/src/server/login.js:150` (server side, same).
|
||||||
|
- Offline-mode servers skip Encryption Request/Response entirely (`node-minecraft-protocol/src/server/login.js:88-106`).
|
||||||
|
- Set Compression was added in 1.8 (protocol 47). Prior versions (1.7.x) have no compression packet; per-packet compression existed in very early snapshots but was dropped. Source: `StateRegistry.java:869-870` (`SetCompressionPacket` mapped from `MINECRAFT_1_8`).
|
||||||
|
|
||||||
|
### 5b. 1.20.2+ (protocol ≥ 764): Login → Configuration → Play
|
||||||
|
|
||||||
|
The big change in 1.20.2 inserted the **Configuration** state between Login and Play. The gate is `LoginAcknowledged` — the client must explicitly acknowledge Login Success before Configuration begins.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>S: "Handshake (0x00, nextState=2)"
|
||||||
|
Note over C,S: Both switch to LOGIN state
|
||||||
|
C->>S: "Login Start (0x00) — username + UUID"
|
||||||
|
S->>C: "Encryption Request (0x01) [online-mode only]"
|
||||||
|
Note over C: Mojang auth
|
||||||
|
C->>S: "Encryption Response (0x01)"
|
||||||
|
Note over C,S: AES-128-CFB8 encryption enabled
|
||||||
|
S->>C: "Set Compression (0x03) [optional]"
|
||||||
|
Note over C,S: Compressed format enabled if threshold >= 0
|
||||||
|
S->>C: "Login Success (0x02) — UUID + username + properties"
|
||||||
|
C->>S: "Login Acknowledged (0x03) [NEW in 1.20.2]"
|
||||||
|
Note over C,S: Both switch to CONFIGURATION state
|
||||||
|
S->>C: "Registry Data (0x05/0x07) — codec entries"
|
||||||
|
S->>C: "Feature Flags (0x0C) — enabled experiments"
|
||||||
|
S->>C: "Known Packs (0x0E) [>=1.20.5]"
|
||||||
|
C->>S: "Known Packs (0x07) — client's pack list [>=1.20.5]"
|
||||||
|
S->>C: "Tags (0x0D/varies) — tag registry"
|
||||||
|
S->>C: "Finish Configuration (0x02/0x03)"
|
||||||
|
C->>S: "Acknowledge Finish Configuration (0x02/0x03)"
|
||||||
|
Note over C,S: Both switch to PLAY state
|
||||||
|
S->>C: "Login / Join Game (Play 0x29/varies)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Sources:
|
||||||
|
- `LoginAcknowledgedPacket` registered serverbound at `0x03` from `MINECRAFT_1_20_2`: `StateRegistry.java:853-854`
|
||||||
|
- Server side transition: `node-minecraft-protocol/src/server/login.js:189-239` — on `login_acknowledged`, server switches to `states.CONFIGURATION`, sends `registry_data`, then `finish_configuration`; client acks with `finish_configuration`, server switches to `states.PLAY`
|
||||||
|
- Client side: `node-minecraft-protocol/src/client/play.js:40-43` — on `success`, writes `login_acknowledged` and calls `enterConfigState()`
|
||||||
|
- `KnownPacksPacket` registered in CONFIG from `MINECRAFT_1_20_5`: `StateRegistry.java:193-194, 249-250`
|
||||||
|
- Velocity CONFIG state registration starts at `MINECRAFT_1_20_2` throughout: `StateRegistry.java:163-261`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Configuration State
|
||||||
|
|
||||||
|
**Introduced**: 1.20.2, protocol 764. Source: `StateRegistry.java:163` (first CONFIG packet mapped from `MINECRAFT_1_20_2`); minecraft.wiki/w/Java_Edition_protocol/Packets.
|
||||||
|
|
||||||
|
Purpose: synchronise server-side data (registry codec, feature flags, resource packs, tags, known data packs) to the client before any gameplay packets flow. Replacing what was previously stuffed into the Login sequence or early Play packets.
|
||||||
|
|
||||||
|
Key packets (clientbound unless noted):
|
||||||
|
- `Plugin Message` — channel-based custom data, same as Play
|
||||||
|
- `Disconnect` — kick during config
|
||||||
|
- `Keep Alive` — connection liveness check
|
||||||
|
- `Registry Data` — serialised registry codec entries (dimension types, biomes, damage types, etc.)
|
||||||
|
- `Feature Flags` — experimental feature toggles
|
||||||
|
- `Resource Pack Request` — push a resource pack
|
||||||
|
- `Tags Update` — tag data (formerly in Play)
|
||||||
|
- `Known Packs` (≥1.20.5) — bidirectional; server asks, client responds with list of known data packs to skip redundant sync
|
||||||
|
- `Finish Configuration` (clientbound) → `Acknowledge Finish Configuration` (serverbound) — the handshake that exits Configuration
|
||||||
|
|
||||||
|
Full CONFIG packet table: `StateRegistry.java:163-261`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Re-configuration: Play → Configuration
|
||||||
|
|
||||||
|
**Since 1.20.2.** A connected player can be sent back to Configuration state without disconnecting. This allows servers to push updated registries (e.g., when switching between sub-servers with different dimensions).
|
||||||
|
|
||||||
|
Trigger: server sends **Start Configuration** (`StartUpdatePacket`) in the Play state.
|
||||||
|
- Velocity: `StateRegistry.java:805-813`, registered from `MINECRAFT_1_20_2`
|
||||||
|
|
||||||
|
Client responds: **Acknowledge Configuration** (serverbound Play packet, Velocity name `FinishedUpdatePacket`)
|
||||||
|
- Velocity: `StateRegistry.java:407-413`, registered from `MINECRAFT_1_20_2`
|
||||||
|
|
||||||
|
Both sides switch back to Configuration state. The full Configuration exchange runs again (registry data, finish handshake). On `Acknowledge Finish Configuration`, both switch back to Play.
|
||||||
|
|
||||||
|
Client-side handling in node-minecraft-protocol:
|
||||||
|
```js
|
||||||
|
// node-minecraft-protocol/src/client/play.js:43
|
||||||
|
client.on('start_configuration', () => enterConfigState())
|
||||||
|
// ...
|
||||||
|
// play.js:51-53: if state===PLAY, write 'configuration_acknowledged' before switching
|
||||||
|
if (client.state === states.PLAY) {
|
||||||
|
client.write('configuration_acknowledged', {})
|
||||||
|
}
|
||||||
|
client.state = states.CONFIGURATION
|
||||||
|
```
|
||||||
|
Source: `node-minecraft-protocol/src/client/play.js:43-55`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Encryption Details
|
||||||
|
|
||||||
|
- **Algorithm**: AES-128-CFB8, symmetric, both directions use the same 16-byte shared secret.
|
||||||
|
- **Key exchange**: RSA (server's ephemeral keypair, ≥1024-bit); client encrypts shared secret and verify token with server's public key.
|
||||||
|
- **Activation point**: immediately after Encryption Response is sent/received — before the next byte on the wire. Encryption applies to all subsequent data including Set Compression and Login Success.
|
||||||
|
- **Online-mode only** (vanilla): offline-mode servers skip the Encryption Request/Response entirely. As of 1.21, vanilla offline-mode servers do not use encryption. <!-- VERIFY: whether any version change altered this for 1.21 specifically -->
|
||||||
|
- Source: `node-minecraft-protocol/src/client/encrypt.js` (client), `node-minecraft-protocol/src/server/login.js:88-155` (server)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Compression Details
|
||||||
|
|
||||||
|
- **Algorithm**: zlib deflate, wrapped in the compressed packet format (prepended with uncompressed data length as VarInt).
|
||||||
|
- **Threshold**: packets with uncompressed size ≥ threshold are compressed; smaller packets are sent uncompressed (length VarInt = 0).
|
||||||
|
- **Activation point**: Set Compression (Login 0x03) must arrive before Login Success. All subsequent packets — including Login Success — use the compressed format. Source: minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol
|
||||||
|
- **Version gating**: added in 1.8 (protocol 47). Not present in 1.7.x. Source: `StateRegistry.java:869-870`
|
||||||
|
- Default threshold used by node-minecraft-protocol: 256 bytes. Source: `node-minecraft-protocol/src/server/login.js:179`
|
||||||
|
- Client receives `compress` / `set_compression` event: `node-minecraft-protocol/src/client/compress.js:3-8`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Protocol Version Reference
|
||||||
|
|
||||||
|
| Minecraft version | Protocol number | Notable state-machine change |
|
||||||
|
|---|---|---|
|
||||||
|
| 1.7.2 | 4 | Baseline modern protocol; Handshake/Status/Login/Play |
|
||||||
|
| 1.8 | 47 | Set Compression (0x03 in Login) added |
|
||||||
|
| 1.13 | 393 | Login Plugin Message (0x04) added |
|
||||||
|
| 1.20.2 | 764 | **Configuration state inserted**; Login Acknowledged; re-configuration |
|
||||||
|
| 1.20.5 | 766 | Known Packs exchange in Configuration; Cookie packets; Transfer login intent=3 |
|
||||||
|
| 1.21 | 767 | (Config/Play packet renumbering) |
|
||||||
|
| 1.21.2 | 769 | (further renumbering) |
|
||||||
|
| 26.1 | 775 | Current as of 2026-06 |
|
||||||
|
|
||||||
|
Sources: Velocity `StateRegistry.java` import block (lines 21-50); minecraft.wiki/w/Java_Edition_protocol/Packets (protocol 773/775 noted).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## VERIFY flags
|
||||||
|
|
||||||
|
<!-- VERIFY: intent=3 (Transfer) exact minimum version — wiki says ≥1.20.5 (protocol 766); Velocity TRANSFER_ID=3 at StateRegistry.java:883 consistent, but the Transfer login packet path vs. the Transfer clientbound packet in Play/Config should be distinguished. -->
|
||||||
|
<!-- VERIFY: "offline-mode servers do not use encryption as of 1.21" — wiki claim; node-minecraft-protocol skips encryption if !needToVerify regardless of version, which is correct, but the "as of 1.21" qualifier may be wiki-specific. -->
|
||||||
|
<!-- VERIFY: exact packet IDs for Configuration-state packets shift between 1.20.2, 1.20.3, 1.20.5 — the table at StateRegistry.java:163-261 is authoritative for Velocity's mapping but wiki IDs may differ for vanilla. -->
|
||||||
+228
@@ -0,0 +1,228 @@
|
|||||||
|
# 03 — Handshake Packet
|
||||||
|
|
||||||
|
**State:** Handshaking → Login or Status
|
||||||
|
**Direction:** Client → Server
|
||||||
|
**Packet ID:** `0x00`
|
||||||
|
**Stable since:** 1.7.2
|
||||||
|
|
||||||
|
The Handshake packet is the first packet every modern Minecraft client sends. It is sent exactly once per TCP connection, transitions the protocol state, and carries three pieces of metadata that have been overloaded with proxy and modloader signalling data over the years.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Packet fields
|
||||||
|
|
||||||
|
| # | Field | Type | Notes |
|
||||||
|
|---|-------|------|-------|
|
||||||
|
| 1 | Protocol Version | VarInt | Negotiated version; e.g. 47 = 1.8, 340 = 1.12.2, 765 = 1.20.4, 766 = 1.20.5, 775 = 26.1 |
|
||||||
|
| 2 | Server Address | String (255) | Hostname/IP the client used to connect — **subject to all the overloads below** |
|
||||||
|
| 3 | Server Port | Unsigned Short | Default 25565 |
|
||||||
|
| 4 | Intent (Next State) | VarInt enum | `1` = Status, `2` = Login, `3` = Transfer *(1.20.5 / protocol 766+)* |
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets) (Handshaking, C→S, 0x00); field definitions confirmed in `node-minecraft-protocol/src/client/setProtocol.js:21-26` (`protocolVersion`, `serverHost`, `serverPort`, `nextState`); packet schema in `minecraft-data/data/pc/1.8/protocol.json` (`packet_set_protocol`).
|
||||||
|
|
||||||
|
### Intent / Next State values
|
||||||
|
|
||||||
|
| Value | Meaning | Introduced |
|
||||||
|
|-------|---------|------------|
|
||||||
|
| `1` | Status ping | 1.7 |
|
||||||
|
| `2` | Login (play) | 1.7 |
|
||||||
|
| `3` | Transfer login | 1.20.5 (protocol 766) |
|
||||||
|
|
||||||
|
Intent `3` indicates the client arrived via a `Transfer` packet from another server (cookie-based server transfer introduced in 1.20.5). Both `2` and `3` transition into the Login protocol state; `3` lets the receiving server decide to accept or reject transfers. BungeeCord sets `transferred = true` at `InitialHandler.java:391` when intent is `3`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## SRV record resolution
|
||||||
|
|
||||||
|
Before opening the TCP connection the client queries `_minecraft._tcp.<domain>` for a DNS SRV record. If found, the resolved hostname **and port** replace the raw user input:
|
||||||
|
|
||||||
|
- `options.host` and `options.port` are updated from the SRV response before `setSocket()` is called.
|
||||||
|
- The **Server Address field sent in the Handshake packet** is the SRV-resolved hostname, not the original domain the user typed.
|
||||||
|
|
||||||
|
Source: `node-minecraft-protocol/src/client/tcp_dns.js:21-33`
|
||||||
|
|
||||||
|
```js
|
||||||
|
// tcp_dns.js:21-33
|
||||||
|
dns.resolveSrv('_minecraft._tcp.' + options.host, (err, addresses) => {
|
||||||
|
if (addresses && addresses.length > 0) {
|
||||||
|
options.host = addresses[0].name // <-- replaces host
|
||||||
|
options.port = addresses[0].port // <-- replaces port
|
||||||
|
client.setSocket(net.connect(addresses[0].port, addresses[0].name))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Practical consequence: if `play.example.com` has an SRV record pointing to `mc1.example.com:19132`, the Handshake's Server Address field will contain `mc1.example.com`, not `play.example.com`. Proxies doing virtual-host routing must be SRV-aware.
|
||||||
|
|
||||||
|
BungeeCord also strips a trailing `.` that DNS resolvers sometimes append: `InitialHandler.java:362-365`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Server Address field overloads
|
||||||
|
|
||||||
|
The Server Address field is a plain UTF-8 string with a 255-character limit. Three separate systems have abused it by appending NUL-delimited (`\0`) data after the hostname.
|
||||||
|
|
||||||
|
### Parse order matters
|
||||||
|
|
||||||
|
```
|
||||||
|
raw Server Address field
|
||||||
|
│
|
||||||
|
├── contains \0? ──yes──▶ split on first \0
|
||||||
|
│ left = actual hostname (virtualHost)
|
||||||
|
│ right = extra payload (FML tag or BC forward data)
|
||||||
|
└── no ──▶ use as-is
|
||||||
|
```
|
||||||
|
|
||||||
|
BungeeCord implements this at `InitialHandler.java:355-359`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
if ( handshake.getHost().contains( "\0" ) ) {
|
||||||
|
String[] split = handshake.getHost().split( "\0", 2 );
|
||||||
|
handshake.setHost( split[0] );
|
||||||
|
extraDataInHandshake = "\0" + split[1]; // re-prepends \0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Overload 1 — Forge / FML client detection
|
||||||
|
|
||||||
|
Forge appends a NUL-delimited marker to signal that the connecting client has Forge loaded. The marker sits immediately after the hostname, before any BungeeCord forwarding data.
|
||||||
|
|
||||||
|
| Era | Marker (literal) | Forge versions |
|
||||||
|
|-----|-----------------|----------------|
|
||||||
|
| FML (1.7–1.12.2) | `\0FML\0` | Forge for MC 1.7.x – 1.12.x |
|
||||||
|
| FML2 (1.13+) | `\0FML2\0` | Forge for MC 1.13 – 1.19.x <!-- VERIFY: exact cutoff --> |
|
||||||
|
| FML3 (Forge 36+) | `\0FML3\0` | NeoForge / recent Forge <!-- VERIFY: exact version range --> |
|
||||||
|
|
||||||
|
BungeeCord source defines only `\0FML\0` as `FML_HANDSHAKE_TOKEN` (`ForgeConstants.java:20`). The comment in `InitialHandler.java:351-354` reads:
|
||||||
|
|
||||||
|
> "Starting with FML 1.8, a `\0FML\0` token is appended to the handshake. This interferes with Bungee's IP forwarding, so we detect it, and remove it from the host string, for now. We know FML appends `\00FML\00`. However, we need to also consider that other systems might add their own data to the end of the string. So, we just take everything from the `\0` character and save it for later."
|
||||||
|
|
||||||
|
The Forge handler checks `extraDataInHandshake.contains(ForgeConstants.FML_HANDSHAKE_TOKEN)` to set `fmlTokenInHandshake` (`ForgeClientHandler.java:41-46`, `UserConnection.java`).
|
||||||
|
|
||||||
|
**Wire layout (Forge client, no proxy):**
|
||||||
|
|
||||||
|
```
|
||||||
|
hostname\0FML\0
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `mc.example.com\0FML\0`
|
||||||
|
|
||||||
|
**The FML-tag-breaks-naive-proxies gotcha:** Any proxy or server plugin that does a naive split on `\0` expecting exactly the BungeeCord forwarding format will get confused when a Forge client connects without going through BungeeCord. The first field after the split is `FML` (or `FML2`, `FML3`), not a client IP address. Always check whether the first extra token is a known FML marker before attempting to parse it as IP-forwarding data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Overload 2 — BungeeCord legacy IP forwarding
|
||||||
|
|
||||||
|
When `ip_forward: true` in BungeeCord's config, BungeeCord rewrites the Handshake packet it sends **to the backend server** to embed the real client IP, UUID, and texture properties. This happens in `ServerConnector.java:114-123`.
|
||||||
|
|
||||||
|
**Authoritative source — `ServerConnector.java:116-121`:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
String newHost = copiedHandshake.getHost() + "\00" + AddressUtil.sanitizeAddress( user.getAddress() ) + "\00" + user.getUUID();
|
||||||
|
|
||||||
|
LoginResult profile = user.getPendingConnection().getLoginProfile();
|
||||||
|
if ( profile != null && profile.getProperties() != null && profile.getProperties().length > 0 ) {
|
||||||
|
newHost += "\00" + LoginResult.GSON.toJson( profile.getProperties() );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`user.getUUID()` returns the UUID without dashes (`InitialHandler.java:801-803`):
|
||||||
|
|
||||||
|
```java
|
||||||
|
public String getUUID() {
|
||||||
|
return uniqueId.toString().replace( "-", "" );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Exact wire layout:**
|
||||||
|
|
||||||
|
```
|
||||||
|
<hostname>\0<clientIP>\0<uuidNoDashes>\0<propertiesJSON>
|
||||||
|
```
|
||||||
|
|
||||||
|
| Segment | Content | Notes |
|
||||||
|
|---------|---------|-------|
|
||||||
|
| `<hostname>` | Original virtual host (pre-stripped of FML tag) | e.g. `mc.example.com` |
|
||||||
|
| `\0` | NUL byte (0x00) | delimiter |
|
||||||
|
| `<clientIP>` | `AddressUtil.sanitizeAddress()` output | IPv4 dotted-decimal or IPv6 without scope ID |
|
||||||
|
| `\0` | NUL byte | delimiter |
|
||||||
|
| `<uuidNoDashes>` | Player UUID, 32 hex chars, **no dashes** | e.g. `069a79f444e94726a5befca90e38aaf5` |
|
||||||
|
| `\0` | NUL byte | delimiter (only present when properties follow) |
|
||||||
|
| `<propertiesJSON>` | `LoginResult.GSON.toJson(profile.getProperties())` | Texture properties array; **omitted entirely** if `profile == null` or properties array is empty |
|
||||||
|
|
||||||
|
Full example with textures:
|
||||||
|
|
||||||
|
```
|
||||||
|
mc.example.com\0203.0.113.42\0069a79f444e94726a5befca90e38aaf5\0[{"name":"textures","value":"eyJ0...","signature":"abc..."}]
|
||||||
|
```
|
||||||
|
|
||||||
|
Full example without textures (offline-mode or missing profile):
|
||||||
|
|
||||||
|
```
|
||||||
|
mc.example.com\0203.0.113.42\0069a79f444e94726a5befca90e38aaf5
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend servers (e.g. Paper with `settings.bungeecord: true`) re-parse this field on login to reconstruct the real player identity.
|
||||||
|
|
||||||
|
**Note on FML + BungeeCord interaction:** When ip_forward is enabled, BungeeCord does NOT re-append the `extraDataInHandshake` (the FML tag). The code path is mutually exclusive (`ServerConnector.java:124-128`):
|
||||||
|
|
||||||
|
```java
|
||||||
|
} else if ( !user.getExtraDataInHandshake().isEmpty() ) {
|
||||||
|
// Only restore the extra data if IP forwarding is off.
|
||||||
|
// TODO: Add support for this data with IP forwarding.
|
||||||
|
copiedHandshake.setHost( copiedHandshake.getHost() + user.getExtraDataInHandshake() );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This means: **with BungeeCord ip_forward enabled, Forge mod detection via the handshake tag is broken at the backend.** <!-- VERIFY: still true in current BungeeCord HEAD -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Overload 3 — Velocity modern forwarding (does NOT use this field)
|
||||||
|
|
||||||
|
Velocity's **modern** forwarding mode does **not** overload the Server Address field. Instead it uses a Login Plugin Message exchange during the Login state on the backend connection (channel `velocity:player_info`). See [`proxy-forwarding/velocity-modern.md`](proxy-forwarding/velocity-modern.md) for details.
|
||||||
|
|
||||||
|
Velocity's **legacy/BungeeCord-compat** mode (`ip-forwarding-mode = LEGACY`) uses the same `\0`-delimited format described in Overload 2 above, for compatibility with servers that already support BungeeCord forwarding.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## State-machine diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
C["Client"] -->|"Handshake\n(0x00)"| P["Proxy / Server"]
|
||||||
|
P -->|"Intent=1"| S["Status state"]
|
||||||
|
P -->|"Intent=2"| L["Login state"]
|
||||||
|
P -->|"Intent=3 (1.20.5+)"| L
|
||||||
|
S --> Ping["Status / Ping\n(see 04-status-ping.md)"]
|
||||||
|
L --> Login["Login sequence\n(see 05-login.md)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version history summary
|
||||||
|
|
||||||
|
| Version | Change |
|
||||||
|
|---------|--------|
|
||||||
|
| 1.7.2 | Handshake packet introduced in this form; fields stable since |
|
||||||
|
| 1.7–1.12.2 | Forge appends `\0FML\0` for modded clients |
|
||||||
|
| 1.8+ | BungeeCord ip_forward writes `host\0ip\0uuid[\0props]` to backends |
|
||||||
|
| 1.13+ | Forge marker changes to `\0FML2\0` <!-- VERIFY --> |
|
||||||
|
| 1.20.5 (protocol 766) | Intent value `3` (Transfer) added |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key sources
|
||||||
|
|
||||||
|
| Source | What it confirms |
|
||||||
|
|--------|-----------------|
|
||||||
|
| `BungeeCord/proxy/…/connection/InitialHandler.java:355-359` | FML tag stripping logic, extraDataInHandshake |
|
||||||
|
| `BungeeCord/proxy/…/connection/InitialHandler.java:801-803` | UUID stripped of dashes |
|
||||||
|
| `BungeeCord/proxy/…/ServerConnector.java:114-123` | Authoritative ip_forward write path |
|
||||||
|
| `BungeeCord/proxy/…/forge/ForgeConstants.java:20` | `FML_HANDSHAKE_TOKEN = "\0FML\0"` |
|
||||||
|
| `node-minecraft-protocol/src/client/setProtocol.js:21-26` | Client-side field names; `tagHost` extension point |
|
||||||
|
| `node-minecraft-protocol/src/client/tcp_dns.js:21-33` | SRV resolution overwrites host/port before connect |
|
||||||
|
| `minecraft-data/data/pc/1.8/protocol.json` | `packet_set_protocol` field schema (`varint`, `string`, `u16`, `varint`) |
|
||||||
|
| minecraft.wiki/w/Java_Edition_protocol/Packets | Field names, String(255) limit, Intent=3 description |
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
# 04 — Server List Ping (SLP)
|
||||||
|
|
||||||
|
How the multiplayer server list fetches a server's MOTD, version, and player count.
|
||||||
|
Covers modern SLP (1.7+) and the three legacy `0xFE` variants.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Modern SLP (1.7+)
|
||||||
|
|
||||||
|
### 1.1 Packet sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant C as Client
|
||||||
|
participant S as Server
|
||||||
|
|
||||||
|
C->>S: TCP connect (port 25565)
|
||||||
|
C->>S: Handshake (0x00, nextState=1)
|
||||||
|
C->>S: Status Request (0x00, no fields)
|
||||||
|
S-->>C: Status Response (0x00, JSON string)
|
||||||
|
C->>S: Ping Request (0x01, Long timestamp)
|
||||||
|
S-->>C: Pong Response (0x01, echo Long)
|
||||||
|
S->>C: (server closes connection)
|
||||||
|
```
|
||||||
|
|
||||||
|
The client **may** close after receiving the Status Response without sending Ping
|
||||||
|
(e.g. when only scraping the MOTD). Notchian servers will wait up to ~30 s for
|
||||||
|
the Ping Request before timing out if the client leaves the connection open.
|
||||||
|
<!-- wiki: "Notchian servers will for unknown reasons wait to receive the
|
||||||
|
following Ping Request packet for 30 seconds before timing out" -->
|
||||||
|
|
||||||
|
### 1.2 Packet definitions
|
||||||
|
|
||||||
|
All packets use the standard [Minecraft packet framing](https://minecraft.wiki/w/Java_Edition_protocol)
|
||||||
|
(VarInt length prefix + VarInt packet ID + fields).
|
||||||
|
|
||||||
|
#### Handshaking state → Status
|
||||||
|
|
||||||
|
| Direction | ID | Name | Fields |
|
||||||
|
|-----------|------|------------|--------|
|
||||||
|
| C→S | 0x00 | Handshake | `protocolVersion` VarInt, `serverAddress` String(255), `serverPort` Unsigned Short, `nextState` VarInt (1=Status) |
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets)
|
||||||
|
|
||||||
|
The wiki uses **Intent** as the field name (as of 1.20.5+); older docs call it
|
||||||
|
`nextState`. The value `1` always means "go to Status state."
|
||||||
|
<!-- VERIFY: field rename to "Intent" exact version -->
|
||||||
|
|
||||||
|
#### Status state
|
||||||
|
|
||||||
|
| Direction | ID | Name | Fields |
|
||||||
|
|-----------|------|-----------------|--------|
|
||||||
|
| C→S | 0x00 | Status Request | *(none)* |
|
||||||
|
| S→C | 0x00 | Status Response | `jsonResponse` String(32767) |
|
||||||
|
| C→S | 0x01 | Ping Request | `timestamp` Long |
|
||||||
|
| S→C | 0x01 | Pong Response | `timestamp` Long (echoed) |
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets)
|
||||||
|
|
||||||
|
The Ping payload is a `Long` (8 bytes, signed, big-endian) — typically a
|
||||||
|
millisecond epoch timestamp the client uses to measure round-trip latency.
|
||||||
|
`node-minecraft-protocol` sends `[0, 0]` (two 32-bit zero words that compose
|
||||||
|
one 64-bit zero) and measures wall-clock delta separately
|
||||||
|
(`src/ping.js:55`, `src/server/ping.js:62`).
|
||||||
|
|
||||||
|
### 1.3 The Status Response JSON
|
||||||
|
|
||||||
|
The server writes the entire status as a single JSON string inside the
|
||||||
|
Status Response packet.
|
||||||
|
|
||||||
|
#### Annotated example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": {
|
||||||
|
"name": "1.21.8", // free string — shown in client UI on version mismatch
|
||||||
|
"protocol": 772 // actual protocol number client compares
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"max": 20, // max player slots (can be overridden freely)
|
||||||
|
"online": 1, // current player count
|
||||||
|
"sample": [ // optional list shown in hover tooltip
|
||||||
|
{
|
||||||
|
"name": "thinkofdeath",
|
||||||
|
"id": "4566e69f-c907-48ee-8d71-d7ba5aa00d20" // must be well-formed UUID
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"description": { // the MOTD — see §1.4 for evolution
|
||||||
|
"text": "Hello, world!"
|
||||||
|
},
|
||||||
|
"favicon": "data:image/png;base64,<data>", // 64×64 PNG, no newlines (1.13+)
|
||||||
|
"enforcesSecureChat": false // added in 1.19.1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Source: [minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping) — example JSON
|
||||||
|
|
||||||
|
`node-minecraft-protocol` server-side builds this object at
|
||||||
|
`src/server/ping.js:31-39`:
|
||||||
|
```js
|
||||||
|
const response = {
|
||||||
|
version: responseVersion,
|
||||||
|
players: { max: server.maxPlayers, online: server.playerCount, sample: [] },
|
||||||
|
description: server.motdMsg ?? { text: server.motd },
|
||||||
|
favicon: server.favicon
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Field reference
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
|-------|------|----------|-------|
|
||||||
|
| `version.name` | String | No (1.20+: omission shows "Old") | Display label — **not** version-checked by client |
|
||||||
|
| `version.protocol` | Integer | No | Client compares against its own; mismatch → red/orange label |
|
||||||
|
| `players.max` | Integer | No | `???` shown if `players` omitted; max value 2³¹−1 |
|
||||||
|
| `players.online` | Integer | No | Same |
|
||||||
|
| `players.sample` | Array | No | Objects with `name` (String) + `id` (UUID String) |
|
||||||
|
| `description` | Text Component | No | See §1.4 |
|
||||||
|
| `favicon` | String | No | `data:image/png;base64,…`; exactly 64×64 px; no newlines since 1.13 |
|
||||||
|
| `enforcesSecureChat` | Boolean | No | Added **1.19.1** — client shows warning if server value differs from expectation |
|
||||||
|
| `previewsChat` | Boolean | No | Added alongside `enforcesSecureChat`; deprecated/removed in later 1.19.x releases <!-- VERIFY: exact removal version --> |
|
||||||
|
|
||||||
|
Sources: [minecraft.wiki SLP page](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping);
|
||||||
|
`enforcesSecureChat` version from wiki field table.
|
||||||
|
<!-- VERIFY: previewsChat exact add/remove versions -->
|
||||||
|
|
||||||
|
### 1.4 Description field — chat component evolution
|
||||||
|
|
||||||
|
The `description` field has gone through three forms:
|
||||||
|
|
||||||
|
| Era | Format | Example |
|
||||||
|
|-----|--------|---------|
|
||||||
|
| 1.7–1.8 | Plain string with `§` codes | `"§aA §bcoloured §cMOTD"` |
|
||||||
|
| 1.9–1.15 | JSON Text Component object, but vanilla still embeds `§` codes inside `{"text":"…"}` | `{"text":"§aHello"}` |
|
||||||
|
| 1.16+ | Full structured Text Component (Spigot/Paper); vanilla still uses `§`-embedded strings | `{"text":"Hello","color":"green"}` |
|
||||||
|
|
||||||
|
The wiki notes: "Notchian servers embed section-sign-based codes within the
|
||||||
|
text value, while third-party servers such as Spigot and Paper will return full
|
||||||
|
components." Both forms are valid — clients accept either.
|
||||||
|
|
||||||
|
`node-minecraft-protocol`'s server prefers a structured object (`server.motdMsg`)
|
||||||
|
but falls back to `{ text: server.motd }` with a plain string
|
||||||
|
(`src/server/ping.js:38`).
|
||||||
|
|
||||||
|
<!-- VERIFY: exact version 1.7 vs 1.8 for original JSON SLP introduction -->
|
||||||
|
|
||||||
|
### 1.5 Version spoofing
|
||||||
|
|
||||||
|
`version.name` is a **free string** — the server can write anything here.
|
||||||
|
`version.protocol` is the **number** the client actually compares against its own.
|
||||||
|
|
||||||
|
Common patterns:
|
||||||
|
|
||||||
|
| Scenario | `version.name` | `version.protocol` |
|
||||||
|
|----------|----------------|--------------------|
|
||||||
|
| Matching client | `"1.21.8"` | `772` |
|
||||||
|
| Outdated client (server newer) | `"1.21.8"` | `772` — client shows "Outdated client!" |
|
||||||
|
| Outdated server (client newer) | `"1.20.4"` | `765` — client shows "Outdated server!" |
|
||||||
|
| ViaVersion (accepts many) | `"Requires 1.21+ (1.7-1.20.6 supported)"` | client's own protocol number |
|
||||||
|
| Discovery ping (unknown client ver) | `"Paper 1.21"` | `-1` (sentinel) |
|
||||||
|
|
||||||
|
ViaVersion sets `version.protocol` to the **connecting client's** protocol number
|
||||||
|
so the client always sees a match, then handles translation internally.
|
||||||
|
<!-- VERIFY: ViaVersion -1 sentinel vs client-echo behavior — wiki says -1 may cause close -->
|
||||||
|
|
||||||
|
The wiki notes that for 1.6 legacy responses from a 1.7+ server, the protocol
|
||||||
|
version in the response is always `127` (an incompatibility sentinel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Legacy `0xFE` Server List Ping
|
||||||
|
|
||||||
|
Pre-1.7 clients used a simple kick-packet hack. There are three variants,
|
||||||
|
distinguished by how much data the client sends.
|
||||||
|
|
||||||
|
### 2.1 Overview
|
||||||
|
|
||||||
|
| Client version | Client sends | Response separator |
|
||||||
|
|----------------|--------------|--------------------|
|
||||||
|
| Beta 1.8 – 1.3.2 | `FE` | `§` (section sign, U+00A7) |
|
||||||
|
| 1.4 – 1.5.2 | `FE 01` | `\0` (null, U+0000) |
|
||||||
|
| 1.6.x | `FE 01 FA …` (plugin message) | `\0` (null) |
|
||||||
|
|
||||||
|
All responses are sent as a **kick packet** (`0xFF`) containing a UTF-16BE
|
||||||
|
encoded string.
|
||||||
|
|
||||||
|
### 2.2 Variant A — Beta 1.8 through 1.3.2
|
||||||
|
|
||||||
|
**Client → Server:** single byte `0xFE`
|
||||||
|
|
||||||
|
**Server → Client** (kick packet):
|
||||||
|
```
|
||||||
|
FF packet ID
|
||||||
|
XX XX UInt16BE: number of UTF-16 code units in the response string
|
||||||
|
[UTF-16BE data]
|
||||||
|
```
|
||||||
|
|
||||||
|
Response string format (fields joined with `§`):
|
||||||
|
```
|
||||||
|
<MOTD>§<online>§<max>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `A Minecraft Server§5§20`
|
||||||
|
|
||||||
|
`node-minecraft-protocol` handles this as `payload === undefined` (neither 0 nor
|
||||||
|
1), falling into the `else` branch at `src/server/ping.js:73`:
|
||||||
|
```js
|
||||||
|
sendPingResponse([server.motd, server.playerCount.toString(),
|
||||||
|
server.maxPlayers.toString()].join('\xa7'))
|
||||||
|
```
|
||||||
|
(`\xa7` = `§`)
|
||||||
|
|
||||||
|
### 2.3 Variant B — 1.4.2 through 1.5.2
|
||||||
|
|
||||||
|
**Client → Server:** two bytes `FE 01`
|
||||||
|
|
||||||
|
**Server → Client:** same kick packet format, but fields joined with **null**
|
||||||
|
(` | ||||||