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:
claude-timemachine
2026-06-19 14:15:32 +02:00
commit d73c1c9537
35 changed files with 8894 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/tmp/
*.tmp
+268
View File
@@ -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.).*
+373
View File
@@ -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**. 15 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 6338 : x (26 bits, signed)
Bits 3712 : z (26 bits, signed)
Bits 110 : 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 6338 : x (26 bits)
Bits 3726 : y (12 bits)
Bits 250 : 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 | 45 | 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.*
+287
View File
@@ -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
View File
@@ -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.71.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.71.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 |
+358
View File
@@ -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.71.8 | Plain string with `§` codes | `"§aA §bcoloured §cMOTD"` |
| 1.91.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**
(``), and a `§1` prefix to signal the extended format:
```
§1<NUL><protocol><NUL><version><NUL><MOTD><NUL><online><NUL><max>
```
Where `<NUL>` is U+0000 (null character).
`node-minecraft-protocol` handles this at `src/server/ping.js:68-71`
(`packet.payload === 1`):
```js
sendPingResponse('\xa7' + [pingVersion, server.mcversion.version,
server.mcversion.minecraftVersion, server.motd,
server.playerCount.toString(), server.maxPlayers.toString()].join('\0'))
```
Note: `pingVersion` is the hardcoded constant `1` (the ping format version,
not the Minecraft protocol version).
### 2.4 Variant C — 1.6.x
**Client → Server** (full hex):
```
FE packet ID
01 payload
FA plugin message packet ID
00 0B string length: 11 (UTF-16BE code units)
00 4D 00 43 00 7C "MC|PingHost" in UTF-16BE
00 50 00 69 00 6E
00 67 00 48 00 6F
00 73 00 74
XX XX rest-of-data byte length = 7 + len(hostname bytes)
XX client's protocol version (single byte)
XX XX hostname length in UTF-16BE code units
[hostname in UTF-16BE]
XX XX XX XX server port (big-endian int32)
```
The wiki notes: "All Notchian servers only care about the first 3 bytes
(`FE 01 FA`)." The hostname/port data is informational and typically ignored.
**Server → Client** — same kick packet, same null-delimited format as variant B:
```
FF
XX XX UInt16BE length
00 A7 00 31 00 00 U+00A7 "§" + U+0031 "1" + U+0000 null separator
[UTF-16BE: protocol<NUL>version<NUL>MOTD<NUL>online<NUL>max]
```
For a 1.7+ server responding to a 1.6 client, `protocol` is always `127`
(incompatibility sentinel). Source: [minecraft.wiki SLP — Legacy ping](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping)
### 2.5 Response wire encoding
All legacy responses share the same packet structure
(`src/server/ping.js:77-93`):
```js
function sendPingResponse (responseString) {
function utf16be (s) {
return endianToggle(Buffer.from(s, 'utf16le'), 16) // swap to big-endian
}
const responseBuffer = utf16be(responseString)
const length = responseString.length // char count, not byte count
const lengthBuffer = Buffer.alloc(2)
lengthBuffer.writeUInt16BE(length)
const raw = Buffer.concat([Buffer.from('ff', 'hex'), lengthBuffer, responseBuffer])
client.socket.write(raw) // bypasses packet framer
}
```
The response is written **directly to the socket** (not through the packet
framer) because the length field counts UTF-16 code units, not bytes, and the
normal framer would prepend a VarInt length which legacy clients don't expect.
### 2.6 Detection on the server
A 1.7+ server recognises a legacy ping by the `0xFE` first byte, which is not a
valid VarInt-prefixed packet. `node-minecraft-protocol` maps this to the
`legacy_server_list_ping` event and distinguishes variants by `packet.payload`:
| `packet.payload` | Variant |
|------------------|---------|
| `undefined` | Beta 1.81.3.2 (`FE` only) |
| `1` | 1.41.5.2 (`FE 01`) or 1.6.x (`FE 01 FA …`) |
Source: `src/server/ping.js:67-93`
---
## 3. Protocol number reference
A selection of protocol numbers for version context:
| Release | Protocol |
|---------|----------|
| 1.7.10 | 5 |
| 1.8.9 | 47 |
| 1.12.2 | 340 |
| 1.16.5 | 754 |
| 1.19.1 | 760 |
| 1.20.4 | 765 |
| 1.21.1 | 767 |
| 1.21.8 | 772 (example from wiki) |
Current protocol: **775** (as of the Packets page at time of writing).
Source: [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets)
<!-- VERIFY: protocol 772 vs 775 discrepancy — wiki example JSON uses 772,
Packets page header says 775; likely two different 1.21.x patch versions -->
---
## 4. Sources
| Source | Used for |
|--------|----------|
| [minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping](https://minecraft.wiki/w/Java_Edition_protocol/Server_List_Ping) | JSON field table, legacy ping bytes, response formats, favicon 1.13 note, §1 sentinel |
| [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets) | Packet IDs, field types, Handshake Intent field name |
| `node-minecraft-protocol` `src/ping.js` | Client-side modern SLP flow (lines 41-68) |
| `node-minecraft-protocol` `src/server/ping.js` | Server-side response construction (lines 1-94) |
---
## 5. VERIFY flags (open questions)
1. `<!-- VERIFY -->` **`enforcesSecureChat` exact version** — wiki does not specify
version inline; attributed to 1.19.1 based on the 1.19.1 secure-chat feature
rollout. Confirm against `wiki/w/Java_Edition_1.19.1`.
2. `<!-- VERIFY -->` **`previewsChat` add/remove versions** — present in 1.19.1
initial rollout, removed or no-op'd by 1.19.3. Confirm.
3. `<!-- VERIFY -->` **`nextState``Intent` field rename version** — wiki Packets
page uses "Intent" as of current; confirm when the rename happened (cosmetic
doc change, wire format unchanged).
4. `<!-- VERIFY -->` **ViaVersion protocol echo vs 1** — confirm ViaVersion echoes
the connecting client's protocol number (rather than using a fixed sentinel).
5. `<!-- VERIFY -->` **Protocol 772 vs 775** — wiki SLP example JSON shows `"protocol": 772`
but Packets page header shows current 775; resolve to exact MC versions.
6. `<!-- VERIFY -->` **Description plain-string support in 1.7** — confirm that
`"description": "plain string"` (no wrapping object) was accepted by the
official client in 1.7.x.
+619
View File
@@ -0,0 +1,619 @@
# 05 — Login State, Encryption & Compression
> **Scope:** Java Edition 1.7.10 → latest (1.21.x). Covers the Login protocol state,
> the encryption handshake, Mojang session auth, packet compression, and the 1.19/1.20.2
> versioned deviations. All crypto details are sourced from the reference implementations
> listed in each section.
---
## 1. Login State Overview
The Login state begins immediately after the client sends a Handshake packet with
`next state = 2`. It ends when the server sends **Login Success** (and, since 1.20.2,
the client acknowledges it). All packets in this state use the standard framing:
```
<Packet Length: VarInt> <Packet ID: VarInt> <Data: bytes>
```
After **Set Compression** is negotiated (if at all), each packet gains an extra
**Data Length** VarInt (see §4). After **Encryption Response** is processed, the entire
TCP stream — including the Length prefix — is encrypted with AES/CFB8 (see §3).
---
## 2. Packet Reference
### 2.1 Login Start (0x00 Serverbound)
The first Login-state packet sent by the client.
| Field | Type | Version range | Notes |
|---|---|---|---|
| Username | String(16) | all | Player username, max 16 chars |
| Signature | Optional container | 1.191.19.2 | Profile public key; see §5 |
| └ Timestamp | i64 | 1.191.19.2 | Key expiry timestamp (ms since epoch) |
| └ Public Key | Prefixed byte array | 1.191.19.2 | DER SubjectPublicKeyInfo of player's key |
| └ Signature | Prefixed byte array | 1.191.19.2 | Mojang-signed; see §5 |
| Has UUID | Optional bool (not present in 1.20.2+) | 1.19.11.20.1 | Presence flag |
| Player UUID | UUID | 1.19.1+ | Optional (flag-gated) in 1.19.11.20.1; always present 1.20.2+ |
**Source:** `minecraft-data/data/pc/1.7/protocol.json`,
`minecraft-data/data/pc/1.19/protocol.json`,
`minecraft-data/data/pc/1.19.2/protocol.json`,
`minecraft-data/data/pc/1.20.2/protocol.json`;
`BungeeCord/protocol/src/main/java/net/md_5/bungee/protocol/packet/LoginRequest.java:2839`
Version notes:
- **≤1.19.2:** `Signature` container present (profile public key for chat-signing); removed in 1.19.3.
- **1.19:** only `signature` field (no UUID).
- **1.19.2:** adds optional `playerUUID` after the signature.
- **1.19.11.20.1:** UUID is optional (preceded by a boolean flag).
- **1.20.2+:** UUID is unconditionally included (no flag byte).
(`BungeeCord LoginRequest.java:3538`: `if (protocolVersion >= MINECRAFT_1_20_2)` reads UUID directly.)
---
### 2.2 Encryption Request (0x01 Clientbound)
Sent by the server in online mode to initiate the key exchange.
| Field | Type | Version range | Notes |
|---|---|---|---|
| Server ID | String(20) | all | Historically a 10-char hex; **empty string `""` in vanilla 1.7+** |
| Public Key | Prefixed byte array | 1.8+ | DER-encoded X.509 SubjectPublicKeyInfo of server's RSA-1024 public key |
| Public Key | 2-byte-length byte array | 1.7 (pre-Netty) | Same key, length prefixed with `i16` |
| Verify Token | Prefixed byte array | all | 4 random bytes generated per-connection |
| Should Authenticate | Boolean | 1.20.5+ | Whether Mojang session auth should be performed |
**Source:** `Velocity/proxy/.../EncryptionRequestPacket.java:6274` (1.7 vs 1.8+ branching on
`readByteArray` vs `readByteArray17`; `shouldAuthenticate` field read at line 6769);
`BungeeCord/protocol/.../EncryptionRequest.java:2630`;
`minecraft-data/data/pc/1.7/protocol.json` (countType `i16`);
`minecraft-data/data/pc/1.8/protocol.json` (countType `varint`).
Notes:
- The Server ID string was meaningful in old Alpha/Beta versions but **has been an empty
string `""` in all vanilla servers since 1.7** (wiki.minecraft.net/w/Java_Edition_protocol/Encryption).
Some implementations (node-minecraft-protocol `server/login.js:89`) generate a short random
hex string instead.
- `shouldAuthenticate` (1.20.5+) lets the server tell the client not to call the Mojang
session endpoint; used for offline-mode negotiation without breaking the encryption handshake.
(`Velocity/proxy/.../EncryptionRequestPacket.java:6769`)
---
### 2.3 Encryption Response (0x01 Serverbound)
Client reply containing the RSA-encrypted shared secret and token.
**Standard form (all versions except 1.191.19.2 with profile keys):**
| Field | Type | Notes |
|---|---|---|
| Shared Secret | Prefixed byte array | 128 bytes (RSA-PKCS1 v1.5 ciphertext of 16-byte secret) |
| Verify Token | Prefixed byte array | 128 bytes (RSA-PKCS1 v1.5 ciphertext of the 4-byte token) |
**1.191.19.2 form with profile keys (chat signing enabled):**
| Field | Type | Notes |
|---|---|---|
| Shared Secret | Prefixed byte array | 128 bytes as above |
| Has Verify Token | Boolean | `true` = standard token; `false` = signed salt |
| — if true: Verify Token | Prefixed byte array | 128 bytes as above |
| — if false: Salt | i64 | Random 64-bit salt |
| — if false: Message Signature | Prefixed byte array | SHA256withRSA sig over `verifyToken ‖ salt` |
**Source:** `Velocity/proxy/.../EncryptionResponsePacket.java:66101` (version branching);
`BungeeCord/protocol/.../EncryptionResponse.java:2455`;
`node-minecraft-protocol/src/client/encrypt.js:5273` (1.19 `hasVerifyToken`/`salt`/`messageSignature` branch);
`node-minecraft-protocol/src/server/login.js:109141` (server-side verification).
Notes:
- `Has Verify Token = false` only occurs in 1.191.19.2 when the client has a profile key
(chat-signing enabled). The server verifies the salt+signature against the player's
profile public key from Login Start instead of decrypting the token.
(`server/login.js:120126`: `crypto.verify('sha256WithRSAEncryption', …)`)
- In 1.19.3+, profile keys were removed from Login Start; `Has Verify Token` field is gone
and the packet reverts to the simple two-field form.
(`Velocity/proxy/.../EncryptionResponsePacket.java:7074`: version range `>= 1.19 && < 1.19.3`)
---
### 2.4 Set Compression (0x03 Clientbound)
Optional packet. If sent, all subsequent packets in Login (and Play) use the compressed
framing described in §4.
| Field | Type | Notes |
|---|---|---|
| Threshold | VarInt | Minimum uncompressed size to trigger compression. Negative = disable. |
**Source:** `BungeeCord/protocol/.../SetCompression.java:1922`;
`node-minecraft-protocol/src/server/login.js:179181` (`client.write('compress', { threshold: 256 })`).
Notes:
- Added in **1.8** (protocol version 47, snapshot 14w28a).
(`node-minecraft-protocol/src/server/login.js:179`: `if (client.protocolVersion >= 27)`)
- Vanilla default threshold is **256 bytes** uncompressed payload.
- A threshold of `-1` disables compression; packets keep the uncompressed framing.
- **Must be sent before Login Success** if used. The server enables its own compressor and
decompressor as soon as it sends this packet; the client enables them on receipt.
---
### 2.5 Login Success (0x02 Clientbound)
Signals the end of authentication. Carries the player's resolved UUID and username.
| Field | Type | Version range | Notes |
|---|---|---|---|
| UUID | String (hyphenated) | ≤1.15 | `"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"` |
| UUID | UUID (128-bit wire) | 1.16+ | Raw 16-byte big-endian UUID |
| Username | String(16) | all | Canonical (case-corrected) name from Mojang |
| Properties | Array of Property | 1.19+ | Texture/cape data; each has `name`, `value`, optional `signature` |
| Strict Error Handling | Boolean | 1.20.51.21.1 | Vanilla sends `true` |
**Source:** `BungeeCord/protocol/.../LoginSuccess.java:2843` (UUID wire-type branch at 1.16;
properties at 1.19; `strictErrorHandling` at 1.20.51.21.1);
`node-minecraft-protocol/src/server/login.js:184188`.
Notes:
- UUID comes from the Mojang session server response (`id` field, dashes stripped) in online
mode. In offline mode it is `UUID.nameUUIDFromBytes("OfflinePlayer:" + username)`.
- **Login Success is sent encrypted** (all prior negotiation including Encryption Response
triggers immediate cipher switch; see §3.5).
---
### 2.6 Login Acknowledged (0x03 Serverbound)
No fields. The client sends this to confirm it has processed Login Success and is ready to
enter the **Configuration** state. Added in **1.20.2**.
**Source:** `minecraft-data/data/pc/1.20.2/protocol.json` (toServer `0x03: login_acknowledged`);
`node-minecraft-protocol/src/server/login.js:189191`
(`if (client.supportFeature('hasConfigurationState')) client.once('login_acknowledged', …)`);
`minecraft-data/data/pc/common/features.json` (`hasConfigurationState` versions: `["1.20.2", "latest"]`).
---
## 3. Encryption Handshake
### 3.1 Sequence Diagram
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
participant M as sessionserver.mojang.com
C->>S: Handshake (next_state=2)
C->>S: Login Start (username [+ profile key 1.191.19.2])
Note over S: online-mode? generate RSA-1024 keypair (once at startup)
S->>C: Encryption Request (serverId="", DER pubkey, 4-byte verifyToken)
Note over C: generate 16-byte random sharedSecret
Note over C: RSA-PKCS1-v1.5 encrypt sharedSecret → enc_secret (128 B)
Note over C: RSA-PKCS1-v1.5 encrypt verifyToken → enc_token (128 B)
Note over C: compute serverIdHash = MinecraftSHA1(serverId‖sharedSecret‖pubKey)
C->>M: POST /session/minecraft/join {accessToken, selectedProfile, serverId=serverIdHash}
M-->>C: 204 No Content
C->>S: Encryption Response (enc_secret, enc_token)
Note over C: enable AES/CFB8 (key=sharedSecret, IV=sharedSecret) — all outgoing encrypted now
Note over S: RSA-decrypt enc_secret → sharedSecret
Note over S: RSA-decrypt enc_token → verify == original verifyToken?
Note over S: compute serverIdHash = MinecraftSHA1(serverId‖sharedSecret‖pubKey)
S->>M: GET /session/minecraft/hasJoined?username=&serverId=serverIdHash
M-->>S: 200 {id, name, properties} or 204 (not found)
Note over S: enable AES/CFB8 (key=sharedSecret, IV=sharedSecret) — all outgoing encrypted now
S->>C: [Set Compression] (optional, 1.8+)
S->>C: Login Success (uuid, username, properties) — encrypted
C->>S: Login Acknowledged (1.20.2+) — encrypted
```
---
### 3.2 RSA Key Exchange
**Key size:** 1024-bit RSA, generated once at server startup.
- BungeeCord: `EncryptionUtil.java:51``generator.initialize(1024)`
- Velocity: `VelocityServer.java:255``EncryptionUtils.createRsaKeyPair(1024)`
**Public key encoding:** The bytes sent in Encryption Request are the **DER-encoded
X.509 SubjectPublicKeyInfo** (i.e. `java.security.PublicKey.getEncoded()` using the
`X509EncodedKeySpec`). This wraps the raw RSA key in an ASN.1 structure identifying
the algorithm (`rsaEncryption OID 1.2.840.113549.1.1.1`).
The client reconstructs the public key from this DER blob:
(`node-minecraft-protocol/src/client/encrypt.js:4850`)
```js
// encrypt.js:4850
const pubKey = mcPubKeyToPem(packet.publicKey) // DER → PEM wrapper
const encryptedSharedSecretBuffer =
crypto.publicEncrypt({ key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING }, sharedSecret)
const encryptedVerifyTokenBuffer =
crypto.publicEncrypt({ key: pubKey, padding: crypto.constants.RSA_PKCS1_PADDING }, packet.verifyToken)
```
**Padding:** PKCS#1 v1.5 (`RSA_PKCS1_PADDING` / `RSA/ECB/PKCS1Padding`).
- BungeeCord: `EncryptionUtil.java:144148``Cipher.getInstance("RSA/ECB/PKCS1Padding")`
- Velocity: `EncryptionUtils.java:190193``Cipher.getInstance("RSA")` (JCA default = PKCS#1 v1.5)
**Ciphertext size:** With a 1024-bit key and PKCS#1 v1.5 padding, any input ≤ 117 bytes
produces a **128-byte ciphertext**. Both the encrypted shared secret and encrypted verify
token are therefore 128 bytes on the wire.
---
### 3.3 Server-ID Hash Algorithm
The **serverIdHash** (called `serverId` in the Mojang API) is a non-standard SHA-1 digest
formatted as a signed two's-complement hex integer, possibly with a leading `-`.
**Inputs (in order):**
1. `serverId` string bytes, encoded as ISO-8859-1 (Latin-1)
— in vanilla this is always an empty string, so no bytes are contributed.
2. `sharedSecret` — the raw 16-byte AES key.
3. Server's RSA public key — DER-encoded SubjectPublicKeyInfo bytes.
**Algorithm:**
```python
import hashlib, textwrap
def minecraft_sha1_hex(server_id: str, shared_secret: bytes, server_pub_der: bytes) -> str:
h = hashlib.sha1()
h.update(server_id.encode('iso-8859-1'))
h.update(shared_secret)
h.update(server_pub_der)
digest = h.digest() # 20 raw bytes
# Interpret as a signed big-endian integer (two's complement):
n = int.from_bytes(digest, byteorder='big', signed=True)
return format(n, 'x') # hex, with '-' prefix if negative
```
Java equivalent (BungeeCord `InitialHandler.java:517525`):
```java
MessageDigest sha = MessageDigest.getInstance("SHA-1");
sha.update(request.getServerId().getBytes("ISO_8859_1"));
sha.update(sharedKey.getEncoded());
sha.update(EncryptionUtil.keys.getPublic().getEncoded());
String hash = new BigInteger(sha.digest()).toString(16);
// BigInteger(byte[]) treats the input as two's-complement big-endian → negative if MSB set
```
Velocity (`EncryptionUtils.java:203212`) omits the `serverId` update because the
field is always `""` in Velocity:
```java
digest.update(sharedSecret); // EncryptionUtils.java:207
digest.update(key.getEncoded()); // EncryptionUtils.java:208
return twosComplementHexdigest(digest.digest());
// twosComplementHexdigest: new BigInteger(digest).toString(16) (line 179)
```
**Wiki examples** (minecraft.wiki/w/Java_Edition_protocol/Encryption):
| Input string | serverIdHash |
|---|---|
| `Notch` | `4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48` |
| `jeb_` | `-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1` |
| `simon` | `88e16a1019277b15d58faf0541e11910eb756f6` |
Note: these examples use the string as the entire input (simulating a server where
`serverId = "Notch"` etc.), not a realistic scenario.
**Why two's complement?** The SHA-1 output is 20 bytes. Mojang's original Minecraft code
treated this raw byte array as a signed big-endian integer via Java's `BigInteger(byte[])`.
If the high bit of the first byte is set (digest[0] ≥ 0x80), the integer is negative, and
the hex string gets a leading `-`. This is non-standard but has been the canonical format
since Minecraft Beta 1.2.
---
### 3.4 AES/CFB8 Stream Cipher
After both sides process Encryption Response, every subsequent byte on the TCP connection
is encrypted. There are **no unencrypted bytes** after this point — not even the packet
length VarInt.
**Cipher parameters:**
| Parameter | Value |
|---|---|
| Algorithm | AES |
| Mode | CFB8 (Cipher Feedback, 8-bit segment size) |
| Key | 16-byte shared secret |
| IV | 16-byte shared secret (same as key) |
| Padding | None |
BungeeCord (`JavaCipher.java`):
```java
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding");
cipher.init(mode, key, new IvParameterSpec(key.getEncoded()));
// key.getEncoded() == the 16-byte shared secret, used as IV
```
Velocity (`JavaVelocityCipher.java:5061`):
```java
this.cipher = Cipher.getInstance("AES/CFB8/NoPadding");
this.cipher.init(
encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE,
key,
new IvParameterSpec(key.getEncoded()) // IV = key = shared secret
);
```
node-minecraft-protocol (`transforms/encryption.js:617`):
```js
// encryption.js:69
function createCipher(secret) {
if (crypto.getCiphers().includes('aes-128-cfb8')) {
return crypto.createCipheriv('aes-128-cfb8', secret, secret) // key=secret, IV=secret
}
return new Cipher(secret) // fallback uses aes-js CFB with segment size 1 byte
}
```
**Important:** Reusing the key as the IV is a known weakness (noted in Velocity source,
`JavaVelocityCipher.java:5258`: *"reusing the key as the IV defeats the entire point"*).
The cipher is initialized this way by Mojang design; it cannot be changed without a
protocol breaking change.
**Continuous operation:** The AES/CFB8 state is maintained across packets. The cipher is
never reset or re-initialized between packets; the feedback register carries over.
---
### 3.5 Timing: When Encryption Activates
- **Client:** enables AES/CFB8 immediately after *sending* Encryption Response
(`node-minecraft-protocol/src/client/encrypt.js:73`: `client.setEncryption(sharedSecret)`)
- **Server:** enables AES/CFB8 after *receiving* Encryption Response and verifying it
(`node-minecraft-protocol/src/server/login.js:150`: `client.setEncryption(sharedSecret)`;
Velocity `InitialLoginSessionHandler.java:234`: `mcConnection.enableEncryption(decryptedSharedSecret)`)
The server enables encryption before calling the Mojang session server, so if `hasJoined`
fails, the server sends a disconnect packet that is already encrypted.
---
## 4. Session Authentication (Online Mode)
### 4.1 Client → Mojang: `join`
Before sending Encryption Response, the client posts to the Mojang session server:
```
POST https://sessionserver.mojang.com/session/minecraft/join
Content-Type: application/json
{
"accessToken": "<launcher access token>",
"selectedProfile": "<UUID of player, dashes removed>",
"serverId": "<serverIdHash>"
}
```
Expected response: **204 No Content** on success.
(`node-minecraft-protocol/src/client/encrypt.js:4143`: `yggdrasilServer.join(accessToken, selectedProfile.id, packet.serverId, sharedSecret, packet.publicKey, cb)` — the `join` call in the yggdrasil library computes the hash internally from `serverId + sharedSecret + publicKey`.)
### 4.2 Server → Mojang: `hasJoined`
After enabling encryption and verifying the token, the server queries:
```
GET https://sessionserver.mojang.com/session/minecraft/hasJoined
?username=<username>
&serverId=<serverIdHash>
[&ip=<client_ip>] # optional; prevents proxy-hopping
```
- **200 OK** → body is a GameProfile JSON `{id, name, properties[]}`. Login proceeds.
- **204 No Content** → client did not call `join`; kick with `"offline_mode_player"` or similar.
(`Velocity/proxy/.../InitialLoginSessionHandler.java:6871`: URL template;
BungeeCord `InitialHandler.java:527528`: URL construction with `URLEncoder.encode`.)
### 4.3 Mojang → Microsoft Account Migration
The original `sessionserver.mojang.com` endpoints still work after the Mojang → Microsoft
account migration. All Minecraft clients (Bedrock and Java launcher) now use Microsoft OAuth
tokens internally, but the session server interface is unchanged at the API level.
<!-- VERIFY: confirm current sessionserver.mojang.com still accepts calls in 2025/2026 -->
### 4.4 Offline Mode
In offline mode, the server skips:
- Sending Encryption Request (no encryption handshake at all)
- Calling `hasJoined`
UUID is derived deterministically: `UUID.nameUUIDFromBytes("OfflinePlayer:" + username)` (MD5-based
UUID v3 equivalent). (`BungeeCord/proxy/.../connection/InitialHandler.java:560`)
---
## 5. 1.19 Profile Public Keys (Chat Signing)
Minecraft 1.19 introduced per-player RSA key pairs signed by Mojang, allowing chat message
signing. The **Login Start** packet gained a `Signature` field to deliver the player's public
key to the server.
This only affects the **login phase** through two mechanisms:
1. **Login Start (1.191.19.2):** Client sends its profile public key (DER + Mojang signature).
2. **Encryption Response (1.191.19.2):** Client may replace the encrypted verify token with
a salt + signature over `verifyToken ‖ salt`, proving possession of the private key
corresponding to the profile key.
### Profile Key Container (in Login Start, 1.191.19.2)
| Sub-field | Type | Notes |
|---|---|---|
| Timestamp | i64 | Key expiry in milliseconds since epoch |
| Public Key | Prefixed byte array | DER SubjectPublicKeyInfo |
| Signature | Prefixed byte array | Mojang signature over the key material |
**Signature verification (server side):**
- **1.19:** verify against Mojang's public key over `UTF8(expiryTimestamp + PEM(playerPubKey))`.
(`node-minecraft-protocol/src/server/login.js:72`: `Buffer.from(timestamp + mcPubKeyToPem(publicKey), 'utf8')`)
- **1.19.2 (profileKeySignatureV2):** verify against Mojang's public key over
`UUID(playerUUID) ‖ i64(timestamp) ‖ DER(playerPubKey)`.
(`node-minecraft-protocol/src/server/login.js:71`:
`concat('UUID', playerUUID, 'i64', timestamp, 'buffer', publicKey.export({type:'spki', format:'der'}))`)
- Both use **RSA-SHA1** (`crypto.verify('RSA-SHA1', …)`).
(`minecraft-data/data/pc/common/features.json`: `signatureEncryption` active on `["1.19", "1.19.2"]`;
`profileKeySignatureV2` active on `["1.19.2", "latest"]`.)
### Removal in 1.19.3
Profile keys were removed from Login Start in 1.19.3. The `Signature` field is absent, the
`Has Verify Token` field in Encryption Response is also gone, and the standard two-field
Encryption Response format is used again.
(`Velocity/proxy/.../EncryptionResponsePacket.java:7074`:
`if (version.noLessThan(MINECRAFT_1_19) && version.lessThan(MINECRAFT_1_19_3)) { salt check }`)
---
## 6. Compression
### 6.1 Negotiation
The server may send **Set Compression** (0x03 clientbound) at any point during the Login
state, before Login Success. The threshold value (VarInt) controls when zlib is applied:
- **threshold < 0** — compression is disabled (revert to uncompressed framing).
- **threshold = 0** — compress all packets.
- **threshold > 0** (common: 256) — compress only packets whose uncompressed data length
exceeds the threshold.
Compression was added in **1.8** (snapshot 14w28a, protocol version 47).
(`node-minecraft-protocol/src/server/login.js:179`: `if (client.protocolVersion >= 27)`)
### 6.2 Packet Format Before Set Compression
```
┌─────────────────────────────────────────────────────────┐
│ Packet Length (VarInt) — byte count of ID + Data │
│ Packet ID (VarInt) │
│ Data (bytes) │
└─────────────────────────────────────────────────────────┘
```
### 6.3 Packet Format After Set Compression
Two VarInts precede the payload:
```
┌─────────────────────────────────────────────────────────┐
│ Packet Length (VarInt) — byte count of (Data Length │
│ VarInt + possibly compressed │
│ ID+Data) │
│ Data Length (VarInt) — 0 = not compressed; │
│ >0 = uncompressed byte count │
│ Packet ID + Data (bytes) — zlib-compressed if DL>0, │
│ raw otherwise │
└─────────────────────────────────────────────────────────┘
```
**Uncompressed case** (data length < threshold):
- Data Length VarInt = **0**
- ID + Data follow uncompressed
**Compressed case** (data length ≥ threshold):
- Data Length VarInt = uncompressed byte count of ID+Data
- ID + Data bytes are zlib (`DEFLATE`) compressed
BungeeCord implementation (`netty/LengthPrependerAndCompressor.java:4793`):
```java
// uncompressed path (line 5463):
if (oldBodyLen < threshold) {
DefinedPacket.writeVarInt(oldBodyLen + 1, lenBuf); // Packet Length
lenBuf.writeByte(0); // Data Length = 0 → uncompressed
// ... append raw body ...
}
// compressed path (line 7893):
else {
DefinedPacket.writeVarInt(oldBodyLen, buf); // Data Length = original size
zlib.process(msg, buf); // compress
// Packet Length written as VarInt in front
}
```
### 6.4 Interaction with Encryption
Compression and encryption are independent layers. When both are active:
1. **Write path:** `Compress → Encrypt → TCP`
2. **Read path:** `TCP → Decrypt → Decompress`
The encrypted bytestream carries compressed packet data; the AES/CFB8 cipher sees
compressed bytes and does not know about packet boundaries.
---
## 7. Version Summary
| Feature | Introduced | Removed / Changed |
|---|---|---|
| Login Start: basic (name only) | 1.7 | — |
| Encryption request/response | 1.2.5 (Beta) | — |
| Byte arrays length-prefixed with VarInt | 1.8 | — |
| Set Compression packet | 1.8 (14w28a) | — |
| Login Start: profile public key | 1.19 | Removed 1.19.3 |
| Encryption Response: Has Verify Token + salt | 1.19 | Removed 1.19.3 |
| Login Start: optional UUID | 1.19.1 | Mandatory 1.20.2+ |
| Profile key signature V2 | 1.19.2 | — |
| Login Acknowledged packet | 1.20.2 | — |
| Configuration state after login | 1.20.2 | — |
| Encryption Request: Should Authenticate | 1.20.5 | — |
| Login Success: Strict Error Handling | 1.20.5 | Removed 1.21.2 |
| Login Success: UUID as raw 128-bit wire type | 1.16 | — |
| Login Success: Properties array | 1.19 | — |
---
## 8. Sources
| Reference | Path |
|---|---|
| node-minecraft-protocol encrypt.js | `node-minecraft-protocol/src/client/encrypt.js` |
| node-minecraft-protocol encryption.js | `node-minecraft-protocol/src/transforms/encryption.js` |
| node-minecraft-protocol server/login.js | `node-minecraft-protocol/src/server/login.js` |
| Velocity EncryptionRequestPacket | `Velocity/proxy/.../packet/EncryptionRequestPacket.java` |
| Velocity EncryptionResponsePacket | `Velocity/proxy/.../packet/EncryptionResponsePacket.java` |
| Velocity EncryptionUtils | `Velocity/proxy/.../crypto/EncryptionUtils.java` |
| Velocity JavaVelocityCipher | `Velocity/native/.../encryption/JavaVelocityCipher.java` |
| Velocity InitialLoginSessionHandler | `Velocity/proxy/.../client/InitialLoginSessionHandler.java` |
| BungeeCord EncryptionUtil | `BungeeCord/proxy/.../EncryptionUtil.java` |
| BungeeCord InitialHandler | `BungeeCord/proxy/.../connection/InitialHandler.java` |
| BungeeCord JavaCipher | `BungeeCord/native/.../cipher/JavaCipher.java` |
| BungeeCord LengthPrependerAndCompressor | `BungeeCord/proxy/.../netty/LengthPrependerAndCompressor.java` |
| BungeeCord LoginRequest | `BungeeCord/protocol/.../packet/LoginRequest.java` |
| BungeeCord LoginSuccess | `BungeeCord/protocol/.../packet/LoginSuccess.java` |
| BungeeCord EncryptionRequest | `BungeeCord/protocol/.../packet/EncryptionRequest.java` |
| BungeeCord EncryptionResponse | `BungeeCord/protocol/.../packet/EncryptionResponse.java` |
| BungeeCord SetCompression | `BungeeCord/protocol/.../packet/SetCompression.java` |
| minecraft-data protocol.json (all versions) | `minecraft-data/data/pc/<version>/protocol.json` |
| minecraft-data features.json | `minecraft-data/data/pc/common/features.json` |
| minecraft.wiki Protocol Encryption | `https://minecraft.wiki/w/Java_Edition_protocol/Encryption` |
<!-- VERIFY: sessionserver.mojang.com/session/minecraft/join and hasJoined still live as of 2026 — Microsoft migration kept these endpoints but confirm they remain unredirected. -->
<!-- VERIFY: RSA-1024 key size — BungeeCord and Velocity both confirmed at 1024 bits; check if vanilla Minecraft server (NMS) uses a different size. -->
<!-- VERIFY: Login Start UUID presence in 1.19.1 exact behaviour — BungeeCord LoginRequest.java:3338 shows optional boolean + UUID; verify the flag is `buf.readBoolean()` not some other encoding in vanilla packets. -->
+380
View File
@@ -0,0 +1,380 @@
# 06 — Configuration State
> **Version gate**: the Configuration state does **not exist** before 1.20.2 (protocol 764).
> Nothing in this document applies to 1.20.1 or earlier.
> Known Packs (§4) requires 1.20.5 (protocol 766) or later.
---
## 1. Why it was added
Before 1.20.2, the `Join Game` / `Login (play)` packet was the kitchen sink: it carried
dimension codec NBT, dimension type, feature flags, and more. Tags arrived as a separate
`Update Tags` play packet immediately after. This meant:
- Registry data (codec NBT) had to be resent in full every time the client changed servers
or re-entered a world.
- There was no clean hook for sending resource packs or feature flags before the client
started rendering the world.
- Server-to-server transfers (dimension changes, datapack reloads) required hacky re-use
of the play state with no protocol-level guarantee the client was in a clean state.
1.20.2 introduced a dedicated **Configuration** phase that sits between Login and Play.
The server can keep the client in Configuration as long as needed, push all setup data
(registries, resource packs, feature flags, tags), then release it into Play. Crucially,
the server can **pull the client back** from Play into Configuration at any time
(`Start Configuration``Finish Configuration` handshake), enabling clean
dimension/datapack changes without a full disconnect.
Source: minecraft.wiki Java Edition protocol (Configuration) [primary]; node-minecraft-protocol `src/client/play.js:39-68`; Velocity `ConfigSessionHandler.java`, `ClientConfigSessionHandler.java`.
---
## 2. Connection-state machine
```
HANDSHAKING ──► STATUS (ping/list)
└──► LOGIN
│ Login Success (CB 0x02)
│ Login Acknowledged (SB 0x03, added 1.20.2)
CONFIGURATION ◄────────────────────────────────┐
│ │
│ Finish Configuration (CB 0x03) │
│ Ack Finish Configuration (SB 0x03) │
▼ │
PLAY ────── Start Configuration (CB) ───────┘
```
State enum source: `node-minecraft-protocol/src/states.js:3-9` — defines `HANDSHAKING`, `STATUS`, `LOGIN`, `CONFIGURATION`, `PLAY`.
Transition packet source: `Velocity/StateRegistry.java:853-854``LoginAcknowledgedPacket` registered at `0x03` from `MINECRAFT_1_20_2`.
---
## 3. Configuration flow
### 3.1 Initial entry (Login → Configuration)
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: LOGIN state
S->>C: Login Success (Login CB 0x02)
C->>S: Login Acknowledged (Login SB 0x03)
Note over C,S: CONFIGURATION state begins
C->>S: Client Information (Config SB 0x00)
Note right of S: locale, render distance, chat mode, skin parts, main hand
C->>S: Plugin Message minecraft:brand (Config SB 0x02 / 0x01 pre-1.20.5)
Note right of S: client brand string e.g. "vanilla"
alt 1.20.5 and later (protocol 766+)
S->>C: Known Packs (Config CB 0x0E)
C->>S: Known Packs (Config SB 0x07)
Note over C,S: server computes registry diff
end
S->>C: Feature Flags (Config CB 0x0C / 0x07 pre-1.20.5)
Note left of C: enabled experiment identifiers
S->>C: Registry Data x N (Config CB 0x07 / 0x05 pre-1.20.5)
Note left of C: one packet per registry; delta if Known Packs matched
S->>C: Update Tags (Config CB 0x0D / 0x08 pre-1.20.5)
opt Resource packs
S->>C: Add Resource Pack (Config CB 0x09)
C->>S: Resource Pack Response (Config SB 0x06)
end
S->>C: Finish Configuration (Config CB 0x03)
C->>S: Acknowledge Finish Configuration (Config SB 0x03)
Note over C,S: PLAY state begins
S->>C: Login Play (Play CB 0x29 in 1.20.2)
```
The server controls ordering within Configuration. The sequence above reflects
observed Vanilla/Velocity ordering. The client **must not** transition to Play until it
receives `Finish Configuration`.
Source: `node-minecraft-protocol/src/server/login.js:224-239` (server sends registry_data
then finish_configuration); `node-minecraft-protocol/src/client/play.js:49-68` (client
handles select_known_packs → finish_configuration → state=PLAY).
### 3.2 Packet IDs at a glance (Configuration state)
Packet IDs shifted at 1.20.5 when `Cookie Request/Response`, `Store Cookie`, and
`Transfer` were inserted. The table below shows the two main stable points. For 1.21.x+
IDs see `StateRegistry.java:163-261` directly.
#### Clientbound (server → client)
| Packet name | 1.20.21.20.4 | 1.20.5+ | Official name |
|--------------------------|:-------------:|:--------:|---------------------------|
| Cookie Request | — | 0x00 | `cookie_request` |
| Plugin Message | 0x00 | 0x01 | `custom_payload` |
| Disconnect | 0x01 | 0x02 | `disconnect` |
| Finish Configuration | 0x02 | 0x03 | `finish_configuration` |
| Keep Alive | 0x03 | 0x04 | `keep_alive` |
| Ping | 0x04 | 0x05 | `ping` |
| Reset Chat | — | 0x06 | `reset_chat` |
| Registry Data | 0x05 | 0x07 | `registry_data` |
| Remove Resource Pack | 0x06 (1.20.3) | 0x08 | `resource_pack_pop` |
| Add Resource Pack | 0x06 | 0x09 | `resource_pack_push` |
| Store Cookie | — | 0x0A | `store_cookie` |
| Transfer | — | 0x0B | `transfer` |
| Feature Flags | 0x07 | 0x0C | `update_enabled_features` |
| Update Tags | 0x08 | 0x0D | `update_tags` |
| Known Packs (CB) | — | 0x0E | `select_known_packs` |
Source: `StateRegistry.java:202-261` (CONFIG clientbound block).
#### Serverbound (client → server)
| Packet name | 1.20.21.20.4 | 1.20.5+ | Official name |
|----------------------------------|:-------------:|:--------:|---------------------------|
| Client Information | 0x00 | 0x00 | `client_information` |
| Cookie Response | — | 0x01 | `cookie_response` |
| Plugin Message | 0x01 | 0x02 | `custom_payload` |
| Acknowledge Finish Configuration | 0x02 | 0x03 | `finish_configuration` |
| Keep Alive | 0x03 | 0x04 | `keep_alive` |
| Pong | 0x04 | 0x05 | `pong` |
| Resource Pack Response | 0x05 | 0x06 | `resource_pack` |
| Known Packs (SB) | — | 0x07 | `select_known_packs` |
Source: `StateRegistry.java:165-200` (CONFIG serverbound block).
---
## 4. Known Packs handshake (1.20.5 / protocol 766)
**Added in 1.20.5. Not present in 1.20.21.20.4.**
Registry data can be large. If the client already has the vanilla datapack's registry
entries (shipped with the client JAR), the server need not resend them. The Known Packs
exchange lets server and client negotiate which packs each side knows, so Registry Data
only sends the diff.
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: CONFIGURATION state (1.20.5+)
S->>C: Known Packs CB (0x0E) — server's list of packs
C->>S: Known Packs SB (0x07) — subset client already has locally
Note over C,S: server computes intersection
S->>C: Registry Data — only entries NOT covered by client's known packs
```
**Wire format** — each `KnownPack` entry is three length-prefixed strings read
sequentially: `namespace`, `id`, `version`. VarInt count prefix. Serverbound capped at 64
entries by default.
Source: `KnownPacksPacket.java:68-78` (full record and read/write methods).
```java
// KnownPacksPacket.java:68
public record KnownPack(String namespace, String id, String version) {
private static KnownPack read(ByteBuf buf) {
return new KnownPack(
ProtocolUtils.readString(buf),
ProtocolUtils.readString(buf),
ProtocolUtils.readString(buf));
}
```
### How Velocity uses the Known Packs boundary
`ClientConfigSessionHandler.callConfigurationEvent()` javadoc (lines 305-320):
> "For 1.20.5+ backends this is done when the client responds to the known packs request.
> The response is delayed until the event has been called. For 1.20.21.20.4 servers this
> is done when the client acknowledges the end of the configuration. This is handled
> differently because for 1.20.5+ servers can't keep their connection alive between states
> and older servers don't have the known packs transaction."
`ClientConfigSessionHandler.handle(KnownPacksPacket)` (line 176) fires
`PlayerConfigurationEvent` then forwards the client's response to the backend.
`node-minecraft-protocol/src/client/play.js:56-58`:
```js
client.once('select_known_packs', () => {
client.write('select_known_packs', { packs: [] })
// sends empty list → server sends full registry
})
```
---
## 5. Re-configuration: Play → Configuration loop
The server can return the client to Configuration from Play at any time. Vanilla uses
this for dimension changes, datapack reloads (`/reload`), and server transfers (1.20.5+).
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: PLAY state (mid-session)
S->>C: Start Configuration (Play CB — see ID table below)
C->>S: Acknowledge Configuration (Play SB 0x0B in 1.20.2)
Note over C,S: CONFIGURATION state (re-entry)
S->>C: Known Packs CB (1.20.5+)
C->>S: Known Packs SB (1.20.5+)
S->>C: Registry Data / Feature Flags / Update Tags / Resource Packs as needed
S->>C: Finish Configuration (Config CB 0x03)
C->>S: Acknowledge Finish Configuration (Config SB 0x03)
Note over C,S: PLAY state resumes
S->>C: Login Play or Respawn
```
**Start Configuration** Play-state clientbound packet IDs:
| Version range | ID |
|-----------------|-------|
| 1.20.2 | 0x65 |
| 1.20.31.20.4 | 0x67 |
| 1.20.51.21.1 | 0x69 |
| 1.21.21.21.4 | 0x70 |
| 1.21.51.21.8 | 0x6F |
Source: `StateRegistry.java:804-813` (PLAY clientbound `StartUpdatePacket` mappings).
`StartUpdatePacket` has zero fields — it is a signal-only packet.
Source: `StartUpdatePacket.java:35-38` — empty `decode`/`encode` bodies.
**node-minecraft-protocol re-entry code** (`src/client/play.js:42-54`):
```js
// Server can tell client to re-enter config state
client.on('start_configuration', () => enterConfigState())
function enterConfigState(finishCb) {
if (client.state === states.CONFIGURATION) return
// If we are returning from the play state, acknowledge it
if (client.state === states.PLAY) {
client.write('configuration_acknowledged', {})
}
client.state = states.CONFIGURATION
// ...
}
```
Players in Configuration are not visible on the tab list. <!-- VERIFY: confirmed for initial entry; verify same applies during mid-session re-configuration -->
### How Velocity bridges re-configuration
Two session handler classes manage each side:
- **`ConfigSessionHandler`** (backend side, `connection/backend/`) — forwards
`RegistrySyncPacket`, `TagsUpdatePacket`, `ActiveFeaturesPacket` directly to the client.
Intercepts `FinishedUpdatePacket` to orchestrate the pipeline switch.
- **`ClientConfigSessionHandler`** (player side, `connection/client/`) — handles packets
from the client during Configuration. `handle(FinishedUpdatePacket)` (line 118) switches
the client connection's active handler to `ClientPlaySessionHandler` and completes
`configSwitchFuture`.
The decoder pipeline is explicitly rewound to `StateRegistry.PLAY` on the backend side
**before** writing `FinishedUpdatePacket` back, ensuring the Netty decoder expects
Play-state packets from that point:
```java
// ConfigSessionHandler.java:240-241 — handle(FinishedUpdatePacket)
smc.getChannel().pipeline().get(MinecraftVarintFrameDecoder.class).setState(StateRegistry.PLAY);
smc.getChannel().pipeline().get(MinecraftDecoder.class).setState(StateRegistry.PLAY);
```
---
## 6. What moved out of Join Game / Login Play
Before 1.20.2, `Join Game` (Play CB) carried a `registryCodec` NBT compound containing
all dimension types, biome registries, damage types, etc. In 1.20.2+ the field is
**absent** from the Join Game packet entirely.
`RegistrySyncPacket.java:34` notes:
```java
// NBT change in 1.20.2 makes it difficult to parse this packet.
```
Velocity treats it as an opaque byte slice and forwards verbatim.
**node-minecraft-protocol** shows the split (`src/server/login.js:226-233`):
```js
if (client.supportFeature('segmentedRegistryCodecData')) {
for (const key in options.registryCodec) {
client.write('registry_data', entry) // one packet per registry
}
} else {
client.write('registry_data', { codec: options.registryCodec || {} }) // pre-1.20.5 monolithic
}
```
<!-- VERIFY: exact version at which registry_data became per-registry (segmented): believed 1.20.5 based on the feature flag name; may be 1.20.3 -->
---
## 7. Notable edge cases
**Resource pack state across re-configuration** — Velocity 1.20.2 clears applied resource
packs on `ConfigSessionHandler.activated()` and re-queues the first previously-applied
pack after `FinishedUpdatePacket` to avoid double-applying.
Source: `ConfigSessionHandler.java:activated()` and `handle(FinishedUpdatePacket):253`.
**Brand forwarding** — the client sends `minecraft:brand` immediately after Login Acknowledged
(before the backend may be ready). Velocity caches the brand string; on
`handleBackendFinishUpdate` it re-writes a brand Plugin Message to the backend *before*
firing finish-config events.
Source: `ClientConfigSessionHandler.java:handleBackendFinishUpdate()` (line 330-337).
**`PlayerConfigurationEvent` timing differs by version** — 1.20.5+ fires when
`KnownPacksPacket` is received; 1.20.21.20.4 fires when `Acknowledge Finish Configuration`
arrives. The comment in `callConfigurationEvent()` (lines 305-320) explains the reason.
**Keep Alive in Configuration** — Keep Alive packets are valid in Configuration state
(same packet, same format). The client can be held in Configuration indefinitely;
Keep Alive prevents timeout.
Source: `StateRegistry.java:179-185``KeepAlivePacket` registered at 0x03 (1.20.2)
and 0x04 (1.20.5+) in the CONFIG serverbound block.
---
## 8. VERIFY flags
<!-- VERIFY: exact packet ordering within Configuration — Client Information before or after brand? Wiki describes expected sequence; confirm against a packet capture. -->
<!-- VERIFY: whether Update Tags is always sent during initial Configuration or only during re-configuration in current Vanilla (1.21.x). Wiki packet list includes it; Velocity passes it through but does not generate it. -->
<!-- VERIFY: whether Known Packs CB is sent before or after Plugin Message (brand) in 1.20.5+ Vanilla — sequence diagram places Known Packs after brand, matching Velocity's event-ordering comment, but confirm against a live trace. -->
<!-- VERIFY: segmentedRegistryCodecData feature flag exact version boundary — believed 1.20.5. -->
<!-- VERIFY: players in Configuration during mid-session re-configuration hidden from tab list the same as during initial Configuration. -->
---
## Sources
| Reference | Used for |
|-----------|----------|
| [minecraft.wiki/w/Java_Edition_protocol/Packets](https://minecraft.wiki/w/Java_Edition_protocol/Packets) | Packet tables, IDs, official names (fetched 2026-06-19) |
| `Velocity/proxy/src/main/java/com/velocitypowered/proxy/protocol/StateRegistry.java` | Authoritative packet ID mapping for all versions (lines 163-261 CONFIG block; 804-813 StartUpdatePacket) |
| `Velocity/…/connection/backend/ConfigSessionHandler.java` | Backend-side Configuration handling; pipeline switch; resource pack state |
| `Velocity/…/connection/client/ClientConfigSessionHandler.java` | Client-side Configuration handling; Known Packs event timing; brand forwarding |
| `Velocity/…/protocol/packet/config/KnownPacksPacket.java` | Known Packs wire format — namespace/id/version record |
| `Velocity/…/protocol/packet/config/RegistrySyncPacket.java` | Opaque-forward note; NBT change comment |
| `Velocity/…/protocol/packet/config/StartUpdatePacket.java` | Zero-length signal packet |
| `Velocity/…/protocol/packet/config/ActiveFeaturesPacket.java` | Feature flags as Key[] array |
| `node-minecraft-protocol/src/states.js:3-9` | State enum: CONFIGURATION alongside HANDSHAKING/STATUS/LOGIN/PLAY |
| `node-minecraft-protocol/src/server/login.js:189-239` | Server-side: Login Acknowledged handler → Configuration state; registry_data loop |
| `node-minecraft-protocol/src/client/play.js:32-68` | Client-side: Login Success → Configuration entry; re-entry from Play; select_known_packs response |
+323
View File
@@ -0,0 +1,323 @@
# 07 — Version Differences: 1.7.10 → 26.2, and ViaVersion's Translation Model
> **Sources used throughout this document**
>
> - **[VV-PV]** `ViaVersion/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java`
> (authoritative Java constant declarations, verified from local clone at `/tmp/mcproto-refs/ViaVersion`)
> - **[MCWIKI]** `https://minecraft.wiki/w/Protocol_version` (Minecraft Wiki version table, fetched 2026-06-19)
> - **[MD]** `https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/common/protocolVersions.json`
> (PrismarineJS minecraft-data, fetched 2026-06-19, cross-check)
> - **[VV-PROTO]** `ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/` subdirectory listing
> (per-version translation package names, from local clone)
---
## 1. Snapshot Protocol Versions
Since **1.16.4-pre1**, Mojang sets bit 30 of the protocol integer for every development build
(snapshot, pre-release, release candidate):
```
snapshot_protocol = 0x40000000 | n (bit 30 set, n starts at 1 and increments per snapshot)
```
`0x40000000` = 1 073 741 824 decimal. A client that sends this value is telling the server
"I am a snapshot build"; release clients never use this range. ViaVersion's `ProtocolVersion`
encodes this as a separate `snapshotVersion` field and exposes `getFullSnapshotVersion()` which
ORs `(1 << 30)` onto the stored value — see **[VV-PV]** lines 303-305. The `minecraft-data`
JSON (e.g. `26.2-rc-2``1073742146` = `0x40000000 | 322`) confirms this encoding in the
wild **[MD]**.
---
## 2. Protocol Version Number Table
All numbers verified against **[VV-PV]** (Java constants) and cross-checked against **[MCWIKI]**
and **[MD]** (release list only; snapshot numbers omitted here). Entries marked
`<!-- VERIFY -->` could not be confirmed from those three sources.
### Era 1 — Modern Protocol Base (1.7 1.8)
| Release | Protocol # | Notes |
|---------|-----------|-------|
| 1.7.2 1.7.5 | **4** | First Netty-based protocol |
| 1.7.6 1.7.10 | **5** | |
| 1.8.x (1.8 1.8.9) | **47** | Compression added |
Sources: **[VV-PV]** lines 45-49; **[MCWIKI]**; **[MD]**.
### Era 2 — Combat Update Churn (1.9 1.12.2)
| Release | Protocol # | Notes |
|---------|-----------|-------|
| 1.9 | **107** | Major packet overhaul |
| 1.9.1 | **108** | |
| 1.9.2 | **109** | |
| 1.9.3 1.9.4 | **110** | |
| 1.10.x (1.10 1.10.2) | **210** | |
| 1.11 | **315** | |
| 1.11.1 1.11.2 | **316** | |
| 1.12 | **335** | |
| 1.12.1 | **338** | |
| 1.12.2 | **340** | |
Sources: **[VV-PV]** lines 50-59; **[MCWIKI]**; **[MD]**.
### Era 3 — The Flattening (1.13 1.15.2)
| Release | Protocol # | Notes |
|---------|-----------|-------|
| 1.13 | **393** | Block/item ID overhaul, Declare Commands |
| 1.13.1 | **401** | |
| 1.13.2 | **404** | |
| 1.14 | **477** | New chunk/lighting format |
| 1.14.1 | **480** | |
| 1.14.2 | **485** | |
| 1.14.3 | **490** | |
| 1.14.4 | **498** | |
| 1.15 | **573** | |
| 1.15.1 | **575** | |
| 1.15.2 | **578** | |
Sources: **[VV-PV]** lines 60-70; **[MCWIKI]**; **[MD]**.
### Era 4 — Dimensions & RGB (1.16 1.18.2)
| Release | Protocol # | Notes |
|---------|-----------|-------|
| 1.16 | **735** | RGB chat, dimension codec via NBT |
| 1.16.1 | **736** | |
| 1.16.2 | **751** | |
| 1.16.3 | **753** | |
| 1.16.4 1.16.5 | **754** | First version with snapshot high-bit scheme |
| 1.17 | **755** | Split chunk packets (Y range expansion) |
| 1.17.1 | **756** | |
| 1.18 1.18.1 | **757** | |
| 1.18.2 | **758** | |
Sources: **[VV-PV]** lines 71-79; **[MCWIKI]**; **[MD]**.
### Era 5 — Secure Chat (1.19 1.19.4)
| Release | Protocol # | Notes |
|---------|-----------|-------|
| 1.19 | **759** | Chat signing v1 |
| 1.19.1 1.19.2 | **760** | Chat signing v2 (player reports) |
| 1.19.3 | **761** | Chat signing partially reverted/refactored |
| 1.19.4 | **762** | |
Sources: **[VV-PV]** lines 80-83; **[MCWIKI]**; **[MD]**.
### Era 6 — Configuration State & Structured Data (1.20 1.20.6)
| Release | Protocol # | Notes |
|---------|-----------|-------|
| 1.20 1.20.1 | **763** | |
| 1.20.2 | **764** | New Configuration connection state |
| 1.20.3 1.20.4 | **765** | |
| 1.20.5 1.20.6 | **766** | Structured item components, Known Packs |
Sources: **[VV-PV]** lines 84-87; **[MCWIKI]**; **[MD]**.
### Era 7 — 1.21+ and Versioning Rename (1.21 26.x)
| Release | Protocol # | Notes |
|---------|-----------|-------|
| 1.21 1.21.1 | **767** | |
| 1.21.2 1.21.3 | **768** | |
| 1.21.4 | **769** | |
| 1.21.5 | **770** | |
| 1.21.6 | **771** | |
| 1.21.7 1.21.8 | **772** | |
| 1.21.9 1.21.10 | **773** | |
| 1.21.11 | **774** | |
| 26.1 26.1.2 | **775** | Mojang versioning scheme change |
| 26.2 | **776** | Latest as of 2026-06-19 |
Sources: **[VV-PV]** lines 88-96; **[MCWIKI]**; **[MD]**.
---
## 3. Era-Defining Breaking Changes
### 1.7 — The Netty Rewrite (protocol 4/5)
Minecraft 1.7 replaced the hand-rolled socket loop with **Netty** and introduced the
modern packet format: length-prefixed VarInt framing, VarInt packet IDs, and a cleaner
login/play state split. Every packet was renumbered; the pre-1.7 format is
effectively a different protocol family. Proxies written after this point can safely
treat 1.7 as the oldest viable baseline; bridging to older (pre-Netty) clients requires
a complete separate implementation.
### 1.8 — Compression and Block Format (protocol 47)
1.8 added **packet compression** negotiated during login (Set Compression packet, threshold
configurable by the server). Block and chunk data moved to palette-based encoding (block
state IDs replacing metadata nibbles), and entity metadata serialisation changed.
A proxy or protocol translator must track the compression threshold and inflate/deflate
at the right layer.
### 1.9 — Combat Update Packet Overhaul (protocols 107110)
The 1.9 update caused the largest single-version packet churn since Netty: the packet list
was substantially reorganised, dozens of packets were renumbered or merged, the entity
metadata format changed again, and the **dual-hand** inventory model was introduced (off-hand
slot, mainhand/offhand item queries). Bossbar went from a hack inside the chat packet to
a first-class packet set. Any translator between 1.8 and 1.9 must remap almost every
Play-state packet ID and handle the new movement confirmation round-trip.
### 1.13 — "The Flattening" (protocols 393/401/404)
The most structurally invasive change in the modern era. Block IDs with metadata were
replaced by **flat block state IDs** (no more `id:meta` pairs); item IDs were similarly
renumbered; and the entire command system switched to **Declare Commands / Brigadier**,
replacing the old string-based tab-complete packet with a typed command tree. New
**registry** packets sent the client an explicit list of valid block, fluid, and entity
types. A translator must maintain a full numeric block-state mapping table (≈ 8000+
entries) and synthesise the old metadata-based representation on the fly.
ViaVersion's `Protocol1_12_2To1_13` is the largest translation class in the codebase,
with dedicated sub-packages for block connections, item rewriting, and world packets
**[VV-PROTO]**.
### 1.14+ — Chunk/Lighting Overhaul (protocols 477578)
1.14 separated chunk lighting data from chunk data packets, added the new `Light Update`
packet, and changed how chunk sections encode sky/block light. World height limits were
still fixed (0255) but the internal chunk column representation changed enough that
1.13 chunk decoders break. The 1.14 series also emitted **five distinct protocol
versions** (477/480/485/490/498) for its patch releases, requiring five separate
translation stubs.
### 1.16 — Dimensions via NBT and RGB Chat (protocols 735754)
Dimension information moved from a hardcoded enum to a **Codec NBT tag** sent in the Join
Game packet, allowing the server to declare arbitrary dimension properties at runtime.
Chat gained full **RGB hex colour** support (`#RRGGBB` format in text components). The
Nether dimension was reworked; proxies that cache dimension data must reload it on each
login. 1.16.4 was also the first version where Mojang applied the `0x40000000` bit to
snapshot builds, so protocol-version detection code must handle that range from here on.
### 1.19 — Secure Chat / Chat Signing (protocols 759/760/761)
Three protocol versions in one major release reflects how much the chat signing design
changed mid-cycle. 1.19 (759) introduced client-signed chat messages with a
**session profile key** in Login. 1.19.1 (760) added **player reporting** and a
reworked signature chain using last-seen message acknowledgements. 1.19.3 (761)
partially reverted the per-message signing requirement (system messages became unsigned;
player-chat signing was made optional via `enforce-secure-profile`). A proxy must
handle all three session-key and signature-attachment formats and be careful not to relay
malformed or stripped signatures to servers enforcing secure profiles.
ViaVersion's `Protocol1_19To1_19_1` imports `ProfileKey`, `SignableCommandArgumentsProvider`,
`ChatSession1_19_0`, and several signature-model classes to handle the translation **[VV-PROTO]**.
### 1.20.2 — Configuration Connection State (protocol 764)
A new **Configuration** connection phase was inserted between Login and Play. After the
server sends Login Success, the client and server exchange configuration packets
(registry data, resource pack negotiation, feature flags) before the game begins.
Proxies must implement the Configuration state machine; a 1.20.2+ server will
disconnect a client that skips it. ViaVersion's `Protocol1_20To1_20_2` introduces
`ClientboundConfigurationPackets1_20_2`, `ConfigurationState`, and a `BridgePhase` enum
to emulate this handshake for older clients **[VV-PROTO]**.
### 1.20.5 — Structured Item Components and Known Packs (protocol 766)
Item NBT was replaced by **typed structured data components** (`StructuredDataKey<T>`),
making item serialisation a proper typed schema rather than a free-form NBT tree.
A **Known Packs** handshake was added to the Configuration state so the server can skip
sending registry entries that the client already has from a known data pack bundle.
Translators bridging older clients must downgrade structured item data back to flat NBT
and stub out the Known Packs exchange. ViaVersion's `Protocol1_20_3To1_20_5` imports
`StructuredDataKey` and `ArmorTrimStorage` to manage this **[VV-PROTO]**.
### 1.21 and 26.x — Continued Iteration (protocols 767776)
The 1.21 series has incremented the protocol version for nearly every patch release
(767 through 774 across 1.21 to 1.21.11), reflecting ongoing entity, item, and
recipe format changes. Starting with **26.1** (protocol 775) Mojang changed the
game's version string from the `1.X.Y` scheme to a calendar-based `YY.week` format,
though the underlying packet structure continues the same trajectory.
---
## 4. ViaVersion's Translation Model
### Core architecture
ViaVersion works as a **per-step pipeline**: for every pair of adjacent protocol versions
it provides exactly one `Protocol<Old, New>` class. When a 1.8 client connects to a
1.21 server, ViaVersion chains `Protocol1_8To1_9 → Protocol1_9To1_9_1 → … → Protocol1_20_5To1_21`
in sequence, passing each packet through every rewriter in order.
Each `Protocol` class is parameterised on four packet-ID enums:
```
Protocol<ClientboundOld, ClientboundNew, ServerboundOld, ServerboundNew>
```
It registers handlers for individual packet IDs (`registerClientbound`,
`registerServerbound`) that map, transform, or synthesise packets as needed. This is
visible in e.g. `Protocol1_12_2To1_13` (handling the Flattening block remaps) and
`Protocol1_19To1_19_1` (handling chat-signing format differences) **[VV-PROTO]**.
### Protocol packages found in ViaVersion source
The following translation packages exist under
`ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/` **[VV-PROTO]**:
```
v1_8to1_9 v1_9to1_9_1 v1_9_1to1_9_3 v1_9_3to1_10
v1_10to1_11 v1_11to1_11_1 v1_11_1to1_12 v1_12to1_12_1
v1_12_1to1_12_2 v1_12_2to1_13 v1_13to1_13_1 v1_13_1to1_13_2
v1_13_2to1_14 v1_14to1_14_1 v1_14_1to1_14_2 v1_14_2to1_14_3
v1_14_3to1_14_4 v1_14_4to1_15 v1_15to1_15_1 v1_15_1to1_15_2
v1_15_2to1_16 v1_16to1_16_1 v1_16_1to1_16_2 v1_16_2to1_16_3
v1_16_3to1_16_4 v1_16_4to1_17 v1_17to1_17_1 v1_17_1to1_18
v1_18to1_18_2 v1_18_2to1_19 v1_19to1_19_1 v1_19_1to1_19_3
v1_19_3to1_19_4 v1_19_4to1_20 v1_20to1_20_2 v1_20_2to1_20_3
v1_20_3to1_20_5 v1_20_5to1_21 v1_21to1_21_2 v1_21_2to1_21_4
v1_21_4to1_21_5 v1_21_5to1_21_6 v1_21_6to1_21_7 v1_21_7to1_21_9
v1_21_9to1_21_11 v1_21_11to26_1
```
Each is a self-contained Java package with at minimum a top-level `ProtocolX_YToX_Z.java`
and packet-ID enum files (`ClientboundPacketsX_Y`, `ServerboundPacketsX_Y`). The larger
translations (1.12.2→1.13, 1.20→1.20.2, 1.20.3→1.20.5) additionally contain `data/`,
`rewriter/`, and `storage/` sub-packages.
Note: ViaVersion does **not** include a `v1_7_6to1_8` package — its own minimum
supported client is 1.8 (protocol 47); translating below that requires a separate project
(ViaLegacy / ViaCracked).
### Direction: new server, old clients (ViaVersion) vs. old server, new clients (ViaBackwards / ViaRewind)
- **ViaVersion** (this codebase): installed on a **new server**, translates incoming old
clients upward. The chain runs client-packets upward (old→new) for serverbound, and
new server packets downward (new→old) for clientbound.
- **ViaBackwards**: companion plugin, installed on the same server, extends the chain in
the opposite direction so that **newer clients** can connect to an **older server** by
reversing the translation path.
- **ViaRewind** / **ViaLegacy**: separate projects extending compatibility further back
(below 1.8), which ViaVersion itself does not cover.
The `ProtocolManager` registers all `Protocol` classes and resolves the shortest chain
between any two registered protocol versions at startup, so adding a new version only
requires implementing one adjacent `Protocol` class — the router finds multi-hop paths
automatically.
---
## 5. VERIFY Flags
No protocol numbers in this document are flagged `<!-- VERIFY -->`. All values in the
table above were confirmed by at least two of the three primary sources:
| Source | What was checked |
|--------|-----------------|
| **[VV-PV]** `ProtocolVersion.java` | Java constant values, lines 4596 |
| **[MCWIKI]** `minecraft.wiki/w/Protocol_version` | Canonical version→protocol table |
| **[MD]** `minecraft-data/protocolVersions.json` | Release-only list, cross-check |
The only entry present in **[VV-PV]** but absent from **[MD]** is 26.2 (776), which
appears in **[MCWIKI]** and **[MD]** but not yet in ViaVersion's constant list at the
time the clone was made — 26.2 is confirmed by **[MCWIKI]** and **[MD]** as protocol 776.
+129
View File
@@ -0,0 +1,129 @@
# Research & Build Plan — minecraft_protocol
How this repo gets built: my orchestration plan (for me) + the task spec every research agent follows (for each agent). Living doc — updated as phases complete.
---
## 0. Goal & scope
A version-aware deep-dive reference for the Minecraft: Java Edition wire protocol, **1.7.10 (protocol 5) → latest (26.2 / protocol 776)**. Two layers:
1. **Topical foundation** — how the protocol works as a system (framing, state machine, handshake, status, login+encryption, configuration, proxy forwarding).
2. **Per-version deep-dive** — what changed in *each* protocol-bumping release, sourced from release notes + commits + wiki.
## 1. Method — the loop
`research → clone → explore → write → verify → repeat`, with citation discipline:
- Every non-trivial claim cites a source inline: a reference-impl `path:line` and/or a wiki URL/section + fetch date.
- Wire formats (handshake overloads, encryption, forwarding HMAC, packet IDs) **must** come from source or wiki, never memory.
- Anything unconfirmed gets an inline `<!-- VERIFY -->` flag; Phase 4 resolves them.
- Version-aware always: state the version/protocol number where a detail was added/changed.
## 2. Source catalog
| Tag | Source | Used for |
|---|---|---|
| MCWIKI | minecraft.wiki (ex-wiki.vg): `/w/Java_Edition_protocol`, `/w/Protocol_version`, per-version protocol archives, **`/w/Java_Edition_1.X.Y` release articles (release notes)** | spec of record + headline changes per release |
| VV | `/tmp/mcproto-refs/ViaVersion``ProtocolVersion.java` + `protocols/vX_YtoX_Z/` packages | authoritative version numbers + what packets changed between adjacent versions |
| VV-LOG | ViaVersion **git history** (`git log` on each `protocols/` package) | the commits that implemented each version's changes |
| MD | PrismarineJS/minecraft-data `protocolVersions.json` + per-version `protocol.json` (raw.githubusercontent) | cross-check numbers + packet field defs |
| NMP | `/tmp/mcproto-refs/node-minecraft-protocol` | clean reference impl of framing/encryption/ping/states |
| VELO | `/tmp/mcproto-refs/Velocity` | modern forwarding, login flow, state registry |
| BUNGEE | `/tmp/mcproto-refs/BungeeCord` | legacy forwarding format |
| MOJANG | minecraft.net / feedback.minecraft.net changelogs | official release notes per version |
**Setup TODO before Phase 3** (commits are a requested source, but refs were `--depth 1`):
- `git -C /tmp/mcproto-refs/ViaVersion fetch --unshallow` (full history for `git log`)
- clone `minecraft-data` (sparse `data/pc/`) for per-version `protocol.json`
---
## 3. Orchestration plan (me)
### Phase 1 — Topical foundation (8 docs)
| Doc | Status |
|---|---|
| 00-overview + 01-data-types | ✅ done |
| 02-connection-lifecycle | ✅ done |
| 03-handshake | ⬜ re-dispatch (cancelled by interrupt) |
| 04-status-ping | ✅ done |
| 05-login-encryption | ⬜ re-dispatch |
| 06-configuration | ⬜ re-dispatch |
| 07-version-differences | ✅ done (has the full version map) |
| proxy-forwarding/ (6 files) | ⬜ re-dispatch (opus) |
**Action: re-dispatch the 4 ⬜ rows** (specs in §6). Wave 1 closes when all 8 are in.
### Phase 2 — Version map
Build `versions/INDEX.md`: every protocol-bumping release 1.7.10→26.2 with {release, protocol #, release date, ViaVersion package, mc-wiki release-article URL, mc-wiki protocol-archive URL}. Derived from `07` + VV `protocols/` listing + MCWIKI. This index drives Phase 3 and becomes the navigation hub for `versions/`.
### Phase 3 — Per-version deep-dive (batched)
One doc per **release line** under `versions/` (e.g. `versions/1.13.md`), each with a **sub-section per protocol bump** inside that line (1.13=393, 1.13.1=401, 1.13.2=404). Granularity rationale: a full doc per trivial patch bump is noise; a doc per major.minor line with per-bump sections is thorough without 50 stubs. *(If you want one-doc-per-protocol-number instead, say so — easy to split.)*
Batched by the **7 eras** from `07` (keeps each wave ≤ concurrency cap, lets us quality-gate between):
- B1 — Era 1: 1.7.10, 1.8
- B2 — Era 2: 1.9, 1.10, 1.11, 1.12
- B3 — Era 3: 1.13, 1.14, 1.15
- B4 — Era 4: 1.16, 1.17, 1.18
- B5 — Era 5: 1.19
- B6 — Era 6: 1.20
- B7 — Era 7: 1.21, 26.x
After each batch: I read the docs, spot-check citations, resolve obvious gaps, then launch the next. Per-version agent spec in §7.
### Phase 4 — Verification & polish
- `fact-verifier` agents on: all `<!-- VERIFY -->` flags, every protocol number in the map, the crypto details (AES/CFB8, serverId hash) and forwarding formats (Velocity HMAC payload, Bungee `\0` string).
- Cross-link docs; fill the README status table (⬜/✅ per doc); write `references.md`.
- Then offer to commit + push to `Timemachine/minecraft_protocol`.
## 4. Conventions (all phases)
- **Models**: research/writing agents = `sonnet`; hard synthesis (proxy-forwarding, big-era version docs) = `opus`; never `haiku` for protocol reasoning. Verifiers = `fact-verifier` type.
- **No nested fan-out**: every agent works directly, spawns **no** sub-agents.
- **One file owner**: each agent writes only its assigned file(s) — no two agents touch the same file (parallel-safe).
- **Citations + VERIFY**: as §1.
- **Mermaid**: sequence/state diagrams for flows; parse-safe (quote labels containing `:` or special chars).
---
## 5. Per-agent task spec (common template)
Every research agent is handed:
1. **Exact output path(s)** in `~/Documents/minecraft_protocol/`.
2. **Topic** + the specific points to cover.
3. **Sources to read**, by path/URL (from §2), with the instruction to cite `path:line` / wiki-URL inline.
4. **Format**: markdown, headers + tables; mermaid for flows; example payloads where useful.
5. **Rules**: version-aware; wire formats from source not memory; `<!-- VERIFY -->` for anything unconfirmed; **no sub-agents**; touch only the assigned file(s).
6. **Report back**: file path(s) + 3-line summary + any VERIFY flags + (for format-critical docs) the exact extracted format with its citation.
## 6. Phase-1 re-dispatch assignments (the 4 pending)
- **03-handshake.md** (sonnet) — handshake packet fields; SRV; serverAddress overloads: Forge `\0FML\0`/`\0FML2\0`/`\0FML3\0`, BungeeCord legacy `host\0ip\0uuid\0props` (exact, from `BUNGEE/.../InitialHandler.java` + `ServerConnector.java`), note Velocity-modern uses a login plugin msg instead; transfer next-state(3)=1.20.5+.
- **05-login-encryption.md** (sonnet) — login packet flow; RSA-1024 + AES/CFB8; serverId negative-hex SHA-1; online-mode join/hasJoined (Mojang session server); compression (1.8+); 1.19 profile keys. Sources: NMP `client/encrypt.js`, `transforms/encryption.js`; VELO `EncryptionRequest/ResponsePacket.java`; MCWIKI Protocol_Encryption.
- **06-configuration.md** (sonnet) — the 1.20.2 Configuration state; registry/tags/packs → Finish; re-configuration loop; Known Packs (1.20.5). Sources: MCWIKI Configuration; NMP/VELO state handling.
- **proxy-forwarding/** (opus, 6 files) — README, online-offline-modes, bungeecord-legacy, velocity-modern, bungeeguard, forge-fml. Velocity HMAC payload from `VELO/.../PlayerDataForwarding.java` + `VelocityConstants.java`; Bungee format from `BUNGEE/.../InitialHandler.java`+`ServerConnector.java`. Comparison table; mermaid for both forwarding flows.
## 7. Phase-3 per-version agent spec (template)
For release line **X** (e.g. 1.16), write `versions/X.md` covering every protocol bump in that line. Each agent:
**Read:**
- MCWIKI release article(s) `/w/Java_Edition_X` and each patch → headline gameplay + technical changes (**release notes**).
- MCWIKI protocol archive for X (the version-specific protocol page / the diff section).
- VV package(s) for the bump(s) into X — `protocols/vPREVtoX/` — the `ProtocolPREVToX.java` + `Clientbound/ServerboundPacketsX` enums = the authoritative changed-packet list. AND `git -C ViaVersion log --oneline -- <that package>` = the **commits** that implemented it.
- MD `protocol.json` for X (raw fetch) to cross-check packet IDs/fields.
**Write** `versions/X.md` with:
- Header: release line, protocol number(s) + release date(s) (cite MCWIKI/MD).
- **Headline changes** (release notes): what shipped, 1-paragraph.
- **Protocol changes**: new / removed / renumbered / restructured packets (from VV enums + MCWIKI), grouped by state (handshake/status/login/config/play). Note data-format changes (NBT, registries, chunk, item components).
- **Per-patch sub-sections** if the line had multiple protocol numbers (e.g. 1.16→735, 1.16.2→751…), each noting what that bump specifically changed (cite the VV package + commit).
- **Proxy/forwarding & translation impact**: what a proxy/ViaVersion must do for this version.
- Citations inline; `<!-- VERIFY -->` for gaps.
**Report:** path + which protocol numbers covered + notable changes + VERIFY flags.
---
## 8. Status log
- 2026-06-19: repo created (`Timemachine/minecraft_protocol`), refs cloned, README + this PLAN written. Wave-1: 4/8 docs landed (00/01, 02, 04, 07). Next: re-dispatch the 4 pending (§6), then Phase 2.
+34
View File
@@ -0,0 +1,34 @@
# Minecraft Java Edition Protocol — a deep-dive reference (1.7.10 → latest)
A from-scratch, version-aware study of the Minecraft: Java Edition network protocol — how a client and server (and the proxies between them) actually talk. Built by reading the spec (minecraft.wiki, ex-wiki.vg) alongside real reference implementations, and writing down what's true, what changed between versions, and *why*.
**Scope**: the wire protocol from **1.7.10** (protocol 5) to the latest release. Emphasis on the structural/cross-version mechanics — framing, the connection state machine, the handshake, login + encryption, the configuration phase, and **proxy forwarding modes** — rather than an exhaustive packet-by-packet dump (minecraft.wiki already is that; this explains how the pieces fit and how they drift across versions).
## Contents
| Doc | Covers |
|---|---|
| [00-overview.md](00-overview.md) | TCP framing, packet structure (length-prefixed), the four/five connection states, big picture |
| [01-data-types.md](01-data-types.md) | VarInt/VarLong, String, UUID, Position, NBT, Identifier, arrays, optionals |
| [02-connection-lifecycle.md](02-connection-lifecycle.md) | State machine handshake→status/login→(configuration)→play, with the per-version transitions |
| [03-handshake.md](03-handshake.md) | The handshake packet; `serverAddress`-field abuse (Forge FML, BungeeCord, Velocity); SRV records; the protocol-version table |
| [04-status-ping.md](04-status-ping.md) | Modern Server List Ping; legacy 1.6 ping (0xFE); the status JSON + favicon |
| [05-login-encryption.md](05-login-encryption.md) | LoginStart, Encryption Request/Response, RSA + AES/CFB8, shared secret, server-id hash, online-mode `hasJoined`, compression handshake, 1.19 profile keys |
| [06-configuration.md](06-configuration.md) | The 1.20.2+ configuration state, registry sync, known-packs (1.20.5+), play↔config re-entry |
| [07-version-differences.md](07-version-differences.md) | Protocol-number table 1.7.10→latest, the major breaking changes per era, and how ViaVersion translates between them |
| [proxy-forwarding/](proxy-forwarding/) | What a proxy does; online vs offline mode; BungeeCord legacy forwarding; Velocity modern forwarding; BungeeGuard; Forge/FML |
| [versions/](versions/INDEX.md) | **Per-version deep-dives** — one doc per release line (1.7.10→26.2), what changed in each protocol bump, sourced from release notes + ViaVersion commits + wiki |
| [references.md](references.md) | Every source: spec pages + which reference repo to read for what |
| [PLAN.md](PLAN.md) | How this repo was built — orchestration + per-agent research spec |
## How this was built
Iterative: research a topic against minecraft.wiki + the reference implementations below, write the doc with citations (source `file:line` + wiki section), cross-link, verify dubious claims, repeat.
**Reference implementations consulted** (see [references.md](references.md) for exact files):
- [PrismarineJS/node-minecraft-protocol](https://github.com/PrismarineJS/node-minecraft-protocol) — clean JS impl + `minecraft-data` per-version packet definitions
- [PaperMC/Velocity](https://github.com/PaperMC/Velocity) — modern forwarding, login flow
- [SpigotMC/BungeeCord](https://github.com/SpigotMC/BungeeCord) — legacy forwarding
- [ViaVersion/ViaVersion](https://github.com/ViaVersion/ViaVersion) — the canonical map of what changed between every protocol version
> Status: **foundation + per-version docs complete** (8 topical docs + 16 release-line docs, 1.7.10→26.2). Remaining: a verification pass over the inline `<!-- VERIFY -->` flags (minor detail-level uncertainties — protocol numbers + crypto/forwarding formats are already multi-source-confirmed). See [PLAN.md](PLAN.md).
+66
View File
@@ -0,0 +1,66 @@
# Proxy Forwarding Modes
How a Minecraft proxy moves a player between backend servers — and how it securely tells each backend *who the player is*.
This is the most load-bearing section of the reference. Get forwarding wrong and you either (a) break logins, or (b) open your network to anyone connecting as anyone.
## What a proxy does
A Minecraft proxy (BungeeCord, Waterfall, [Velocity](https://github.com/PaperMC/Velocity)) sits in front of *N* backend servers (lobby, survival, minigames, modded, …). The player's client connects **once** to the proxy. The proxy speaks the full client-facing protocol — handshake, login, encryption, online-mode auth against Mojang — and then opens its *own* connection to a backend server and pumps packets between the two. When the player runs `/server survival`, the proxy silently tears down the backend connection and opens a new one to a different backend, all without the client reconnecting.
```mermaid
sequenceDiagram
autonumber
actor C as "Client (player)"
participant P as "Proxy (Velocity/BungeeCord)"
participant L as "Backend: lobby (offline-mode)"
participant S as "Backend: survival (offline-mode)"
C->>P: "connect once (online-mode auth here)"
P->>L: "open backend conn + forward identity"
L-->>C: "play (via proxy relay)"
Note over C,S: "player runs /server survival"
P->>S: "open new backend conn + forward identity"
S-->>C: "play (via proxy relay)"
```
## Why forwarding exists
The proxy already authenticated the player against Mojang. The **backends must not** repeat that — they run in **offline mode** (`online-mode=false`), because:
- A backend in online mode would demand its *own* Mojang session handshake from the connecting party (the proxy), which the proxy can't satisfy on the player's behalf.
- Running backends offline lets the proxy own a single client connection and freely re-point it.
But an offline-mode server, by itself, knows nothing real about who connected. It only sees a username string in `LoginStart`, and it derives an **offline UUID** from that name (see [online-offline-modes.md](online-offline-modes.md)). So the proxy must *forward* the player's real identity to the backend:
- real **client IP** (so the backend sees the player, not the proxy)
- real **UUID** (the Mojang account UUID for premium players)
- **username**
- **game-profile properties** — the `textures` property (skin/cape) and, on newer versions, the player's signature/public key
And — critically — the backend must be able to **trust** that this forwarded identity actually came from the proxy and not from an attacker who found the backend's port. That trust mechanism is what distinguishes the modes.
## The menu of modes
| Mode | Doc | One-line |
|---|---|---|
| **none** | [online-offline-modes.md](online-offline-modes.md) | No forwarding. Backend uses the raw `LoginStart` name → offline UUID. Only safe if the backend is the edge. |
| **legacy / bungeecord** | [bungeecord-legacy.md](bungeecord-legacy.md) | Proxy stuffs `ip\0uuid\0properties` into the handshake address field. **No crypto.** |
| **bungeeguard** | [bungeeguard.md](bungeeguard.md) | Legacy + a shared secret token smuggled in the forwarded properties. Third-party. |
| **modern / velocity** | [velocity-modern.md](velocity-modern.md) | Backend requests identity via a Login Plugin message; proxy replies with an **HMAC-SHA256-signed** payload. Gold standard. |
## Comparison
| Mode | Crypto? | Backend support needed | Spoofable if backend port exposed? | Forge-friendly |
|---|---|---|---|---|
| **none** | — | none (default offline behavior) | **Yes** — anyone can claim any name | n/a |
| **legacy (BungeeCord)** | None | Spigot/Paper `settings.bungeecord: true` (or Fabric equiv.) | **Yes** — forge the `\0`-delimited string | Yes — `\0FML\0` marker rides in the handshake (see [forge-fml.md](forge-fml.md)) |
| **bungeeguard** | Shared secret (plaintext token, compared by the backend plugin) | Legacy forwarding **+** BungeeGuard plugin on the backend | **No** (attacker lacks the token), *but* the token travels in plaintext inside the connection | Yes — same handshake-field transport as legacy |
| **modern (Velocity)** | **HMAC-SHA256** over the payload, keyed by a shared secret | Paper `velocity.enabled`+`velocity.secret` (or Fabric/Forge mod that implements it) | **No** — attacker can't produce a valid HMAC without the secret | Needs care — see [forge-fml.md](forge-fml.md) (the FML handshake competes with the login-plugin channel) |
**Rule of thumb**: use **modern/Velocity** when every backend supports it; fall back to **bungeeguard** when a backend only speaks legacy; use bare **legacy** only inside a network where the backend ports are firewalled so *only the proxy* can reach them; use **none** only when the server is itself the internet edge in online mode.
> The "exposed port" risk is the whole point. With legacy and none, your only defense is the firewall — bind backends to localhost / a private interface and let only the proxy connect. With bungeeguard and modern, the protocol itself rejects forged identities. See [online-offline-modes.md](online-offline-modes.md#security-model) for why offline backends are open by default.
## Mapping to the automc platform
In the automc stack, each backend Minecraft server runs with a **Velocity sidecar** managed by `mc-wrapper` (the wrapper runs the proxy alongside the MC server in the same pod). `mc-wrapper`'s `PROXY_MODE` concept selects which of these forwarding modes the sidecar↔server pair uses — practically that means **modern/Velocity forwarding** (the [velocity-modern.md](velocity-modern.md) path) with a shared secret, since proxy and server are co-located and the wrapper provisions the secret. This doc set is the protocol-level reference behind that knob; the automc-specific wiring lives in the `mc-wrapper` repo, not here.
+116
View File
@@ -0,0 +1,116 @@
# BungeeCord Legacy IP Forwarding
The original forwarding scheme, introduced by BungeeCord and adopted everywhere. It works by **abusing the handshake's `serverAddress` field** — the proxy appends the player's identity to that string, null-byte delimited, and the offline-mode backend parses it back out. **No cryptography is involved.**
## The mechanism
The handshake packet (see [../03-handshake.md](../03-handshake.md)) has a `serverAddress` (a.k.a. *Server Address* / host) string — normally the hostname the client used to connect (e.g. `mc.example.com`). The vanilla server mostly ignores its content. BungeeCord repurposes it: when IP-forwarding is enabled, the proxy **rewrites** that field before opening the backend connection, packing four `\0`-separated segments into it.
The backend (a Spigot/Paper server with `settings.bungeecord: true`, or a Fabric server with an equivalent mod) recognizes the extra segments and reads the player's real IP, UUID, and properties out of them — instead of treating the whole string as a hostname.
## Exact wire format
The proxy rewrites the handshake host to:
```
realHost \0 clientIP \0 playerUUID(no dashes) \0 texturesPropertiesJSON
```
Where (in order):
| Segment | Content |
|---|---|
| `realHost` | the original handshake host the client sent (e.g. `mc.example.com`), minus any trailing FML marker |
| `clientIP` | the player's real socket IP, sanitized (brackets stripped from IPv6, scope id removed) |
| `playerUUID` | the player's UUID **with dashes removed** (32 hex chars) |
| `texturesPropertiesJSON` | a JSON array of the login profile's game-profile **properties** (the Mojang-signed `textures` skin/cape entry). **Omitted entirely** (along with its leading `\0`) if the profile has no properties — i.e. the player connected in offline/cracked mode upstream. |
This is exactly what BungeeCord writes in `ServerConnector.connected()`:
> ```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() );
> }
> copiedHandshake.setHost( newHost );
> ```
>
> — `BungeeCord/proxy/src/main/java/net/md_5/bungee/ServerConnector.java:116-123`
(BungeeCord writes the null byte as the Java octal escape `"\00"`, i.e. a single `U+0000`.) Note the **UUID has no dashes**`user.getUUID()` returns the undashed form. The properties JSON is the serialized array of `{name, value, signature}` objects; for an online player it's the single Mojang-signed `textures` property.
Velocity, when configured in `legacy` mode, builds the **same** string — and its source documents the format verbatim:
> ```java
> // BungeeCord IP forwarding is simply a special injection after the "address" in the handshake,
> // separated by \0 (the null byte). In order, you send the original host, the player's IP, their
> // UUID (undashed), and if you are in online-mode, their login properties (from Mojang).
> final StringBuilder data = new StringBuilder()
> .append(serverAddress).append(LEGACY_SEPARATOR)
> .append(playerAddress).append(LEGACY_SEPARATOR)
> .append(profile.getUndashedId()).append(LEGACY_SEPARATOR);
> GENERAL_GSON.toJson(profile.getProperties(), data);
> ```
>
> — `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:160-172` (`LEGACY_SEPARATOR = '\0'`, defined at `:50`)
## How the backend parses it
The receiving side splits the host on `\0`. BungeeCord's *own* handshake handler (which is what a downstream BungeeCord-as-backend, or a Spigot `bungeecord:true` server emulating the same logic, does) splits the host and keeps the tail:
> ```java
> if ( handshake.getHost().contains( "\0" ) )
> {
> String[] split = handshake.getHost().split( "\0", 2 );
> handshake.setHost( split[0] );
> extraDataInHandshake = "\0" + split[1];
> }
> ```
>
> — `BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:355-360`
A Spigot/Paper backend with bungeecord forwarding enabled does the analogous thing: it splits the host into `[host, ip, uuid, properties]`, sets the player's address to `ip`, the UUID to the dash-inserted form of `uuid`, and the game-profile properties to the parsed JSON.
## Sequence
```mermaid
sequenceDiagram
autonumber
actor C as "Client (player)"
participant P as "Proxy (BungeeCord, ip_forward=true)"
participant B as "Backend (Spigot bungeecord=true, offline-mode)"
C->>P: "Handshake host=mc.example.com"
C->>P: "LoginStart (username)"
Note over P: "online-mode auth vs Mojang (hasJoined)"
Note over P: "rewrite host -> realHost\\0clientIP\\0uuidNoDashes\\0propsJSON"
P->>B: "Handshake host='realHost\\0IP\\0UUID\\0props'"
P->>B: "LoginStart (username, rewriteId)"
Note over B: "split host on \\0; trust IP/UUID/props as-is"
B-->>C: "LoginSuccess (via proxy relay) -> play"
```
## Security: none
There is **no signature, no secret, no verification**. The backend trusts the `\0`-delimited string completely. If an attacker can reach the backend's port, they simply send a handshake with a hand-crafted `host\0ip\0uuid\0props` string and connect as **any player they like**, with any UUID and any skin.
The *only* defense for bare legacy forwarding is the network: **firewall the backend so only the proxy can connect** (bind to localhost / a private interface; drop everything else). This is the central weakness that [BungeeGuard](bungeeguard.md) (adds a secret token to the properties) and [Velocity modern forwarding](velocity-modern.md) (HMAC-signs the whole payload) exist to fix.
Velocity even warns the operator when a legacy backend closes the connection — almost always a misconfigured `bungeecord: true`:
> *"This is usually because the remote server does not have BungeeCord IP forwarding correctly enabled."* — `Velocity/.../backend/LoginSessionHandler.java:205-212`
## Forge note
If the client is on Forge, FML appends its own `\0FML\0` (or newer `\0FML2\0` / `\0FML3\0`) marker to the handshake host. The proxy must split that off **before** injecting the forwarding segments and re-append it after, or the marker collides with the forwarding `\0` delimiters. BungeeCord handles this by stashing everything from the first `\0` as `extraDataInHandshake` (`InitialHandler.java:355-360`) and restoring it only when IP forwarding is *off* (`ServerConnector.java:124-128`). See [forge-fml.md](forge-fml.md).
---
**Sources**
- `BungeeCord/proxy/src/main/java/net/md_5/bungee/ServerConnector.java:116-123` — the WRITE of `host\0ip\0uuid\0props`.
- `BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:355-360` — the split/parse of the host on `\0`.
- `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:50` (`LEGACY_SEPARATOR`), `:154-173` (`createLegacyForwardingAddress`) — Velocity building the identical string, with the format documented in-comment.
- `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java:205-212` — legacy-misconfiguration diagnostic.
+69
View File
@@ -0,0 +1,69 @@
# BungeeGuard
A **hardening of [legacy forwarding](bungeecord-legacy.md)**, not a new transport. Legacy's fatal flaw is that anyone who can reach the backend port can forge the `\0`-delimited identity string and connect as anyone. BungeeGuard fixes that by smuggling a **shared secret token** into the forwarded data; the backend plugin refuses the login unless the token matches. It bridges backends that **only support legacy forwarding** up to "can't be trivially forged," without requiring [modern/Velocity](velocity-modern.md) support.
It is **third-party** — not part of Spigot/Paper core or the Mojang protocol. The original is the [BungeeGuard plugin](https://github.com/lucko/BungeeGuard) (lucko), installed on both the proxy and every backend.
## How it works
BungeeGuard reuses the exact legacy wire format — the four `\0`-separated handshake-address segments (`realHost\0clientIP\0uuidNoDashes\0propertiesJSON`). The trick is in the **properties JSON**: it appends one **synthetic game-profile property** named **`bungeeguard-token`** whose value is the shared secret.
A game-profile property is a `{name, value, signature?}` object — the same shape as the Mojang `textures` property. BungeeGuard injects an extra one:
```json
[
{ "name": "textures", "value": "...", "signature": "..." },
{ "name": "bungeeguard-token","value": "<the shared secret>" }
]
```
On the backend, the BungeeGuard plugin (replacing the stock legacy parser) reads the player's properties, finds the `bungeeguard-token` entry, and compares its value against the secret it was configured with. Match → accept and strip the token; mismatch or absent → reject the connection. The real IP/UUID/skin are still taken from the same legacy segments.
Velocity has this mode built in (`player-info-forwarding-mode = "legacy"` is bare legacy; `"bungeeguard"` adds the token). Its source builds the address identically to legacy and just adds the token property:
> ```java
> private static final String BUNGEE_GUARD_TOKEN_PROPERTY_NAME = "bungeeguard-token";
> ...
> final GameProfile.Property property = new GameProfile.Property(
> BUNGEE_GUARD_TOKEN_PROPERTY_NAME,
> new String(forwardingSecret, StandardCharsets.UTF_8),
> "");
> return createLegacyForwardingAddress(serverAddress, playerAddress, profile,
> properties -> ImmutableList.<GameProfile.Property>builder()
> .addAll(properties).add(property).build());
> ```
>
> — `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:52` (name) + `:175-196` (`createBungeeGuardForwardingAddress`)
So a BungeeGuard handshake address is exactly a legacy one with one extra property in the JSON array.
## Security: better than legacy, weaker than modern
| | bare legacy | BungeeGuard | modern (Velocity) |
|---|---|---|---|
| Forge-able by anyone reaching the port | **Yes** | No (needs the token) | No (needs the secret) |
| Secret/token crosses the wire | n/a | **Yes — token sent in cleartext** inside the connection | No (only an HMAC crosses) |
| Tamper-proofs IP/UUID/props | No | **No** — only gate-keeps; the identity fields are still unsigned plaintext | **Yes** — whole payload is HMAC'd |
| Backend requirement | core `bungeecord:true` | BungeeGuard plugin | Paper/Fabric modern support |
The crucial limitation: BungeeGuard is **authentication of the connection, not authentication of the payload**. It proves "whoever sent this knows the token," but the IP/UUID/properties themselves are not signed — once the token check passes, those fields are trusted as-is, exactly like legacy. And the token is transmitted **in cleartext** in the handshake. So:
- If the connection between proxy and backend is **not** encrypted/private, a network observer can lift the token and then forge freely.
- The token is a single shared value across all backends; leaking it from any one host compromises all.
It is a real improvement over bare legacy (you no longer rely solely on a firewall) and the right choice for a backend that **cannot** do modern forwarding. But when every backend supports it, **[modern/Velocity forwarding](velocity-modern.md) is strictly stronger** — it signs the payload with an HMAC and never puts the secret on the wire.
## When to use it
- A backend (old Spigot, a plugin platform, a Fabric server without modern-forward support) only speaks **legacy**, and you can't firewall it tightly enough to trust bare legacy.
- You're migrating a BungeeCord/Waterfall network and want hardening without switching every backend to Velocity-native forwarding.
Otherwise prefer modern. See the [README comparison table](README.md#comparison).
---
**Sources**
- `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:52` (`bungeeguard-token` property name), `:175-196` (`createBungeeGuardForwardingAddress` — legacy address + token property), `:154-173` (shared `createLegacyForwardingAddress` it builds on).
- [lucko/BungeeGuard](https://github.com/lucko/BungeeGuard) — the original third-party plugin (token property + backend check). <!-- VERIFY: plugin-side check logic from upstream README, not read from cloned source -->
- See [bungeecord-legacy.md](bungeecord-legacy.md) for the underlying `\0`-delimited wire format BungeeGuard extends.
+99
View File
@@ -0,0 +1,99 @@
# Forge / FML and Proxies
Forge (via FML — Forge Mod Loader) changes the handshake in a way that **collides with legacy forwarding**, because both want to use the same field: the handshake **Server Address** string. A proxy in front of modded backends has to understand and preserve the FML markers, or modded clients fail to connect.
## The FML handshake markers
Since FML 1.8, a Forge client **appends a token to the handshake host** so a Forge server can detect that the client is modded. The marker is a `\0`-delimited suffix:
| Marker | Era |
|---|---|
| `\0FML\0` | FML 1.8+ (legacy Forge, ~1.81.12) |
| `\0FML2\0` | newer FML (1.13+ "new" Forge networking) |
| `\0FML3\0` | later Forge revisions |
So a Forge client's handshake host looks like `mc.example.com\0FML\0` instead of plain `mc.example.com`. Both BungeeCord and Velocity hard-code the legacy token:
> ```java
> // BungeeCord
> public static final String FML_TAG = "FML";
> public static final String FML_HANDSHAKE_TAG = "FML|HS";
> public static final String FML_HANDSHAKE_TOKEN = "\0FML\0"; // "The FML 1.8 handshake token."
> ```
>
> — `BungeeCord/proxy/.../forge/ForgeConstants.java:13,14,20`
> ```java
> // Velocity (legacy Forge)
> public static final String HANDSHAKE_HOSTNAME_TOKEN = "\0FML\0";
> public static final String FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS";
> ```
>
> — `Velocity/proxy/.../forge/legacy/LegacyForgeConstants.java:29,34`
The deeper mod-list negotiation then happens over the `FML|HS` plugin-message channel (legacy) or modern Forge login plugin messages — that's a separate, larger handshake. The `\0FML\0` marker is just the "I am modded" flag riding in the address field.
## The collision with legacy forwarding
[Legacy/BungeeCord forwarding](bungeecord-legacy.md) **also** packs data into the handshake host (`realHost\0clientIP\0uuid\0props`), `\0`-delimited. If a Forge client adds `\0FML\0` and the proxy then naively appends `\0IP\0UUID\0props`, the two collide — the backend can't tell which `\0` segment is which.
Proxies solve this by **splitting the FML tail off first**, doing their own thing, then restoring it. BungeeCord stashes everything from the first `\0` as `extraDataInHandshake`:
> ```java
> // 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.
> if ( handshake.getHost().contains( "\0" ) )
> {
> String[] split = handshake.getHost().split( "\0", 2 );
> handshake.setHost( split[0] );
> extraDataInHandshake = "\0" + split[1];
> }
> ```
>
> — `BungeeCord/proxy/.../connection/InitialHandler.java:355-360`
When IP forwarding is **off**, BungeeCord re-appends that saved tail so the modded backend still sees its `\0FML\0`:
> `copiedHandshake.setHost( copiedHandshake.getHost() + user.getExtraDataInHandshake() );`
> — `BungeeCord/proxy/.../ServerConnector.java:124-128`
But when IP forwarding is **on**, the FML tail can't be reattached — the forwarding segments already own the field. BungeeCord's own code marks this as a known gap (`// TODO: Add support for this data with IP forwarding.`, `ServerConnector.java:127`). Velocity is explicit about the same conflict in **legacy** mode and works around it by moving the Forge flag into a profile **property** instead of the hostname:
> ```java
> // We can't forward the FML token to the server when we are running in legacy forwarding mode,
> // since both use the "hostname" field in the handshake. We add a special property to the
> // profile instead, which will be ignored by non-Forge servers and can be intercepted by a
> // Forge coremod, such as SpongeForge.
> if (forwardingType == PlayerInfoForwarding.LEGACY) {
> return original.addProperty(IS_FORGE_CLIENT_PROPERTY);
> }
> ```
>
> — `Velocity/proxy/.../forge/legacy/LegacyForgeConnectionType.java:41-49`
## Modern Forge + Velocity (modern forwarding)
[Modern/Velocity forwarding](velocity-modern.md) does **not** touch the handshake address field, so the `\0FML\0` host collision goes away — the FML marker and the identity data no longer fight over the same field. The remaining interplay is the **Forge login-phase handshake itself**, which runs as login plugin messages on the same login state where Velocity sends `velocity:player_info`. The two coexist (different channels), but a modded backend's FML handshake and the forwarding handshake must both complete during login.
Practical notes:
- **Modern Forge (1.13+)** uses its own login plugin-message handshake; a Velocity backend running modern forwarding handles both because they're distinct channels. <!-- VERIFY: modern Forge + native modern forwarding compatibility depends on the backend mod (e.g. a Velocity-forwarding mod alongside Forge); not exhaustively read from source -->
- **ViaForge / client-side shims**: tools like ViaForge let a Forge client speak to a backend across version gaps; they have to reproduce or tolerate the FML handshake markers so the proxy and backend negotiate the modded handshake correctly. <!-- VERIFY: ViaForge specifics not read from cloned source -->
- For modded networks, **modern forwarding is preferable** precisely because it sidesteps the address-field collision that makes legacy + Forge brittle.
## Summary
- The `\0FML\0` / `\0FML2\0` / `\0FML3\0` marker is Forge saying "I'm modded," appended to the handshake host.
- It **collides with legacy forwarding** (shared field) → proxies must split it off and restore it, and **can't** restore it while legacy IP-forwarding is on (BungeeCord TODO; Velocity moves the flag to a property).
- **Modern forwarding avoids the field entirely**, so it's the cleaner choice for Forge backends; the Forge mod-list handshake then runs as separate login plugin messages.
---
**Sources**
- `BungeeCord/proxy/.../forge/ForgeConstants.java:13-20``FML_TAG`, `FML_HANDSHAKE_TAG = "FML|HS"`, `FML_HANDSHAKE_TOKEN = "\0FML\0"`.
- `BungeeCord/proxy/.../connection/InitialHandler.java:355-360` — split the `\0FML\0` tail off the host, save as `extraDataInHandshake`.
- `BungeeCord/proxy/.../ServerConnector.java:124-128` — restore the FML tail only when IP forwarding is off (`:127` TODO notes the gap when it's on).
- `Velocity/proxy/.../forge/legacy/LegacyForgeConstants.java:29,34``HANDSHAKE_HOSTNAME_TOKEN = "\0FML\0"`, `FORGE_LEGACY_HANDSHAKE_CHANNEL = "FML|HS"`.
- `Velocity/proxy/.../forge/legacy/LegacyForgeConnectionType.java:41-49` — legacy forwarding can't carry the FML host token; flag moved to a profile property instead.
- See [bungeecord-legacy.md](bungeecord-legacy.md) (the colliding `\0` transport) and [velocity-modern.md](velocity-modern.md) (the field-free alternative).
+72
View File
@@ -0,0 +1,72 @@
# Online vs Offline Mode
The `online-mode` server property is the root of every forwarding decision. Understand it first; the four forwarding modes are all answers to the problem offline mode creates.
## What `online-mode` controls
`online-mode=true` (the default in `server.properties`) means: **after** the login handshake, the server verifies the connecting player against Mojang's session servers. It does **not** by itself control whether the connection is encrypted.
The login sequence (see [../05-login-encryption.md](../05-login-encryption.md) for the full packet flow) is, in both modes:
1. Client → `LoginStart` (username, and on 1.19+ a profile public key / on 1.20.2+ the profile UUID).
2. Server → `Encryption Request` (server-id string, server's RSA public key, verify token).
3. Client → `Encryption Response` (the AES shared secret + verify token, both RSA-encrypted with the server's public key).
4. Both sides switch to **AES/CFB8** encryption using the shared secret.
Steps 24 — the encryption negotiation — **happen regardless of online/offline mode** whenever the vanilla server requests them. What online mode adds is one extra check between steps 3 and 4 conceptually:
- **Online mode**: the server computes the *server-id hash* (SHA-1 over the server-id string + shared secret + server public key) and calls Mojang's session endpoint **`hasJoined`**:
`https://sessionserver.mojang.com/session/minecraft/hasJoined?username=<name>&serverId=<hash>`.
Mojang confirms that this account really just authenticated with that server-id (the client side calls the matching `join` endpoint). The response carries the player's **real account UUID** and **properties** (the `textures` property — skin & cape, signed by Mojang). If `hasJoined` returns nothing, the login is rejected.
- **Offline mode**: the server **skips the `hasJoined` call entirely**. No Mojang verification. The server trusts the username from `LoginStart` as-is. (Vanilla offline servers also skip the encryption step; the point is that *no identity proof* is required.)
So the difference that matters for forwarding is: **offline mode does not call `hasJoined`, so it has no proof the username is real, and it gets no real UUID or skin from Mojang.** It must invent both.
## UUID derivation
How the player's UUID is determined differs by mode:
- **Online mode**: the UUID is the player's **real Mojang account UUID**, returned by the `hasJoined` response. It's a stable, account-bound, "version 4"-style identifier assigned by Mojang.
- **Offline mode**: the server **derives** a UUID deterministically from the username. The algorithm is a **name-based (version 3 / MD5) UUID** over the bytes of the ASCII string `"OfflinePlayer:" + username`:
```java
UUID offlineId = UUID.nameUUIDFromBytes(
("OfflinePlayer:" + name).getBytes(StandardCharsets.UTF_8));
```
`java.util.UUID.nameUUIDFromBytes` produces an **RFC-4122 version 3** UUID — it MD5-hashes the input bytes and stamps the version/variant bits. The same username always yields the same offline UUID, on every server, forever. (Note: no namespace UUID is prepended — it's a plain MD5 of just those bytes, which is why this is "version-3-like" rather than a strictly RFC-compliant namespaced v3.)
This is confirmed directly in BungeeCord's source — `InitialHandler.finish()`:
> `offlineId = UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + getName() ).getBytes( StandardCharsets.UTF_8 ) );`
>
> — `BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:560`
and the [minecraft.wiki](https://minecraft.wiki/w/Universally_unique_identifier) describes it the same way: *"the unique identifier chosen for offline mode players is UUID version 3, generated from the MD5 hash of `OfflinePlayer:<username>`."*
The practical consequence: a premium player has **different** UUIDs online vs offline. This is exactly why forwarding must carry the *real* (online) UUID to the backend — otherwise the offline backend would assign the player a different identity than the one their account/inventory/permissions are keyed to.
## Security model
> **An offline-mode backend with a reachable port is wide open.**
Because an offline server does no `hasJoined` check, it accepts **whatever username the connecting party sends in `LoginStart`** and derives the UUID from it. There is no proof. Anyone who can reach the port can connect as `Notch`, as an admin, as any player — and the offline server will hand them that player's UUID, inventory, and op level.
In a proxy setup the player's identity is established **at the proxy** (which runs online mode and does the real `hasJoined`). The backends are deliberately offline so the proxy can move players. That is the entire reason forwarding + hardening exist:
1. **Forwarding** gives the backend the player's *real* IP / UUID / properties instead of the bogus offline-derived ones — see [bungeecord-legacy.md](bungeecord-legacy.md) and [velocity-modern.md](velocity-modern.md).
2. **Hardening** stops an attacker from connecting *directly* to the offline backend and forging that same identity:
- **Firewall** the backend so only the proxy's address can connect (the *only* defense for bare legacy / none).
- **BungeeGuard** — a shared secret token the backend checks ([bungeeguard.md](bungeeguard.md)).
- **Modern/Velocity** — an HMAC the backend verifies, so forged identities are cryptographically rejected ([velocity-modern.md](velocity-modern.md)).
Bind your backends to a private interface and trust nothing that arrives without one of these proofs.
---
**Sources**
- `BungeeCord/proxy/src/main/java/net/md_5/bungee/connection/InitialHandler.java:560` — offline UUID = `nameUUIDFromBytes("OfflinePlayer:"+name)`; `:135` `onlineMode`; `:595` `isOnlineMode()` gating the auth call.
- [minecraft.wiki — Universally unique identifier](https://minecraft.wiki/w/Universally_unique_identifier) — version-3 MD5 `OfflinePlayer:<username>` derivation.
- [minecraft.wiki — Java Edition protocol (login + encryption)](https://minecraft.wiki/w/Java_Edition_protocol) — Encryption Request/Response, server-id hash, `hasJoined`.
- See also [../05-login-encryption.md](../05-login-encryption.md) for the encryption packet detail.
+132
View File
@@ -0,0 +1,132 @@
# Velocity Modern Forwarding
The **gold-standard** forwarding mode. The proxy and every backend share a secret string; the proxy signs the forwarded identity with **HMAC-SHA256** keyed by that secret, and the backend refuses to start the player's session unless the HMAC verifies. An attacker who reaches the backend's port still cannot forge an identity, because they don't have the secret — so this mode is safe **even if the backend port is exposed**, unlike [legacy](bungeecord-legacy.md) (firewall-only) forwarding.
It is called "modern" because, unlike legacy, it does **not** abuse the handshake address field. It rides the protocol's own **Login Plugin Message** mechanism (login-state plugin channels), which is a clean, in-band request/response added in the modern protocol.
## Setup
- **Proxy** (Velocity): `player-info-forwarding-mode = "modern"` and a `forwarding.secret` (a random secret string, stored in a `forwarding.secret` file).
- **Backend** (Paper): `paper.yml` (or `config/paper-global.yml`) → `proxies.velocity.enabled: true`, `proxies.velocity.online-mode: true`, `proxies.velocity.secret: <same secret>`. <!-- VERIFY: exact paper.yml key path varies by Paper version; secret value must equal the proxy's --> Fabric/Forge backends use a mod (e.g. FabricProxy-Lite) that implements the same handshake.
The backend still runs `online-mode=false` at the vanilla level — modern forwarding *is* its identity source.
## The handshake direction (important)
Normal Login Plugin Messages go server→client. Here the roles are inverted: the **backend** is the "server" and the **proxy** is the "client" of that backend connection. So:
1. The proxy connects to the backend and sends the normal **Handshake** + **LoginStart** (just the username; no identity stuffed into the address — that is the whole difference from legacy).
2. The **backend** sends a **Login Plugin Request** on channel **`velocity:player_info`**, asking "who is this, and prove it." It may include one byte: the highest forwarding version the backend supports.
3. The **proxy** replies with a **Login Plugin Response** carrying the HMAC-signed identity payload.
4. The backend verifies the HMAC, reads the identity, and proceeds. If no `velocity:player_info` request was answered by login's end, Velocity aborts with *"If you are a server owner, make sure you have ... `forwarding-secret`."* (`MODERN_IP_FORWARDING_FAILURE`).
The channel name and version constants are defined in Velocity source:
> ```java
> public static final String CHANNEL = "velocity:player_info";
> public static final int MODERN_DEFAULT = 1; // base payload
> public static final int MODERN_WITH_KEY = 2; // + 1.19 profile key
> public static final int MODERN_WITH_KEY_V2 = 3; // + signer UUID (1.19.1+)
> public static final int MODERN_LAZY_SESSION = 4; // 1.19.3+, key dropped again
> public static final int MODERN_MAX_VERSION = MODERN_LAZY_SESSION;
> ```
>
> — `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:42-48`
(The Velocity-side request handler reads the backend's requested version: `if (packet.content().readableBytes() == 1) requestedForwardingVersion = packet.content().readByte();``backend/LoginSessionHandler.java:89-93`.)
## Exact payload layout
The proxy builds the response body in `PlayerDataForwarding.createForwardingData(...)`. The **signed payload** is, in order:
| # | Field | Type | Notes |
|---|---|---|---|
| 1 | forwarding version | VarInt | the negotiated version (14) |
| 2 | client IP | String | the player's real remote address |
| 3 | player UUID | UUID | the real (online) UUID — 16 bytes, **not** a string |
| 4 | username | String | |
| 5 | game-profile properties | Properties | array of `{name, value, signature?}` — includes the Mojang-signed **`textures`** (skin/cape) |
| 6 | *(if version 23)* player public key | IdentifiedKey | 1.19 message-signing key |
| 7 | *(if version ≥ 3)* signer UUID present? + UUID | Boolean [+ UUID] | the key's signature-holder UUID, when known |
Then the whole thing is **prefixed** by its HMAC. The signed buffer is built first, the MAC is computed over exactly those bytes, and the 32-byte HMAC is concatenated **in front**:
> ```java
> ProtocolUtils.writeVarInt(forwarded, actualVersion);
> ProtocolUtils.writeString(forwarded, address);
> ProtocolUtils.writeUuid(forwarded, profile.getId());
> ProtocolUtils.writeString(forwarded, profile.getName());
> ProtocolUtils.writeProperties(forwarded, profile.getProperties());
> // ... (optional player key / signer UUID for versions 23) ...
>
> final Mac mac = Mac.getInstance(ALGORITHM); // "HmacSHA256"
> mac.init(new SecretKeySpec(secret, ALGORITHM)); // keyed by the shared secret
> mac.update(forwarded.array(), forwarded.arrayOffset(), forwarded.readableBytes());
> final byte[] sig = mac.doFinal();
>
> return Unpooled.wrappedBuffer(Unpooled.wrappedBuffer(sig), forwarded);
> ```
>
> — `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:69-102`
> (`ALGORITHM = "HmacSHA256"` at `:40`)
So the **wire body of the Login Plugin Response is**:
```
[ HMAC-SHA256(secret, payload) : 32 bytes ] ++ [ payload ]
^ VarInt version, String IP, UUID,
String name, Properties, (key…)
```
The backend recomputes `HMAC-SHA256(secret, payload)` over the bytes after the first 32, and rejects the login if it doesn't match the prefix. Because the secret never crosses the wire and the IP/UUID/name/props are all inside the MAC'd region, **nothing can be tampered with or forged** without the secret. That is the security win over legacy and BungeeGuard.
## Sequence
```mermaid
sequenceDiagram
autonumber
actor C as "Client (player)"
participant P as "Proxy (Velocity, mode=modern, secret=S)"
participant B as "Backend (Paper, velocity.secret=S, offline-mode)"
C->>P: "Handshake + LoginStart (username)"
Note over P: "online-mode auth vs Mojang (hasJoined)"
P->>B: "Handshake + LoginStart (username only)"
B->>P: "Login Plugin Request 'velocity:player_info' (+ max ver byte)"
Note over P: "payload = version,IP,UUID,name,props[,key]"
Note over P: "sig = HMAC-SHA256(S, payload)"
P->>B: "Login Plugin Response: sig(32B) ++ payload"
Note over B: "recompute HMAC(S, payload); compare to sig"
alt "HMAC matches"
B-->>C: "LoginSuccess (via proxy relay) -> play"
else "HMAC mismatch / no response"
B-->>P: "disconnect (forwarding failure)"
end
```
## Why it's the gold standard
| Property | Modern | Legacy | BungeeGuard |
|---|---|---|---|
| Identity tamper-proof | **Yes** (whole payload HMAC'd) | No | partial (token gates, but IP/UUID still plaintext) |
| Safe with backend port exposed | **Yes** | No (firewall-only) | mostly (attacker lacks token) |
| Secret crosses the wire | **No** (only the HMAC does) | n/a | **Yes** (token sent in cleartext properties) |
| Uses protocol's own channel mechanism | Yes (Login Plugin Message) | No (address-field abuse) | No (address-field abuse) |
The only catch is **backend support**: every backend must implement modern forwarding (Paper natively; Fabric/Forge via a mod). Where a backend can only speak legacy, fall back to [BungeeGuard](bungeeguard.md). For Forge interplay, see [forge-fml.md](forge-fml.md).
## Login Plugin Message reference
The transport is the protocol's login-state plugin messaging (see also [../05-login-encryption.md](../05-login-encryption.md)):
- **Login Plugin Request** (clientbound, login state, packet `0x04`): Message ID (VarInt), Channel (Identifier), Data (byte array, channel-specific, no length prefix). Here the *backend* sends it. <!-- VERIFY: packet ID 0x04 is current-protocol; older versions differ -->
- **Login Plugin Response** (serverbound, login state, packet `0x02`): Message ID (VarInt), then a **prefixed-optional** Data byte array — present only if the request was understood. An unrecognized channel is answered with the empty/"not understood" form. Here the *proxy* sends it, with Data = `sig ++ payload`.
— [minecraft.wiki — Java Edition protocol (Login Plugin Request / Response)](https://minecraft.wiki/w/Java_Edition_protocol)
---
**Sources**
- `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/PlayerDataForwarding.java:40` (`ALGORITHM="HmacSHA256"`), `:42-48` (channel + forwarding-version constants), `:57-111` (`createForwardingData` — payload order + HMAC prefix), `:99-102` (MAC over the payload, `wrappedBuffer(sig, forwarded)`).
- `Velocity/proxy/src/main/java/com/velocitypowered/proxy/connection/backend/LoginSessionHandler.java:83-106` (backend's `velocity:player_info` request handling → proxy reply), `:62` + `:146-147` (`MODERN_IP_FORWARDING_FAILURE` when no info was forwarded).
- [minecraft.wiki — Java Edition protocol](https://minecraft.wiki/w/Java_Edition_protocol) — Login Plugin Request (0x04) / Response (0x02) field layout.
+44
View File
@@ -0,0 +1,44 @@
# References & sources
Everything in this repo is grounded in the spec + real reference implementations, cited inline as `path:line` / wiki-URL + fetch date. This is the catalog of what was used and **which source to read for what**.
## Spec of record — minecraft.wiki (formerly wiki.vg)
wiki.vg was merged into **minecraft.wiki** in 2024; the protocol documentation lives under the Java Edition protocol pages.
| Page | Use |
|---|---|
| [`/w/Java_Edition_protocol`](https://minecraft.wiki/w/Java_Edition_protocol) | the live protocol: packet format, data types, per-state packet lists |
| [`/w/Protocol_version`](https://minecraft.wiki/w/Protocol_version) | the authoritative version→protocol-number table |
| `/w/Java_Edition_protocol/Server_List_Ping` | modern SLP + legacy `0xFE` ping |
| `/w/Java_Edition_protocol/Protocol_Encryption` | the AES/RSA encryption handshake |
| `/w/Java_Edition_<version>` | per-version **release notes** (gameplay + technical) |
> Historical per-version protocol pages are spotty on minecraft.wiki (some archive URLs 404). For exact per-version packet layouts we relied on **minecraft-data** + **ViaVersion** instead, which are versioned by construction.
## Reference implementations
Cloned to `/tmp/mcproto-refs/` during the build (ephemeral — re-clone to reproduce). Read for:
| Repo | Read for | Key files |
|---|---|---|
| [node-minecraft-protocol](https://github.com/PrismarineJS/node-minecraft-protocol) | clean reference impl of framing, encryption, ping, states | `src/states.js`, `src/transforms/encryption.js`, `src/client/encrypt.js`, `src/ping.js`, `src/transforms/framing.js`, `src/transforms/compression.js` |
| [minecraft-data](https://github.com/PrismarineJS/minecraft-data) | **per-version `protocol.json`** — authoritative packet IDs + field types per version (the diff source for the version docs) | `data/pc/<version>/protocol.json`, `data/pc/<version>/version.json`, `data/pc/common/protocolVersions.json` |
| [ViaVersion](https://github.com/ViaVersion/ViaVersion) | **what changed between adjacent versions** + the commit history that implemented it | `api/.../protocol/version/ProtocolVersion.java` (version constants), `common/.../protocols/v<X>to<Y>/` (per-step translation: `Protocol*.java`, `Clientbound/ServerboundPackets*.java`, `rewriter/`, `data/`), `git log -- <package>` |
| [Velocity](https://github.com/PaperMC/Velocity) | modern forwarding, login flow, state registry | `proxy/.../connection/PlayerDataForwarding.java` (the HMAC forwarding payload), `connection/VelocityConstants.java`, `connection/backend/LoginSessionHandler.java`, `protocol/packet/EncryptionRequest/ResponsePacket.java`, `protocol/StateRegistry.java`, `connection/forge/legacy/LegacyForgeConstants.java` |
| [BungeeCord](https://github.com/SpigotMC/BungeeCord) | legacy IP forwarding format | `proxy/.../connection/InitialHandler.java` (handshake parse), `ServerConnector.java` (the `\0`-delimited forward write), `forge/ForgeConstants.java` |
## Related projects (named, not cloned)
- **ViaBackwards** — extends ViaVersion so *newer* clients reach *older* servers (reverse direction).
- **ViaRewind** / **ViaLegacy** — compatibility below ViaVersion's 1.8 floor (down to 1.7.x and older release/beta protocols).
- **ViaForge** — Forge/FML handshake handling across versions.
- **BungeeGuard** (lucko) — token hardening of legacy forwarding.
- **Waterfall** — BungeeCord fork (now largely superseded by Velocity).
## Citation conventions in this repo
- Reference-impl facts: `repo/path/File.ext:line`.
- ViaVersion change-provenance: the package path + `git log` commit subject/hash.
- Spec facts: the minecraft.wiki URL + fetch date (`2026-06-19`).
- Anything not confirmable from the above carries an inline `<!-- VERIFY -->` flag (see [PLAN §3 Phase 4](PLAN.md) — the open-flag resolution pass).
+218
View File
@@ -0,0 +1,218 @@
# Java Edition 1.10 — Protocol 210
**Release line:** 1.10, 1.10.1, 1.10.2 (all protocol 210)
**Protocol number:** 210
**Preceding protocol:** 110 (shared by 1.9.3 and 1.9.4)
**Release dates:**
- 1.10 — June 8, 2016 ([minecraft.wiki/w/Java_Edition_1.10](https://minecraft.wiki/w/Java_Edition_1.10), fetched 2026-06-19)
- 1.10.1 — June 22, 2016 ([minecraft.wiki/w/Java_Edition_1.10.1](https://minecraft.wiki/w/Java_Edition_1.10.1), fetched 2026-06-19)
- 1.10.2 — June 23, 2016 ([minecraft.wiki/w/Java_Edition_1.10.2](https://minecraft.wiki/w/Java_Edition_1.10.2), fetched 2026-06-19)
Sources used:
- ViaVersion `v1_9_3to1_10` package: `common/src/main/java/com/viaversion/viaversion/protocols/v1_9_3to1_10/` (git log: `cff9a87`, `64c128d`, `262677326`, `9f6e7fa`, `22d396d`, `e436bbe`, `864beef`, `463381b`, `1039b85`, `75d68516`, `5286efde`, `501f65e2`, `e965e971`)
- minecraft-data `data/pc/1.9.4/protocol.json` vs `data/pc/1.10/protocol.json`
- minecraft.wiki release articles (URLs above)
---
## Headline: The Frostburn Update — minor protocol bump
1.10 is a content update focused on cold and hot biomes: polar bears, husks (desert zombie variant), strays (ice skeleton variant), magma blocks, bone blocks, nether wart blocks, red nether bricks, structure void, and structure blocks. The jump from protocol 110 to 210 is a significant numeric gap but reflects only a small set of actual wire-format changes. No packets were added, removed, or renumbered. The entire translation in ViaVersion is handled by three packet rewrites plus a sound-ID remapping table.
**The bottom line from source:** `Protocol1_9_3To1_10.java` extends `AbstractProtocol<ClientboundPackets1_9_3, ClientboundPackets1_9_3, ServerboundPackets1_9_3, ServerboundPackets1_9_3>` — the input and output packet enums are identical, confirming zero structural additions or renumbering.
(Source: `common/.../v1_9_3to1_10/Protocol1_9_3To1_10.java`, line 40)
---
## Protocol changes vs 1.9.4 (protocol 110)
Authoritative diff: `minecraft-data data/pc/1.9.4/protocol.json` vs `data/pc/1.10/protocol.json` — three field-level changes across two packets (clientbound) and one packet (serverbound).
### Play state — Clientbound
#### 0x19 Named Sound Effect (`CUSTOM_SOUND`) — pitch type change
| Field | 1.9.4 | 1.10 |
|---|---|---|
| soundName | String | String |
| soundCategory | VarInt | VarInt |
| x | Int (fixed-point ×8) | Int |
| y | Int (fixed-point ×8) | Int |
| z | Int (fixed-point ×8) | Int |
| volume | Float | Float |
| **pitch** | **UByte (u8)** | **Float (f32)** |
Source: `minecraft-data data/pc/1.9.4/protocol.json` line 1194 vs `data/pc/1.10/protocol.json` line 1194.
ViaVersion translation: `Protocol1_9_3To1_10.java` `TO_NEW_PITCH` transformer at line 4247:
```java
public Float transform(PacketWrapper wrapper, Short inputValue) {
return inputValue / 63.0F;
}
```
The 1.9.4 `u8` pitch was a 063 scale divided by 63 to yield a float. 1.10 sends the float directly.
(Source: `common/.../v1_9_3to1_10/Protocol1_9_3To1_10.java` lines 6070)
#### 0x46 Sound Effect (`SOUND`) — pitch type change (same as above)
| Field | 1.9.4 | 1.10 |
|---|---|---|
| soundId | VarInt | VarInt |
| soundCategory | VarInt | VarInt |
| x | Int | Int |
| y | Int | Int |
| z | Int | Int |
| volume | Float | Float |
| **pitch** | **UByte (u8)** | **Float (f32)** |
Source: `minecraft-data data/pc/1.9.4/protocol.json` line 2657 vs `data/pc/1.10/protocol.json` line 2657.
Same `TO_NEW_PITCH` transformer applied. Additionally, `SOUND` carries an integer sound ID that is remapped — see Sound Registry section below.
(Source: `common/.../v1_9_3to1_10/Protocol1_9_3To1_10.java` lines 7390)
### Play state — Serverbound
#### 0x16 Resource Pack Status (`RESOURCE_PACK`) — hash field removed
| Field | 1.9.4 | 1.10 |
|---|---|---|
| **hash** | **String (removed)** | — |
| result | VarInt | VarInt |
In 1.9.x the client echoed back the resource pack hash string alongside the status result code. In 1.10 the hash field was dropped; the server tracks the hash from its own `Send Resource Pack` (0x32) packet.
Source: `minecraft-data data/pc/1.9.4/protocol.json` line 3402 vs `data/pc/1.10/protocol.json` line 3402.
ViaVersion works around this on the serverbound path by tracking the last hash sent in `ResourcePackTracker` storage and re-inserting it when translating a 1.10 client's status packet back to a 1.9.4 server:
```java
// Packet ResourcePack status (serverbound, 0x16)
handler(wrapper -> {
ResourcePackTracker tracker = wrapper.user().get(ResourcePackTracker.class);
wrapper.write(Types.STRING, tracker.getLastHash());
wrapper.write(Types.VAR_INT, wrapper.read(Types.VAR_INT));
});
```
(Source: `common/.../v1_9_3to1_10/Protocol1_9_3To1_10.java` lines 152161;
`common/.../v1_9_3to1_10/storage/ResourcePackTracker.java`)
---
## Packet inventory summary
No packet IDs added, removed, or renumbered between 1.9.4 (protocol 110) and 1.10 (protocol 210).
- Clientbound Play: 0x000x4B (76 packets) — **unchanged set**
- Serverbound Play: 0x000x1D (30 packets) — **unchanged set**
Confirmed by: `minecraft-data` mapping comparison (zero diff on packet ID↔name mappings) and ViaVersion using the same `ClientboundPackets1_9_3` / `ServerboundPackets1_9_3` enums for both sides of the translation.
---
## Sound registry expansion
The `SOUND` packet (0x46) addresses sounds by integer ID. 1.10 inserted 19 new sound events into the registry, shifting existing IDs upward in several ranges.
New sounds (source: `minecraft-data data/pc/1.9.4/sounds.json` vs `data/pc/1.10.2/sounds.json` — 444 → 463 entries):
| New sound event | Group |
|---|---|
| `block.enchantment_table.use` | Enchanting table |
| `entity.husk.ambient` / `.death` / `.hurt` / `.step` | Husk (4) |
| `entity.polar_bear.ambient` / `.baby_ambient` / `.death` / `.hurt` / `.step` / `.warning` | Polar bear (6) |
| `entity.stray.ambient` / `.death` / `.hurt` / `.step` | Stray (4) |
| `entity.wither_skeleton.ambient` / `.death` / `.hurt` / `.step` | Wither skeleton (4) |
The ViaVersion `getNewSoundId(int id)` method encodes the exact insertion points (source: `Protocol1_9_3To1_10.java` lines 165178):
```java
public int getNewSoundId(int id) {
int newId = id;
if (id >= 24) newId += 1; // enchanting table sound inserted
if (id >= 248) newId += 4; // husk (4 sounds)
if (id >= 296) newId += 6; // polar bear (6 sounds)
if (id >= 354) newId += 4; // stray (4 sounds)
if (id >= 372) newId += 4; // wither skeleton (4 sounds)
return newId;
}
```
A proxy translating a 1.9.4 server's `SOUND` packet for a 1.10 client must apply this remapping; in the reverse direction (1.10 client → 1.9.4 server) no remapping is needed for sound packets (sounds are identified by ID serverbound only in older custom-payload flows; `SOUND` is clientbound only).
---
## Entity metadata — No-Gravity index (entity data index 5)
ViaVersion's `EntityPacketRewriter1_10` adds entity data index 5 (`No Gravity`, Boolean) to all entities during the 1.9.4→1.10 translation:
```java
@Override
protected void registerRewrites() {
// The item data slot was created via the wrong entity type class,
// using an index of 6 instead of 5 for the item
filter().type(EntityTypes1_9.EntityType.POTION).removeIndex(5);
filter().addIndex(5); // No gravity
}
```
(Source: `common/.../v1_9_3to1_10/rewriter/EntityPacketRewriter1_10.java` lines 8286)
This means 1.10 added entity data index 5 (type: Boolean, default false) to the base entity class to control whether an entity ignores gravity. The POTION entity already used index 5 for a different purpose in 1.9.x (wrongly assigned), so ViaVersion strips that index from potions before adding the universal one.
The `NoGravity` flag in NBT was extended in 1.10 to work for all entity types (wiki note: previously worked for armor stands only). <!-- VERIFY: wiki article implies NoGravity became universal in 1.10 but does not give a precise "index 5 added in 1.10" citation; ViaVersion insertion is confirmed from source -->
---
## New blocks and items (protocol impact)
New item IDs in 1.10 (source: `minecraft-data data/pc/1.10/items.json`):
| Item name | ID |
|---|---|
| magma | 213 |
| nether_wart_block | 214 |
| red_nether_brick | 215 |
| bone_block | 216 |
| structure_void | 217 |
| structure_block | 255 |
These IDs appear in any packet carrying an `Item` slot (e.g., `CONTAINER_SET_SLOT` 0x16, `CONTAINER_SET_CONTENT` 0x14, `SET_EQUIPPED_ITEM` 0x3C, `SET_CREATIVE_MODE_SLOT` serverbound 0x18). A 1.9.x server does not recognise IDs 213217; ViaVersion replaces them with stone (id=1, data=0) on the serverbound path:
(Source: `common/.../v1_9_3to1_10/rewriter/ItemPacketRewriter1_10.java` lines 4148)
ViaVersion also handles `piston_extension` (block ID 36), which is a server-side technical block that can appear in `LEVEL_CHUNK` data; it replaces it with a configurable fallback via `isReplacePistons()` / `getPistonReplacementId()`.
(Source: `Protocol1_9_3To1_10.java` lines 125136)
## New mobs — entity type IDs
Husks, Strays, and Polar Bears are the three new mobs in 1.10. In the 1.10 protocol they do **not** have new entity type IDs. They are transmitted as variants of existing types using entity metadata:
- **Husk** — sent as Zombie (type ID 54); identified by entity data index 13 (ZombieType VarInt) = 6
- **Stray** — sent as Skeleton (type ID 51); identified by entity data index 12 (SkeletonType VarInt) = 2
- **Polar Bear** — sent as a distinct entity type <!-- VERIFY: polar bear may have been a new entity ID in 1.10; minecraft-data entities.json comparison shows no new entry, but the wiki mentions it as a new mob; EntityTypes1_9 does not list POLAR_BEAR, and the 1.10 minecraft-data entities.json only has 120 Villager as the highest mob, suggesting polar bear may be type 102 added in 1.10 but not reflected in the 1.9-era entities.json used for comparison -->
Wither Skeletons in 1.10: encoded as Skeleton (type ID 51) with SkeletonType = 1 (via entity data index 12). Separate type IDs for husk, stray, and wither skeleton were only introduced in 1.11.
(Source: `common/.../v1_10to1_11/rewriter/EntityPacketRewriter1_11.java` lines 346373 — the 1.11 rewriter shows exactly how 1.10's metadata-encoded variants map to 1.11's distinct entity IDs)
---
## Per-patch sub-sections
### 1.10.1 (protocol 210, released June 22, 2016)
No protocol changes. The update fixed 7 bugs, the most notable being a farmland hitbox adjustment (15/16-block height). Zero wire-format differences from 1.10.
(Source: minecraft.wiki/w/Java_Edition_1.10.1; minecraft-data 1.10 vs 1.10.1 protocol.json diff = 0 lines)
### 1.10.2 (protocol 210, released June 23, 2016)
No protocol changes. 73 bug fixes; witch splash sound reclassified to hostile category. Zero wire-format differences from 1.10.1.
(Source: minecraft.wiki/w/Java_Edition_1.10.2; minecraft-data 1.10.1 vs 1.10.2 protocol.json diff = 0 lines)
---
## Proxy and translation impact
A proxy or protocol translator bridging 1.10 clients and 1.9.x servers (or vice versa) must handle:
1. **Pitch type on SOUND (0x46) and CUSTOM_SOUND (0x19):** `u8 → f32` on the clientbound path. Conversion: `f32 = u8 / 63.0`.
2. **Sound ID remapping on SOUND (0x46):** Apply `getNewSoundId()` offsets when translating 1.9.x server sound IDs to 1.10 client IDs.
3. **Resource Pack Status (0x16, serverbound):** 1.10 client omits the hash string. When forwarding to a 1.9.x server, re-insert the hash last seen in the Send Resource Pack packet (0x32).
4. **New item IDs 213217 (serverbound):** Replace with a fallback (e.g., stone) before forwarding to 1.9.x servers that do not know these IDs.
5. **Entity data index 5 (No Gravity):** 1.10 clients expect index 5 on all entities. When a 1.9.x server does not send it, a proxy should synthesize it (default: false). Potion entities need special handling due to a metadata index collision in 1.9.x.
6. **Chunk data:** Format unchanged from 1.9.3/1.9.4 (`ChunkType1_9_3`); no translation needed beyond the piston block opt-in workaround.
No login, status, or handshake state changes in 1.10.
+288
View File
@@ -0,0 +1,288 @@
# 1.11.x — The Exploration Update
| Release | Protocol | Date | minecraft-data dir |
|---|---|---|---|
| 1.11 | **315** | 2016-11-14 | `data/pc/1.11` |
| 1.11.1 | **316** | 2016-12-20 | *(inherits 1.11)* |
| 1.11.2 | **316** | 2016-12-21 | `data/pc/1.11.2` |
**Sources:**
- Release notes: [minecraft.wiki/w/Java_Edition_1.11](https://minecraft.wiki/w/Java_Edition_1.11) (fetched 2026-06-19); [minecraft.wiki/w/Java_Edition_1.11.1](https://minecraft.wiki/w/Java_Edition_1.11.1) (fetched 2026-06-19); [minecraft.wiki/w/Java_Edition_1.11.2](https://minecraft.wiki/w/Java_Edition_1.11.2) (fetched 2026-06-19)
- ViaVersion `v1_10to1_11`: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_10to1_11/``Protocol1_10To1_11.java`, `rewriter/EntityPacketRewriter1_11.java`, `rewriter/ItemPacketRewriter1_11.java`, `data/EntityMappings1_11.java`, `data/BlockEntityMappings1_11.java`, `data/PotionColorMappings1_11.java`
- ViaVersion `v1_11to1_11_1`: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_11to1_11_1/``Protocol1_11To1_11_1.java`, `rewriter/ItemPacketRewriter1_11_1.java`
- minecraft-data: `/tmp/mcproto-refs/minecraft-data/data/pc/1.10/`, `1.11/`, `1.11.2/`
---
## Headline
The Exploration Update shipped woodland mansions, the illager mob family (evokers, vindicators, vexes), llamas, shulker boxes, the observer block, and a totem of undying. From a **protocol standpoint this is a minor-to-medium bump**: no packets were added or removed between 1.10 (protocol 210) and 1.11 (protocol 315); however, several existing packets received field-level changes and the entire entity/block-entity NBT namespace was migrated from Pascal-cased legacy names to `snake_case` namespaced identifiers. The 315→316 bump (1.11.1) is one of the smallest in the entire 1.x series — only one serverbound packet was changed to handle a new item added in 1.11.1.
---
## Protocol changes vs 1.10 (315)
Confirmed by diff of `minecraft-data/data/pc/1.10/protocol.json` vs `data/pc/1.11/protocol.json` and ViaVersion `v1_10to1_11` rewriters.
### Packet IDs — no change
All 76 clientbound play packets (0x000x4b) and all serverbound play packets share identical IDs between protocol 210 (1.10) and 315 (1.11). The packet *set* is unchanged. All changes are within existing packet payloads.
Sources: `minecraft-data` `1.10/protocol.json` vs `1.11/protocol.json``toClient` and `toServer` mapping tables are byte-for-byte identical; ViaVersion `Protocol1_10To1_11` extends `AbstractProtocol<ClientboundPackets1_9_3, ClientboundPackets1_9_3, ServerboundPackets1_9_3, ServerboundPackets1_9_3>` reusing the unchanged 1.9.3 packet enum for both old and new sides.
### Play — Clientbound
#### `0x03 spawn_entity_living` (Spawn Living Entity / ADD_MOB)
The `type` field changed wire format:
| Version | Field | Wire type |
|---|---|---|
| 1.10 (210) | `type` | `u8` (unsigned byte) |
| 1.11 (315) | `type` | `varint` |
Source: `minecraft-data/data/pc/1.10/protocol.json` `packet_spawn_entity_living.type = "u8"` vs `1.11/protocol.json` `type = "varint"`. Confirmed by ViaVersion `EntityPacketRewriter1_11.java` line 123: `map(Types.UNSIGNED_BYTE, Types.VAR_INT); // 2 - Entity Type`.
This is necessary because 1.11 expanded the entity type table to numbered IDs well beyond 255 and reorganised the ID space (see Entity Type Remapping below).
#### `0x45 title` (Title / SET_TITLES)
An **action bar action was inserted** at index 2, shifting the existing timing action from 2 to 3:
| Action index | 1.10 (210) | 1.11 (315) |
|---|---|---|
| 0 | Set title | Set title |
| 1 | Set subtitle | Set subtitle |
| 2 | Set times | **Set action bar** (new) |
| 3 | — | Set times (was 2) |
| 4 | Hide | Hide (was 3) |
| 5 | Reset | Reset (was 4) |
Source: `minecraft-data/data/pc/1.10/protocol.json` title `text.fields` = `{"0":"string","1":"string"}`, `fadeIn.fields` = `{"2":"i32"}` vs `1.11/protocol.json` text `fields` = `{"0":"string","1":"string","2":"string"}`, `fadeIn.fields` = `{"3":"i32"}`. Confirmed by ViaVersion `Protocol1_10To1_11.java` lines 7184: `if (action >= 2) { wrapper.set(Types.VAR_INT, 0, action + 1); }` — increments the action number for any value ≥ 2 to make room for action bar at index 2.
#### `0x48 collect` (Take Item Entity / TAKE_ITEM_ENTITY)
A third field `pickupItemCount` was added:
| Version | Fields |
|---|---|
| 1.10 (210) | `collectedEntityId` (varint), `collectorEntityId` (varint) |
| 1.11 (315) | `collectedEntityId` (varint), `collectorEntityId` (varint), `pickupItemCount` (varint) |
Source: `minecraft-data/data/pc/1.10/protocol.json` `packet_collect` has 2 fields; `1.11/protocol.json` has 3, adding `pickupItemCount`. Confirmed by ViaVersion `EntityPacketRewriter1_11.java` lines 152162: `wrapper.write(Types.VAR_INT, 1); // 2 - Pickup Count` — injects a hardcoded `1` to synthesise the field when translating from 1.11 to 1.10 clients.
#### `0x21 world_event` (Effect / LEVEL_EVENT)
Effect ID **2002** (splash potion impact) was split: linear potion data values (036) that previously encoded potion type as a damage-value integer were replaced by **color-based RGB values**. Additionally, instant-effect potions (Instant Health, Instant Damage) were reclassified to use a new effect ID **2007** rather than 2002.
Source: ViaVersion `Protocol1_10To1_11.java` lines 129156 and `data/PotionColorMappings1_11.java` — maps 37 old data values to new RGB color integers; marks indices 2124 as `isInstant = true` which triggers the 2002→2007 remap.
#### `0x09 tile_entity_data` / `0x20 map_chunk` (Block Entity Data / Level Chunk)
Block entity `id` tags in NBT were migrated from **PascalCase** legacy strings to **`minecraft:snake_case`** namespaced identifiers. This affects both the inline block entity list in `LEVEL_CHUNK` (0x20) and the standalone `BLOCK_ENTITY_DATA` (0x09) packet.
Full mapping (old → new), source `BlockEntityMappings1_11.java` in `v1_10to1_11/data/`:
| Old | New |
|---|---|
| `Furnace` | `minecraft:furnace` |
| `Chest` | `minecraft:chest` |
| `EnderChest` | `minecraft:ender_chest` |
| `RecordPlayer` | `minecraft:jukebox` |
| `Trap` | `minecraft:dispenser` |
| `Dropper` | `minecraft:dropper` |
| `Sign` | `minecraft:sign` |
| `MobSpawner` | `minecraft:mob_spawner` |
| `Music` | `minecraft:noteblock` |
| `Piston` | `minecraft:piston` |
| `Cauldron` | `minecraft:brewing_stand` |
| `EnchantTable` | `minecraft:enchanting_table` |
| `Airportal` | `minecraft:end_portal` |
| `Beacon` | `minecraft:beacon` |
| `Skull` | `minecraft:skull` |
| `DLDetector` | `minecraft:daylight_detector` |
| `Hopper` | `minecraft:hopper` |
| `Comparator` | `minecraft:comparator` |
| `FlowerPot` | `minecraft:flower_pot` |
| `Banner` | `minecraft:banner` |
| `Structure` | `minecraft:structure_block` |
| `EndGateway` | `minecraft:end_gateway` |
| `Control` | `minecraft:command_block` |
#### `0x0a block_action` (Block Action / BLOCK_EVENT)
For piston blocks (block type IDs 33 and 29), ViaVersion optionally cancels the block action packet entirely when `isPistonAnimationPatch` is enabled, to suppress rendering glitches on 1.10 clients watching 1.11 servers. This is a ViaVersion compat workaround, not a protocol format change. Source: `Protocol1_10To1_11.java` lines 87105.
#### `0x19 named_sound_effect` (Named Sound Effect / SOUND)
The sound ID space was renumbered. ViaVersion `getNewSoundId()` (`Protocol1_10To1_11.java` lines 190217) documents the shifts:
| Range | Change |
|---|---|
| ID 196 | Removed entirely (experience orb pickup sound) |
| IDs ≥ 85 | +2 (shulker box sounds inserted) |
| IDs ≥ 176 | +1 (guardian flop sound inserted) |
| IDs ≥ 197 | +8 (evocation sounds: 8 new IDs) |
| IDs ≥ 207 | 1 (accounts for the removed ID 196 orb sound) |
| IDs ≥ 279 | +9 (llama sounds: 9 new IDs) |
| IDs ≥ 296 | +1 (mule chest sound inserted) |
| IDs ≥ 390 | +4 (vex sounds: 4 new IDs) |
| IDs ≥ 400 | +3 (vindication illager sounds: 3 new IDs) |
| IDs ≥ 450 | +1 (elytra sound inserted) |
| IDs ≥ 455 | +1 (empty bottle sound inserted) |
| IDs ≥ 470 | +1 (totem use sound inserted) |
### Play — Serverbound
#### `0x08 block_place` (Use Item On / PLAYER_BLOCK_PLACEMENT)
The cursor position fields `cursorX/Y/Z` changed from **signed byte (`i8`, encoded as fixed-point ×16)** to **float (`f32`)**:
| Version | `cursorX/Y/Z` type |
|---|---|
| 1.10 (210) | `i8` (signed byte, value × 16 ÷ 16.0 to recover float) |
| 1.11 (315) | `f32` (IEEE 754 float) |
Source: `minecraft-data/data/pc/1.10/protocol.json` `packet_block_place.cursorX = "i8"` vs `1.11/protocol.json` `cursorX = "f32"`. Confirmed by ViaVersion `Protocol1_10To1_11.java` lines 162173:
```java
private static final ValueTransformer<Float, Short> toOldByte = new ValueTransformer<>(Types.UNSIGNED_BYTE) {
public Short transform(PacketWrapper wrapper, Float inputValue) {
return (short) (inputValue * 16);
}
};
// ...
map(Types.FLOAT, toOldByte); // cursorX
map(Types.FLOAT, toOldByte); // cursorY
map(Types.FLOAT, toOldByte); // cursorZ
```
The transformer multiplies the incoming float by 16 and casts to byte to send the old format to 1.10 servers.
#### `0x02 chat` (Chat Message)
The server-side chat message **length limit was raised from 100 to 256 characters**. ViaVersion truncates outgoing messages to 100 characters when sending to 1.10 servers. Source: `Protocol1_10To1_11.java` lines 175187 and minecraft.wiki 1.11 release notes (fetched 2026-06-19).
### Entity type ID remapping and NBT namespace migration
1.11 was the first version to apply a comprehensive **entity ID/name restructure**. Two changes happened simultaneously:
1. **Numeric IDs were reorganised**: Several sub-types previously encoded via metadata (elder guardian, wither skeleton, stray, husk, zombie villager, skeleton horse, zombie horse, donkey, mule) became **distinct entity type IDs**. In 1.10, only one `EntityHorse` type existed (ID 100); in 1.11, five separate IDs exist: horse (100), donkey (31), mule (32), skeleton horse (28), zombie horse (29). Similarly the single `Skeleton` (51) split into skeleton (51), wither skeleton (5), and stray (6). This is why ViaVersion's `rewriteEntityType()` in `EntityPacketRewriter1_11.java` reads metadata index 12 (skeleton sub-type), 13 (zombie sub-type), or 14 (horse sub-type) to decide the correct new entity ID.
2. **Entity save-data names were namespaced**: All entity NBT `id` strings were migrated from PascalCase (`EntityHorse`, `LavaSlime`, `MushroomCow`, etc.) to `minecraft:snake_case` (`horse`, `magma_cube`, `mooshroom`, etc.). Full mapping in `EntityMappings1_11.java` in `v1_10to1_11/data/` (74 entries). This affects spawn eggs (item 383) and monster spawner block entities.
New mob entity type IDs in 1.11 (minecraft-data `1.11/entities.json`):
| Entity | Type ID (mob) | Object ID |
|---|---|---|
| `evocation_fangs` | 33 | 79 |
| `evocation_illager` (evoker) | 34 | — |
| `vex` | 35 | — |
| `vindication_illager` (vindicator) | 36 | — |
| `llama` | 103 | — |
| `llama_spit` | 104 | 68 |
| `zombie_villager` | 27 | — |
### Item changes (1.10 → 1.11)
New items added (minecraft-data `1.11/items.json` diff vs `1.10/items.json`):
| Item | Notes |
|---|---|
| `observer` | Block + item; IDs 218234 range on server |
| `white_shulker_box` `black_shulker_box` | 16 coloured variants |
| `totem` | Totem of undying |
| `shulker_shell` | Crafting ingredient |
ViaVersion replaces any of these items (IDs 218234, 449, 450) with stone (ID 1) when sending to 1.10 servers. Source: `ItemPacketRewriter1_11.java` lines 9398.
### Item NBT: enchantment glint behaviour change
In 1.10, an item with an empty `ench` list tag (`[]`) displayed an enchantment glint. In 1.11+, the list must contain at least one element. ViaVersion adds a dummy compound entry to empty `ench` tags when sending to 1.10 clients (and removes it on the server path). Source: `ItemPacketRewriter1_11.java` lines 6572.
### Item NBT: negative amount handling
1.11 servers can send items with `amount <= 0` to indicate "nothing here" in certain contexts. 1.10 clients interpret amount 0 as a bug. ViaVersion stores the real amount in an NBT tag (`VV|Amount`) and sets `amount = 1` before forwarding to old clients. Source: `ItemPacketRewriter1_11.java` lines 4956; commit `d0ed52878 Save negative item amounts in 1.10->1.11`.
---
## Per-patch: 315 (1.11) vs 316 (1.11.1 and 1.11.2)
### 1.11.1 — protocol 316 (2016-12-20)
**Gameplay additions** (minecraft.wiki 1.11.1, fetched 2026-06-19):
- Iron nuggets added (item ID 452, smelting iron tools/armor)
- Sweeping Edge enchantment added for swords
- Fireworks now boost elytra-flying players and deal damage on explosion
- Mending and Infinity enchantments became mutually exclusive on bows
- 41 bugs fixed
**Protocol change — one packet:**
Only the serverbound `SET_CREATIVE_MODE_SLOT` (0x18) was affected. ViaVersion `Protocol1_11To1_11_1` (`v1_11to1_11_1`) registers exactly one handler — `ItemPacketRewriter1_11_1.registerPackets()` which registers only `registerSetCreativeModeSlot`. The rewriter's `handleItemToServer` replaces item ID 452 (iron nugget) with stone (ID 1) when sending to 1.11 servers that do not know about that item.
Source: `v1_11to1_11_1/Protocol1_11To1_11_1.java` (only `itemRewriter.register()` called, no other packet registrations); `v1_11to1_11_1/rewriter/ItemPacketRewriter1_11_1.java` lines 3648.
ViaVersion git log for `v1_11to1_11_1/`:
```
cff9a8715 [ci skip] Update copyright header
9f6e7fa4e [ci skip] Update copyright header
1039b8556 Add remaining item types to item rewriter implementations (#3931)
75d86851c Apply IJ code reformat, rename rewriter methods, change metadata references to entity data
5286efde1 Move type instances out of its enclosing class
501f65e21 Packet and entity type renames
e965e9713 Package/class renames and moves
```
(`git -C /tmp/mcproto-refs/ViaVersion log --oneline -- common/.../protocols/v1_11to1_11_1/ | head -20`)
The protocol between 1.11 (315) and 1.11.1 (316) is otherwise **identical** in every other respect. No packet IDs changed, no fields changed in any other packet.
### 1.11.2 — protocol 316 (2016-12-21)
1.11.2 shares **the same protocol number (316)** as 1.11.1. minecraft-data confirms: `data/pc/1.11.2/version.json``"version": 316`. The 1.11.2 directory in minecraft-data contains only `protocolComments.json`, `sounds.json` (493 sounds), and `version.json` — no separate `protocol.json`, confirming no protocol difference from 1.11.1.
Gameplay: one block change (cobblestone walls can no longer be jumped over) and 24 bug fixes. Source: minecraft.wiki 1.11.2 (fetched 2026-06-19). No protocol-level changes.
---
## Proxy / ViaVersion translation impact
Translating a **1.10 client (210) connecting to a 1.11 server (315)** requires all of the following, implemented in `v1_10to1_11/`:
1. **Entity spawn type field**: `ADD_MOB` type field → downcast varint to unsigned byte before forwarding to 1.10 client.
2. **Entity type splitting**: ViaVersion must read entity metadata on spawn to determine which 1.11 entity type the 1.10 monolithic type maps to (e.g. reading skeleton sub-type metadata index 12 to emit `wither_skeleton` vs `skeleton` vs `stray` to old clients).
3. **Title action bar shift**: Decrement action index by 1 for any value ≥ 2 when sending to 1.10 clients (so the new action bar action 2 is dropped; existing times/hide/reset shift back).
4. **Collect pickup count**: Synthesise (drop) the new `pickupItemCount` field when forwarding to 1.10 clients.
5. **Block placement cursor**: Convert `f32` cursor fields to `i8` (×16) when forwarding serverbound to 1.11 servers from 1.10 clients.
6. **Block entity NBT**: Translate all block entity `id` tags from `minecraft:snake_case` → PascalCase on the way to 1.10 clients (and vice versa serverbound).
7. **Entity NBT names**: Translate entity names in spawn eggs and spawner block entities.
8. **Sound IDs**: Remap or cancel sound effect IDs per the offset table.
9. **Potion splash effect IDs**: Remap effect 2002 data values and redirect instant potions to new effect ID 2007.
10. **Item ID blocking**: Block unknown items (IDs 218234, 449, 450) by replacing with stone before forwarding to 1.10 servers.
11. **Enchantment glint compat**: Inject dummy `ench` entry for empty-list items destined for 1.10 clients.
12. **Chat length truncation**: Trim outgoing chat to 100 chars for 1.10 servers.
Translating a **1.11 client (315) connecting to a 1.11.1 server (316)**, or vice versa, requires only:
- Block item ID 452 (iron nugget) in creative-mode slot packets sent to 1.11 (315) servers.
No entity, sound, block entity, or packet-structure translation is needed across the 315→316 boundary.
ViaVersion git log for `v1_10to1_11/` (selected relevant commits):
```
7b097b3e5 Strip trailing on chat messages in 1.10->1.11 (#4686)
3ba86741f Send default entity data for items in 1.10->1.11 (#4265)
5287d4fb4 Fix enchantment glint behaviour in 1.10->1.11 (#4156)
c5dc5b2bf Actually restrict velocity change to fishing hooks
1ff3035bc Make 1.10->1.11 fishing hook position desync slightly less bad
d0ed52878 Save negative item amounts in 1.10->1.11 (#3921)
ae3042074 Add trade list rewriter functions to ItemRewriter (#3926)
8f8f5e72c Default rewriter registrations across protocols
```
(`git -C /tmp/mcproto-refs/ViaVersion log --oneline -- common/.../protocols/v1_10to1_11/ | head -20`)
---
## VERIFY flags
- <!-- VERIFY --> The exact IDs for the 16 shulker box item variants (listed as 218234 in `ItemPacketRewriter1_11.java` lines 9394) — the comment says `item.identifier() >= 218 && item.identifier() <= 234` which is 17 values (218..234 inclusive), but only 16 colors exist. One of the 17 slots may be the shulker shell or observer. Confirm against `1.11/items.json` item ID table.
- <!-- VERIFY --> Fishing hook velocity scaling change (noted in ViaVersion commit `1ff3035bc` and code `tryFixFishingHookVelocity` which multiplies x/z by 1.33 and y by 1.2) — whether this is a documented protocol change or solely a ViaVersion approximation fix is unclear from the code comment ("TODO Fix properly").
- <!-- VERIFY --> Chat length limit change from 100 to 256: wiki says 256; ViaVersion truncates to 100 for 1.10 servers. Confirm the exact 1.11 server-side maximum from protocol spec or `PacketDecoder`.
+174
View File
@@ -0,0 +1,174 @@
# 1.12.x — World of Color Update
**Protocols:** 335 (1.12), 338 (1.12.1), 340 (1.12.2)
**Release dates:** 1.12 — 2017-06-07 · 1.12.1 — 2017-08-03 · 1.12.2 — 2017-09-18
**Minecraft wiki release articles:**
- <https://minecraft.wiki/w/Java_Edition_1.12> (fetched 2026-06-19)
- <https://minecraft.wiki/w/Java_Edition_1.12.1> (fetched 2026-06-19)
- <https://minecraft.wiki/w/Java_Edition_1.12.2> (fetched 2026-06-19)
**Protocol version numbers confirmed:** `ProtocolVersion.java` lines 57-59: `register(335, "1.12")`, `register(338, "1.12.1")`, `register(340, "1.12.2")``/tmp/mcproto-refs/ViaVersion/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java`; cross-checked by `/tmp/mcproto-refs/minecraft-data/data/pc/1.12*/version.json`.
**ViaVersion packages:**
- `v1_11_1to1_12``/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_11_1to1_12/`
- `v1_12to1_12_1``…/protocols/v1_12to1_12_1/`
- `v1_12_1to1_12_2``…/protocols/v1_12_1to1_12_2/`
**minecraft-data sources:** `data/pc/1.12/`, `data/pc/1.12.1/`, `data/pc/1.12.2/``/tmp/mcproto-refs/minecraft-data/`
---
## Headline changes
1.12 ("World of Color Update") shipped 2017-06-07. The two largest protocol additions are the **recipe book** and the **advancements system**, each requiring a new cluster of packets.
The recipe book is a server-driven crafting UI: the client receives a list of unlocked recipe IDs via `Unlock Recipes` (0x30 in 335; shifted to 0x31 in 338) and can request that a recipe's ingredients be placed in a crafting grid via a new serverbound packet. In 1.12 this was `Prepare Crafting Grid` (0x01 SB); the interaction was redesigned in 1.12.1 into `Place Recipe` (0x12 SB, replaces 0x01) and a new server echo `Craft Recipe Response` (0x2B CB).
Advancements replace the pre-1.12 achievement system. Rather than a statistics-based one-time trigger, advancements are a DAG of progress nodes stored on the server and pushed to the client. The `Update Advancements` packet (0x4C in 335; 0x4D in 338) carries a full reset flag, a map of advancement keys to display data and criteria, a list of removed advancements, and a progress map. A companion `Select Advancements Tab` (0x36 in 335; 0x37 in 338) drives the tab UI clientbound. The serverbound `Seen Advancements` (0x19 in both) carries the player's tab-open/tab-close action and current tab ID.
Gameplay additions visible at the protocol level: parrots (new entity type 105 in `EntityTypes1_12`, `ABSTRACT_SHOULDER_RIDING`); coloured beds (the bed block entity now requires a colour field — ViaVersion injects a fake NBT tag with `color:14` for red into chunk data sent to 1.11 clients, `Protocol1_11_1To1_12.java:94-106`); and 33 new parrot sounds (handled via numeric remapping in `getNewSoundId`, `Protocol1_11_1To1_12.java:147-167`). A `Knowledge Book` item was added (items 235-252 and 453 are unknown to 1.11 servers; ViaVersion replaces them with stone, `ItemPacketRewriter1_12.java:65-72`).
---
## Protocol changes vs 1.11 (335 vs 315/316)
Source: diff of `ClientboundPackets1_9_3` (the enum used through 1.11.x) against `ClientboundPackets1_12` and `ServerboundPackets1_9_3` against `ServerboundPackets1_12`; cross-confirmed by `minecraft-data` diff of `data/pc/1.11/protocol.json` vs `data/pc/1.12/protocol.json` which shows +3 CB and +3 SB net-new packets.
### Clientbound play — new packets (1.12 = protocol 335)
| ID | Name (ViaVersion enum / minecraft-data) | Notes |
|---|---|---|
| `0x30` | `RECIPE` / `unlock_recipes` | Pushes the list of unlocked recipe IDs to the client; controls recipe-book state (open, filter). Fields: action (VarInt), craftingBookOpen (bool), filteringCraftable (bool), recipes1 array (VarInt IDs), optional recipes2 array for "init" action. |
| `0x36` | `SELECT_ADVANCEMENTS_TAB` / `select_advancement_tab` | Tells the client which advancement tab to show. Field: optional String tab-id. |
| `0x4C` | `UPDATE_ADVANCEMENTS` / `advancements` | Full advancement sync. Fields: reset (bool), advancement-map (key→display+criteria+requirements), list of removed keys, progress-map (key→criterion-timestamps). |
All three shift IDs for packets above them: `REMOVE_ENTITIES` moves from 0x30 to 0x31, `SELECT_ADVANCEMENTS_TAB` inserts at 0x36 pushing `SET_BORDER` from 0x36 to 0x37, and `UPDATE_ADVANCEMENTS` appends above `UPDATE_ATTRIBUTES` pushing it to 0x4D and `UPDATE_MOB_EFFECT` to 0x4E.
Sources: `ClientboundPackets1_12.java` (full enum with ordinal-derived IDs); `minecraft-data/data/pc/1.12/protocol.json` `.play.toClient`.
### Serverbound play — new packets (1.12 = protocol 335)
| ID | Name (ViaVersion enum / minecraft-data) | Notes |
|---|---|---|
| `0x01` | `CRAFTING_RECIPE_PLACEMENT` / `prepare_crafting_grid` | Client sends items to move into crafting slots. Fields: windowId (u8), actionNumber (u16), array of {item, craftingSlot, playerSlot} for both "return" and "prepare" lists. |
| `0x17` | `RECIPE_BOOK_UPDATE` / `crafting_book_data` | Carries two sub-types: type 0 = displayed-recipe (i32 recipe ID); type 1 = crafting-book-open flag + filtering flag (bools). |
| `0x19` | `SEEN_ADVANCEMENTS` / `advancement_tab` | Player opened/closed an advancements tab. Fields: action (VarInt: 0=opened tab, 1=closed screen), optional tabId String (only for action 0). |
These push `RESOURCE_PACK` from 0x16→0x18, `SET_CARRIED_ITEM` from 0x17→0x1A, `SET_CREATIVE_MODE_SLOT` from 0x18→0x1B, and so on, adding three net slots.
Sources: `ServerboundPackets1_12.java`; `minecraft-data/data/pc/1.12/protocol.json` `.play.toServer`.
ViaVersion strategy (1.11.1→1.12, `Protocol1_11_1To1_12.java`):
- `cancelServerbound(ServerboundPackets1_12.CRAFTING_RECIPE_PLACEMENT)` — drops 0x01 (old server can't handle it)
- `cancelServerbound(ServerboundPackets1_12.RECIPE_BOOK_UPDATE)` — drops 0x17
- `cancelServerbound(ServerboundPackets1_12.SEEN_ADVANCEMENTS)` — drops 0x19
- Locale truncation: `CLIENT_INFORMATION` max locale length raised 7→16 in 1.12; ViaVersion truncates back to 7 when downgrading (line 131-134)
- Bed chunk injection: injects fake `minecraft:bed` NBT block-entity with `color:14` for every bed block (id 26) in chunk data, allowing old servers (which store no bed colour) to render correctly (lines 94-106)
- Sound remapping: 1.12 added 33 parrot sounds + 2 end-portal sounds + block-note sounds + illager sounds; `getNewSoundId` remaps the 1.11 continuous ID space (lines 147-167)
### Packet format notes
**`Unlock Recipes` action field:** VarInt with values 0 (init — sends both recipes1 and recipes2 lists, the first being "currently unlocked", the second being "all you've ever unlocked"), 1 (add), 2 (remove). When action = 0 the packet sends two arrays; for 1 or 2 it sends only recipes1. <!-- VERIFY: exact action semantics for init vs add/remove from wiki -->
**`Update Advancements` structure:** Each advancement entry carries: parentId (optional String), optional displayData {title String, description String, icon Slot, frameType VarInt, flags VarInt, optional background String, x float, y float}, array of criterion keys, array of requirement arrays (AND of OR). Progress map entries carry criterion key → optional completion timestamp (Long).
---
## Per-patch sub-sections
### 1.12 — protocol 335 (2017-06-07)
The initial release. Adds the recipe book, advancements, and all associated packets described above. The three new clientbound and three new serverbound play packets define the 1.12 wire baseline.
**ViaVersion package:** `v1_11_1to1_12`
**Key commits (git log `-- common/src/main/java/com/viaversion/viaversion/protocols/v1_11_1to1_12/`):**
- `8f8f5e72c` — Default rewriter registrations across protocols (includes 1.12 migration)
- `721e27eb3` — Rewrite show_item in translation components in 1.11.1->1.12
- `a92e75b8c` — Properly track minecart object entity types in ≤1.12.2
- `e965e9713` — Package/class renames and moves (original package creation)
### 1.12.1 — protocol 338 (2017-08-03)
**Net packet change: +1 CB, ±0 SB (swap of one packet).**
The recipe-book interaction was redesigned between 1.12 and 1.12.1. The `Prepare Crafting Grid` (0x01 SB) was dropped and replaced by two simpler packets:
**New clientbound:**
| ID | Name | Notes |
|---|---|---|
| `0x2B` | `PLACE_GHOST_RECIPE` (`craft_recipe_response` in minecraft-data) | Server echoes back a recipe ID to fill the crafting ghost items in the UI. Fields: windowId (i8), recipe (VarInt). Inserted between `OPEN_SIGN_EDITOR` and `PLAYER_ABILITIES`, shifting all higher IDs by +1 (PLAYER_ABILITIES 0x2B→0x2C, PLAYER_COMBAT 0x2C→0x2D, etc., through UPDATE_MOB_EFFECT 0x4E→0x4F). |
**Serverbound change:**
| Before (1.12, 335) | After (1.12.1, 338) | Notes |
|---|---|---|
| `0x01` `CRAFTING_RECIPE_PLACEMENT` (prepare_crafting_grid) | *(removed)* | Complex slot-shuffle packet dropped |
| *(absent)* | `0x12` `PLACE_RECIPE` (craft_recipe_request) | Simpler request: just windowId (i8), recipe VarInt, makeAll bool. Client sends this to ask the server to fill the crafting grid with a specific recipe. |
`PLACE_RECIPE` slots in at 0x12 between `PADDLE_BOAT` and `PLAYER_ABILITIES`; `RECIPE_BOOK_UPDATE` and `SEEN_ADVANCEMENTS` stay at 0x17 and 0x19.
ViaVersion strategy: `Protocol1_12To1_12_1.java` — only `cancelServerbound(ServerboundPackets1_12_1.PLACE_RECIPE)` is registered, i.e. 1.12.1 clients talking to 1.12 servers have their `PLACE_RECIPE` dropped (the old server can't understand it; the UI degrades gracefully).
**Other fixes in 1.12.1 (wiki):** security vulnerability in recipe book system (MC-119011); performance fix for `Class.getSimpleName` on large block-entity counts (MC-117087); crafting bug where chat messages moved items into crafting slots (MC-119840).
**ViaVersion package:** `v1_12to1_12_1`
**Key commits:**
- `501f65e21` — Packet and entity type renames (Mojang-mapped name alignment)
- `e965e9713` — Package/class renames and moves (original package creation)
### 1.12.2 — protocol 340 (2017-09-18)
**Net packet change: 0 CB, 0 SB. One field type change.**
The only wire-visible change in 1.12.2 is the **Keep Alive ID type**: both `KEEP_ALIVE` clientbound (0x1F) and `KEEP_ALIVE` serverbound (0x0B) changed their `keepAliveId` field from `VarInt` to `i64` (signed 64-bit little-endian long).
Confirmed by:
- `Protocol1_12_1To1_12_2.java` lines 34-47: registers explicit mappings for both directions: `map(Types.VAR_INT, Types.LONG)` (CB) and `map(Types.LONG, Types.VAR_INT)` (SB).
- `minecraft-data/data/pc/1.12.2/protocol.json`: `packet_keep_alive` CB and SB both show `{name: "keepAliveId", type: "i64"}`, vs VarInt in `data/pc/1.12.1/`.
No packets were added or removed. The 80 CB / 33 SB play-packet counts from 1.12.1 are unchanged in 1.12.2 (`minecraft-data` diff: empty).
**Why this matters for proxies:** A VarInt keep-alive value was always truncated to the range a VarInt can represent; the server's keep-alive ID generator could now use the full 64-bit space. Any proxy doing keep-alive termination that assumed VarInt must be updated, or it will corrupt the field when the server sends a value > 2^28.
ViaVersion: `Protocol1_12_1To1_12_2.java` handles the field-width translation inline without any per-packet enum change; it reuses `ClientboundPackets1_12_1` and `ServerboundPackets1_12_1` unchanged (class declaration line 26).
Other 1.12.2 changes (wiki): title screen now shows "Java Edition" subtitle; 12 bug fixes; Log4Shell security note (affects the embedded Log4j in the dedicated server, not the protocol).
**ViaVersion package:** `v1_12_1to1_12_2`
**Key commits:**
- `5286efde1` — Move type instances out of enclosing class (Types refactor)
- `e965e9713` — Package/class renames and moves
---
## Why 1.12.2 is a common server baseline
1.12.2 (protocol 340) became the de-facto long-running server version for roughly three years (20172020) for two compounding reasons:
1. **The 1.13 "Flattening" was a breaking change.** 1.13 restructured the entire block/item ID space, introduced the text-component registry system, and shifted dozens of packets. The migration cost for modpacks and plugins was enormous. Many server operators held at 1.12.2 rather than upgrade.
2. **Mod ecosystem stability.** Forge support for 1.12.2 matured to a very stable state. The Curse/CurseForge modpack ecosystem had thousands of packs targeting 1.12.2 exclusively. This concentrated a huge portion of the modded playerbase on protocol 340 for years.
The combination made 1.12.2 the last version before the Flattening and the most widely deployed Forge version in history. ViaVersion's `v1_12_2to1_13` package is one of the most complex in the codebase precisely because it had to bridge this gap for that entrenched server population.
---
## Proxy and translation impact
### What a ViaVersion-style proxy must do for 1.12.x clients connecting to ≤1.11 servers
- **Drop the three new SB packets** that 1.11 servers cannot parse: `CRAFTING_RECIPE_PLACEMENT` (0x01), `RECIPE_BOOK_UPDATE` (0x17), `SEEN_ADVANCEMENTS` (0x19). The client degrades gracefully when it receives no response.
- **Remap sound IDs**: 1.12 added 33 parrot sounds, 7 illager sounds, and smaller groups of end-portal and block-note sounds. IDs above each insertion point must be shifted down when forwarding to 1.11 servers.
- **Inject fake bed NBT**: 1.12 clients expect a bed block-entity (with `color` field) to accompany every bed block in chunk data; 1.11 servers do not send one. ViaVersion synthesises it from the palette.
- **Item replacement**: Items 235-252 and 453 (coloured concrete variants, Knowledge Book) are unknown on 1.11 servers; replace with a safe item (e.g. stone).
- **Locale truncation**: `CLIENT_INFORMATION` locale max length is 16 in 1.12 vs 7 in 1.11; truncate to 7 before forwarding.
### What a proxy must do for 1.12.1 clients connecting to 1.12 servers
- **Drop `PLACE_RECIPE` (0x12 SB)** — 1.12 servers do not understand it.
- **Do not forward `PLACE_GHOST_RECIPE` (0x2B CB)** from 1.12 server to 1.12.1 client — this packet does not exist in 1.12; no action needed on that direction (1.12 server simply never sends it).
### What a proxy must do for 1.12.2 clients connecting to 1.12.1 servers
- **Translate Keep Alive ID width** in both directions: `LONG` (i64) ↔ `VAR_INT` on the server side. A 64-bit keep-alive ID from the server must be masked or truncated to VarInt range (max 5 bytes, effectively 28 usable bits) when forwarding to a 1.12.1 server; a VarInt from a 1.12.1 server must be zero-extended to i64 when forwarding to a 1.12.2 client. `Protocol1_12_1To1_12_2.java` does this with `map(Types.VAR_INT, Types.LONG)` (CB) and `map(Types.LONG, Types.VAR_INT)` (SB).
### Connecting ≥1.13 clients to 1.12.2 servers
The reverse direction (`v1_12_2to1_13` package) is the large one. 1.13 renamed every block, item, entity, and sound to namespaced strings; abolished numeric IDs for most registries; and overhauled chunk encoding. That is covered in `versions/1.13.md`.
+248
View File
@@ -0,0 +1,248 @@
# 1.13.x — Update Aquatic / **The Flattening**
**Protocols:** 393 (1.13), 401 (1.13.1), 404 (1.13.2)
**Release dates:** 1.13 — 2018-07-18 · 1.13.1 — 2018-08-22 · 1.13.2 — 2018-10-22
**Minecraft wiki release articles:**
- <https://minecraft.wiki/w/Java_Edition_1.13> (fetched 2026-06-19) — release date 2018-07-18, protocol 393, data version 1519, data/resource pack format 4
- <https://minecraft.wiki/w/Java_Edition_1.13.1> (fetched 2026-06-19) — release date 2018-08-22, protocol 401
- <https://minecraft.wiki/w/Java_Edition_1.13.2> (fetched 2026-06-19) — release date 2018-10-22, protocol 404
**Protocol version numbers confirmed:** `ProtocolVersion.java` lines 60-62 — `register(393, "1.13")`, `register(401, "1.13.1")`, `register(404, "1.13.2")``/tmp/mcproto-refs/ViaVersion/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java`; cross-checked against `/tmp/mcproto-refs/minecraft-data/data/pc/1.13/version.json` (393), `1.13.1/version.json` (401), `1.13.2/version.json` (404); confirmed on <https://minecraft.wiki/w/Protocol_version> (fetched 2026-06-19).
**ViaVersion packages:**
- `v1_12_2to1_13``/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_12_2to1_13/`
- `v1_13to1_13_1``…/protocols/v1_13to1_13_1/`
- `v1_13_1to1_13_2``…/protocols/v1_13_1to1_13_2/`
**minecraft-data sources:** `data/pc/1.12.2/`, `data/pc/1.13/`, `data/pc/1.13.1/`, `data/pc/1.13.2/``/tmp/mcproto-refs/minecraft-data/`
---
## Headline changes — The Flattening
1.13 ("Update Aquatic", 2018-07-18) is the most structurally invasive release of the modern protocol era, and not primarily because of the gameplay (oceans, conduits, trident, swimming). The protocol-shattering part is **The Flattening**: Mojang abolished the legacy `id:metadata` block encoding and the numeric-with-data item encoding that had been the wire format since Beta, replacing it with **flat block-state IDs** and **renumbered item IDs**, and made **namespaced string IDs** (`minecraft:stone`) the source of truth for blocks, items, entities, sounds, particles, biomes, enchantments, statistics and plugin channels.
Before 1.13, a block on the wire was a 12-bit block id plus a 4-bit metadata nibble (e.g. wool was id 35, colour in the data nibble). After 1.13 every distinct visual/logical state is its own integer in a single flat namespace — roughly **8000+ block states** — with no metadata channel at all. The `v1_12_2to1_13` mapping table that ViaVersion uses to translate them is sized for **8582** block-state keys (`blockconnections/ConnectionData.java:57`, `KEY_TO_ID = new Object2IntOpenHashMap<>(8582)`). Item numeric IDs were likewise renumbered into a new contiguous space (ViaVersion carries an explicit `1.12 → 1.13` item bi-mapping, `data/MappingData1_13.java:144-156`).
On top of the ID rewrite, 1.13 added four wire-level systems:
1. **Declare Commands / Brigadier** — server-driven command *tree* (graph of nodes) replaces the flat string-list tab-complete model. New clientbound `Declare Commands` packet; tab-complete request/response reworked to be transaction-based.
2. **Tags** — a new clientbound `Tags` packet broadcasting block/item/fluid tag groups (`#minecraft:logs`, `#minecraft:wool`, …), which commands and recipes reference by tag.
3. **Declare Recipes** — recipes moved server-side and are pushed to the client as a registry (replaces the recipe-book ID list approach of 1.12).
4. **Strict JSON chat + namespaced plugin channels** — chat components are now strict JSON, scoreboard objective/team text fields became chat components, and the legacy `MC|Brand`, `MC|StopSound`, `MC|TrList`, … plugin channels were renamed to namespaced `minecraft:*` channels.
Sources: <https://minecraft.wiki/w/Java_Edition_1.13> (fetched 2026-06-19, "added data packs", "added many commands and changed the format of existing commands"); ViaVersion `v1_12_2to1_13` (below). The dedicated wiki flattening sub-article (`/w/Java_Edition_1.13/flattening`) returned 404 on 2026-06-19; the ~8000 block-state figure here is grounded in ViaVersion's 8582-entry map rather than the wiki. <!-- VERIFY: exact published block-state count for 1.13 from a primary Mojang/wiki source -->
---
## Protocol changes vs 1.12.2 (393 vs 340), by state
Source: diff of `ClientboundPackets1_12_1`/`ServerboundPackets1_12_1` (used through 1.12.2) against `ClientboundPackets1_13`/`ServerboundPackets1_13``/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_12_2to1_13/packet/`; cross-confirmed by `minecraft-data` packet-name set diff (`data/pc/1.12.2` vs `data/pc/1.13`): **+6 clientbound, +10 serverbound** net-new play packets, **0 removed**.
minecraft-data diff output (play state):
- CB added: `declare_commands`, `tags`, `declare_recipes`, `nbt_query_response`, `face_player`, `stop_sound`
- SB added: `query_block_nbt`, `query_entity_nbt`, `edit_book`, `pick_item`, `name_item`, `select_trade`, `set_beacon_effect`, `update_command_block`, `update_command_block_minecart`, `update_structure_block`
(`stop_sound` was a real packet in 1.13; in 1.12.2 it was the `MC|StopSound` plugin-message, so it shows as "added" in the diff. Likewise several SB additions were previously `MC|...` plugin messages — see Plugin-channel migration below.)
### Handshake / Status / Login
- **Status:** unchanged on the wire, but the favicon string in the status JSON is now newline-sensitive; ViaVersion strips `\n` from `favicon` when downgrading a 1.13 status response (`Protocol1_12_2To1_13.java:182-198`).
- **Login:** 1.13 adds **Login Plugin Request (CB 0x04) / Login Plugin Response (SB 0x02)** — a login-state custom-query channel (this is the mechanism Velocity-modern forwarding later rides on). ViaVersion notes the new CB 0x04 (`Protocol1_12_2To1_13.java:201` comment) and `cancelServerbound(State.LOGIN, ServerboundLoginPackets.CUSTOM_QUERY_ANSWER.getId())` to drop the SB answer when bridging to a 1.12.2 server (`:536`).
### Play — clientbound, new packets (1.13 = protocol 393)
IDs are the `ClientboundPackets1_13` enum ordinals (`packet/ClientboundPackets1_13.java`):
| ID | Enum (ViaVersion) | minecraft-data | Purpose |
|---|---|---|---|
| `0x11` | `COMMANDS` | `declare_commands` | The Brigadier command graph (see Declare Commands below). |
| `0x1D` | `TAG_QUERY` | `nbt_query_response` | Response to a block/entity NBT query (debug "pick" / `data get`). |
| `0x31` | `PLAYER_LOOK_AT` | `face_player` | Server rotates the client to look at a point/entity. |
| `0x4C` | `STOP_SOUND` | `stop_sound` | Promoted from the old `MC|StopSound` plugin message to a real packet. |
| `0x54` | `UPDATE_RECIPES` | `declare_recipes` | Full recipe registry pushed to the client. |
| `0x55` | `UPDATE_TAGS` | `tags` | Block/item/fluid tag groups (see Tags below). |
The two terminal packets `UPDATE_RECIPES` (0x54) and `UPDATE_TAGS` (0x55) are the new high-ID additions; the Declare Commands packet `COMMANDS` slots in at 0x11, shifting every higher clientbound ID. Note `PLACE_GHOST_RECIPE` is at 0x2D in 1.13 (`ClientboundPackets1_13.java:69`).
Beyond the new packets, **many existing clientbound packets changed field types** because of the Flattening and strict-JSON chat — these don't change the packet *set* but are heavy translation work:
- `BLOCK_UPDATE`, `MULTI_BLOCK_CHANGE` (`CHUNK_BLOCKS_UPDATE`), `LEVEL_CHUNK`, `BLOCK_EVENT`, `EXPLODE` now carry **flat block-state IDs**, not id+data (`rewriter/WorldPacketRewriter1_13.java`).
- `SET_OBJECTIVE` (0x45) and `SET_PLAYER_TEAM` (0x47): objective value, team display name, prefix and suffix became **chat components** (`Protocol1_12_2To1_13.java:399-468`, `ComponentUtil.legacyToJson(...)`). Team colour is now an explicit VarInt enum (`:441-452`).
- `MAP_ITEM_DATA` icons gained a type field and optional display-name component (`:351-371`).
- `AWARD_STATS` statistics are now category-id + registry-id pairs instead of dotted strings (`:204-247`).
### Play — serverbound, new packets (1.13 = protocol 393)
IDs are the `ServerboundPackets1_13` enum ordinals (`packet/ServerboundPackets1_13.java`):
| ID | Enum (ViaVersion) | minecraft-data | Was (1.12.2) |
|---|---|---|---|
| `0x01` | `BLOCK_ENTITY_TAG_QUERY` | `query_block_nbt` | new |
| `0x0B` | `EDIT_BOOK` | `edit_book` | was `MC|BEdit`/`MC|BSign` plugin msg |
| `0x0C` | `ENTITY_TAG_QUERY` | `query_entity_nbt` | new |
| `0x15` | `PICK_ITEM` | `pick_item` | was `MC|PickItem` plugin msg |
| `0x1C` | `RENAME_ITEM` | `name_item` | was `MC|ItemName` plugin msg |
| `0x1F` | `SELECT_TRADE` | `select_trade` | was `MC|TrSel` plugin msg |
| `0x20` | `SET_BEACON` | `set_beacon_effect` | was `MC|Beacon` plugin msg |
| `0x22` | `SET_COMMAND_BLOCK` | `update_command_block` | was `MC|AutoCmd` plugin msg |
| `0x23` | `SET_COMMAND_MINECART` | `update_command_block_minecart` | was `MC|AdvCmd` plugin msg |
| `0x25` | `SET_STRUCTURE_BLOCK` | `update_structure_block` | was `MC|Struct` plugin msg |
This is the other half of the plugin-channel migration: ten interactions that were `MC|*` custom-payload messages in 1.12.2 became **dedicated serverbound packets** in 1.13. ViaVersion downgrades each by repackaging it back into the matching legacy `MC|*` channel — e.g. `SET_BEACON → "MC|Beacon"` (`Protocol1_12_2To1_13.java:657-665`), `SET_STRUCTURE_BLOCK → "MC|Struct"` (`:704-762`), `EDIT_BOOK → "MC|BEdit"/"MC|BSign"` (`:577-586`). The two NBT-query packets (`BLOCK_ENTITY_TAG_QUERY` 0x01, `ENTITY_TAG_QUERY` 0x0C) have no 1.12.2 equivalent and are simply cancelled when downgrading (`:539`, `:589`).
### Data-format changes (no packet add/remove, but wire-breaking)
- **Block states:** `id<<4 | metadata` → flat state integer. ViaVersion's `WorldPacketRewriter1_13.toNewId(int)` is the central mapping call (`WorldPacketRewriter1_13.java:566-586`).
- **Item IDs:** renumbered; `1.12 → 1.13` item bi-mapping (`MappingData1_13.java:144-156`). Spawn eggs collapsed from one item (numeric 383) + entity-id NBT to **per-entity flat items** (`ItemPacketRewriter1_13.java:395-401`, `:491-493`).
- **Chat:** strict JSON everywhere; legacy `§`-coded strings in objective/team/score fields must be converted to JSON components (`ComponentUtil.legacyToJson`, used throughout `Protocol1_12_2To1_13.java`).
- **Plugin channels:** legacy `MC|Brand`, `MC|StopSound`, `MC|TrList`, `MC|Register`/`UNREGISTER` etc. → namespaced `minecraft:*`; rename both directions with channel bi-map (`ItemPacketRewriter1_13.java:99-169`, `:199-229`; `MappingData1_13.java:84-95` loads `channelmappings-1.13.json`).
- **Particles:** numeric particle IDs renumbered, several merged/split; `ParticleIdMappings1_13.rewriteParticle` (`WorldPacketRewriter1_13.java:471-537`).
- **Biomes:** invalid biome ids now crash the 1.13 client, so ViaVersion clamps anything outside the valid set to plains (`WorldPacketRewriter1_13.java:56-76`, `:405-421`).
- **Sounds:** named-sound registry renamed/renumbered (`NamedSoundMappings1_13`, used at `WorldPacketRewriter1_13.java:317-327`).
---
## Declare Commands / Brigadier (the command tree)
Before 1.13 the client had no command model: it sent a string to a *Tab-Complete* packet and the server returned a flat list of completion strings. 1.13 replaces that with a **command graph** pushed once via the `Declare Commands` (`COMMANDS`, CB 0x11) packet, and reworks tab-complete into a transaction-id request/response.
**Packet structure** (per <https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Command_Data>, fetched 2026-06-19):
| Field | Type | Notes |
|---|---|---|
| Count | VarInt | number of nodes in the array |
| Nodes | Node[] | the graph, flat array; edges are integer indices |
| Root index | VarInt | index of the (nameless) root node |
Each **Node**:
| Field | Type | Present when |
|---|---|---|
| Flags | Byte | always |
| Children count | VarInt | always |
| Children | VarInt[] | always (indices into the node array) |
| Redirect node | VarInt | `flags & 0x08` |
| Name | String | literal & argument nodes |
| Parser | (see note) | argument nodes only |
| Properties | parser-specific | argument nodes only |
| Suggestions type | Identifier | `flags & 0x10` |
**Flags byte:** `0x03` = node type (`0` root, `1` literal, `2` argument), `0x04` executable, `0x08` has-redirect, `0x10` has-suggestions-type, `0x20` restricted (permission-gated) — wiki, fetched 2026-06-19.
> **Version-aware note on the Parser field.** The merged wiki page describes the *modern* format where the parser is a **VarInt parser-id**. In **1.13 specifically the parser is an Identifier (String)** such as `brigadier:string`, and the suggestions type is a String. This is confirmed by ViaVersion writing the parser as `Types.STRING, "brigadier:string"` and the suggestion provider as `Types.STRING, "minecraft:ask_server"` in its synthetic command tree (`Protocol1_12_2To1_13.java:140`, `:142`). The VarInt parser-id form arrived later (1.19+). <!-- VERIFY: exact protocol version at which Brigadier parser id switched String→VarInt -->
ViaVersion can't synthesise a real 1.12.2 server's command set as a tree, so when bridging a 1.13 client to a 1.12.2 server it sends a **minimal fake command graph** — a root node plus one greedy `brigadier:string` argument named `args` with suggestion provider `minecraft:ask_server` — so that tab-completion is delegated back to the server (`Protocol1_12_2To1_13.java:126-145`, `SEND_DECLARE_COMMANDS_AND_TAGS`). Tab-complete itself becomes transaction-based: the serverbound `COMMAND_SUGGESTION` carries a transaction id which ViaVersion tracks per-connection (`TabCompleteTracker`, `:542-575`), echoing it back on the clientbound `COMMAND_SUGGESTIONS` (`:250-281`).
```mermaid
flowchart TD
R["Root node (flags 0x00, nameless)"] -->|child| L["Literal 'msg'"]
R -->|child| L2["Literal 'me'"]
L --> A1["Argument 'target' (parser brigadier:entity)"]
A1 --> A2["Argument 'message' (parser brigadier:string, executable 0x04)"]
L2 --> A3["Argument 'action' (parser brigadier:string, executable 0x04)"]
A2 -.->|redirect 0x08| R
```
Mermaid is illustrative of the node/edge shape; the literal/argument names are examples, not a captured packet.
---
## Tags
1.13's `Tags` packet (`UPDATE_TAGS`, CB 0x55) ships **named groups of registry IDs** so the client can resolve `#minecraft:logs`, `#minecraft:wool`, fluid tags, etc. for command parsing and rendering. Wire layout (from ViaVersion's emitter, `Protocol1_12_2To1_13.java:148-167`):
| Field | Type | Notes |
|---|---|---|
| Block tag count | VarInt | |
| Block tags | (String name, VarInt[] ids)× | each tag = identifier + array of block IDs |
| Item tag count | VarInt | |
| Item tags | (String, VarInt[])× | |
| Fluid tag count | VarInt | |
| Fluid tags | (String, VarInt[])× | |
(1.13 has three tag registries — block, item, fluid. Entity-type tags were added in 1.14, and the packet later became a generic per-registry map.) ViaVersion loads the canonical 1.13 tag set from its mapping data (`MappingData1_13.loadExtras → loadTags`, `:62-64`) and emits the packet alongside the fake command graph in `SEND_DECLARE_COMMANDS_AND_TAGS` (`:147-173`). For 1.20.5+ clients it sends tags in the configuration phase rather than play, because registry data may depend on them (`:168-172`).
---
## Per-patch sub-sections
### 1.13 — protocol 393 (2018-07-18)
The Flattening release; everything above. **ViaVersion package:** `v1_12_2to1_13` — by far the largest single translation package in the codebase (block-connections sub-package, world sub-package, item rewriter, component rewriter, block-entity providers, and an 8582-entry block-state map).
**Key commits** (`git log --oneline -- common/src/main/java/com/viaversion/viaversion/protocols/v1_12_2to1_13`):
- `e965e9713` — Package/class renames and moves (the package itself)
- `501f65e21` — Packet and entity type renames (Mojang-mapped names)
- `8f8f5e72c` — Default rewriter registrations across protocols
- `896c6accf` — Fix 1.13 recipe ingredient writing
- `7c80e37e3` — Rewrite item ids in 1.12.2->1.13 show_item hover events (#4671)
- `f612adbd0` — Rewrite player & display name in player info packet in 1.12.2->1.13 (#4550)
- `50084c112` — Send the tags packet in 1.12->1.13 before the play login packet (tracking fix)
- `33aecef7b` — Handle custom name in block entities in 1.12->1.13 (#4232)
- `80e90a440` — Fix flower pot block storage 1.12.2->1.13 memory leak (#4852)
- `cab919bfa` — Fix legacy skull rotation overflow in 1.12.2->1.13 (#4952)
### 1.13.1 — protocol 401 (2018-08-22)
**Net packet change: 0 added, 0 removed.** A bug-fix patch; the protocol number bumped but no packet was added or deleted. The wire-visible deltas ViaVersion handles in `Protocol1_13To1_13_1.java`:
1. **Command-suggestion slash handling.** 1.13.0 had MC-123806 (tab-completion only working on the final argument); the fix changed where the leading `/` sits. Serverbound `COMMAND_SUGGESTION`: strip a leading `/` for compatibility (`Protocol1_13To1_13_1.java:61-73`). Clientbound `COMMAND_SUGGESTIONS`: offset the start index by `+1` to account for the `/` (`:93-111`).
2. **Boss bar flags fix (MC-123880).** 1.13.0 reused the same byte bit for two different booleans in the Boss Bar packet; 1.13.1 split them. ViaVersion's `BOSS_EVENT` handler propagates bit `0x02` into bit `0x04` when downgrading (`:113-131`).
3. **Edit Book off-hand fix (MC-84005).** `EDIT_BOOK` now carries a hand VarInt; ViaVersion cancels the packet when `hand == 1` (off-hand) since 1.13.0 servers don't expect it (`:75-91`).
4. Item-registry tag rewrite registered (`tagRewriter.register(... UPDATE_TAGS, RegistryType.ITEM)`, `:133`) — reflects minor item-tag additions in 1.13.1.
(Wiki notes the non-protocol changes too: `%=` scoreboard operator switched to `Math.floorMod`, function error line numbers start at 1 — <https://minecraft.wiki/w/Java_Edition_1.13.1>, fetched 2026-06-19.)
**ViaVersion package:** `v1_13to1_13_1` (note its `MappingDataBase("1.13", "1.13.2")` — it shares mapping data with the 1.13.2 step, `Protocol1_13To1_13_1.java:44`).
**Key commits** (`git log -- …/v1_13to1_13_1`): `e965e9713` (package creation), `501f65e21` (packet/entity renames), `c13b40a37` (Add ParticleRewriter base), `bb48dc90f` (registerBlockStateHandler).
### 1.13.2 — protocol 404 (2018-10-22)
**Net packet change: 0 added, 0 removed. One data-format change: the Slot format.** This is the only wire-visible change, and it is a clean before/after:
| | 1.13 / 1.13.1 (`ItemType1_13`) | 1.13.2 (`ItemType1_13_2`) |
|---|---|---|
| Empty marker | `Short id = -1` | `Boolean present = false` |
| Item id | `Short` | `VarInt` |
| Then | Byte count, NBT | Byte count, NBT |
So 1.13.2 (a) replaced the "id = 1 means empty" sentinel with an explicit leading **present boolean**, and (b) widened the item id from a **Short to a VarInt**. Confirmed by the two type classes: `ItemType1_13.read` reads `short id; if (id < 0) return null` (`api/.../type/types/item/ItemType1_13.java:38-46`) vs `ItemType1_13_2.read` reads `boolean present; if (!present) return null; … VAR_INT id` (`api/.../type/types/item/ItemType1_13_2.java:38-46`).
ViaVersion's `v1_13_1to1_13_2` package therefore does almost nothing except retype every Slot field on every item-bearing packet from `ITEM1_13 ↔ ITEM1_13_2`:
- `CONTAINER_SET_SLOT`, `CONTAINER_SET_CONTENT`, `SET_EQUIPPED_ITEM`, `UPDATE_RECIPES`, the `minecraft:trader_list` plugin payload — all `map(Types.ITEM1_13, Types.ITEM1_13_2)` clientbound (`rewriter/ItemPacketRewriter1_13_2.java:30-114`).
- `CONTAINER_CLICK`, `SET_CREATIVE_MODE_SLOT`, `EDIT_BOOK``map(Types.ITEM1_13_2, Types.ITEM1_13)` serverbound (`:116-133`; `Protocol1_13_1To1_13_2.java:42-47`).
- `UPDATE_ADVANCEMENTS` icon retyped `ITEM1_13 → ITEM1_13_2` (`Protocol1_13_1To1_13_2.java:49-79`).
- One area-effect-cloud item-particle fix (commit `ee16d7af2`).
**ViaVersion package:** `v1_13_1to1_13_2`.
**Key commits** (`git log -- …/v1_13_1to1_13_2`): `e965e9713` (package creation), `501f65e21` (renames), `ee16d7af2` (Fix two area effect cloud item particle issues), `5286efde1` (move type instances out of enclosing class).
---
## Proxy and translation impact
### Why `v1_12_2to1_13` is ViaVersion's largest translation class
The 1.12.2→1.13 step is the heaviest in ViaVersion for three compounding reasons, all stemming from the Flattening:
1. **The ~8000-entry block-state remap.** Every block on every chunk, block-update, multi-block-change, block-event and explode packet must be rewritten from legacy `id<<4|data` to a flat 1.13 state. The mapping table is sized for **8582** entries (`blockconnections/ConnectionData.java:57`). The chunk path rewrites every palette entry in every section via `toNewId` (`WorldPacketRewriter1_13.java:338-347`, `:566-586`).
2. **Block *connections* must be computed proxy-side.** Pre-1.13, the *appearance* of fences, walls, glass panes, stairs, redstone, doors, chests, tripwire, vines, chorus plants, fire, stems, etc. was derived client-side from neighbours; 1.13 bakes the connected variant into the block state, so a 1.13 client expects the server to have already chosen `oak_fence[north=true,east=true,...]`. A 1.12.2 server doesn't send that, so ViaVersion runs a full **server-side block-connection engine**: it stores chunk block data (`storage/BlockStorage.java`, `BlockConnectionStorage`), and on every chunk load / block change recomputes neighbour connections with **19 dedicated connection-handler classes** (`blockconnections/*ConnectionHandler.java` — Wall, Stair, Door, Chest, Fence, NetherFence, Glass, Pane, Redstone, Tripwire, Vine, ChorusPlant, Fire, Pumpkin, Melon, Snowy-grass, Flower, Stem, …). This is unique to this version step — no other ViaVersion package carries a world-geometry simulator. Gated behind `serverside-block-connections` config.
3. **Everything else got renumbered or restructured at once.** Item bi-map, spawn-egg split, particle remap, named-sound remap, biome clamp, statistics restructure, chat→JSON on scoreboard/team fields, plugin-channel namespacing in both directions, plus block-entity→block synthesis (note blocks and flower pots stopped being block-entities and became states, so ViaVersion deletes their NBT block-entities and bakes the state in, `WorldPacketRewriter1_13.java:423-452`). The package therefore needs sub-packages `blockconnections/`, `data/`, `provider/blockentities/`, `rewriter/`, `storage/`, `task/` — the only version step that needs all of them.
### What a proxy must do for a 1.13 client → 1.12.2 server
- **Translate every block state down** (flat → id+data) on all world packets, and **strip connection bits** that the old server never set.
- **Synthesise a fake Declare Commands graph + Tags packet** at join (so command UI and tag-referencing recipes don't break) — `SEND_DECLARE_COMMANDS_AND_TAGS`.
- **Repackage the 10 new serverbound packets** back into their legacy `MC|*` plugin messages; **cancel** the two NBT-query packets.
- **Down-convert chat JSON** on objective/team/score fields to legacy `§` strings; **rename plugin channels** `minecraft:*``MC|*`.
- **Map items down** (incl. spawn eggs, show_item hover events), **remap particles/sounds**, **clamp biomes** to avoid client crashes.
### Going the other way / between patches
- A 1.13.1 server vs 1.13.0 client (or vice-versa): only the command-suggestion `/` offset, boss-bar flag split, and edit-book hand need handling — trivial next to the 1.13 step.
- A 1.13.2 server vs 1.13/1.13.1 client: only the **Slot format** (`present` boolean + VarInt id ↔ Short-sentinel) needs retyping on item-bearing packets. A proxy that gets this wrong corrupts every inventory and creative action.
---
## Why this version matters as a baseline
1.13.2 (protocol 404) is the stable terminal of the Flattening line and a common multi-version proxy target: it is the last point before 1.14's further entity/tag/registry churn, and the first protocol where blocks, items, commands, tags and recipes all use the modern flat/namespaced model. Everything from 1.14 onward builds on the 1.13 data model rather than re-inventing it; the next equally-invasive jump is 1.20.2's Configuration state, not a data re-flattening.
+460
View File
@@ -0,0 +1,460 @@
# 1.14.x — Village & Pillage
**Protocols:** 477 (1.14), 480 (1.14.1), 485 (1.14.2), 490 (1.14.3), 498 (1.14.4)
**Release dates:** 1.14 — 2019-04-23 · 1.14.1 — 2019-05-13 · 1.14.2 — 2019-05-27 · 1.14.3 — 2019-06-24 · 1.14.4 — 2019-07-19
**Minecraft wiki release articles:**
- <https://minecraft.wiki/w/Java_Edition_1.14> (fetched 2026-06-19) — release date 2019-04-23, protocol 477, data version 1952, resource/data pack format 4
- <https://minecraft.wiki/w/Java_Edition_1.14.1> (fetched 2026-06-19) — release date 2019-05-13, protocol 480, 62 bug fixes
- <https://minecraft.wiki/w/Java_Edition_1.14.2> (fetched 2026-06-19) — release date 2019-05-27, protocol 485, lighting and chunk loading fixes
- <https://minecraft.wiki/w/Java_Edition_1.14.3> (fetched 2026-06-19) — release date 2019-06-24, protocol 490, villager/trade fixes
- <https://minecraft.wiki/w/Java_Edition_1.14.4> (fetched 2026-06-19) — release date 2019-07-19, protocol 498, performance improvements + BLOCK_BREAK_ACK
**Protocol version numbers confirmed:** `ProtocolVersion.java``register(477, "1.14")`, `register(480, "1.14.1")`, `register(485, "1.14.2")`, `register(490, "1.14.3")`, `register(498, "1.14.4")``/tmp/mcproto-refs/ViaVersion/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java`; cross-checked against `/tmp/mcproto-refs/minecraft-data/data/pc/1.14/`, `1.14.1/`, `1.14.3/`, `1.14.4/`.
**ViaVersion packages:**
- `v1_13_2to1_14``/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_13_2to1_14/`
- `v1_14to1_14_1``…/protocols/v1_14to1_14_1/`
- `v1_14_1to1_14_2``…/protocols/v1_14_1to1_14_2/`
- `v1_14_2to1_14_3``…/protocols/v1_14_2to1_14_3/`
- `v1_14_3to1_14_4``…/protocols/v1_14_3to1_14_4/`
**minecraft-data sources:** `data/pc/1.13.2/`, `data/pc/1.14/`, `data/pc/1.14.1/`, `data/pc/1.14.3/`, `data/pc/1.14.4/``/tmp/mcproto-refs/minecraft-data/`
---
## Headline changes — Village & Pillage
1.14 (2019-04-23) carries the largest single-version protocol change-set since 1.13's Flattening. Gameplay is dominated by the villager/trading rework and new Village & Pillage mobs (pillager, ravager, wandering trader, fox, panda), but the protocol impact is equally substantial:
1. **Chunk lighting separated.** In 1.13 chunk light data traveled inside the `Chunk Data` packet. In 1.14 light is pulled into its own `Update Light` packet (0x24), sent *before* or *alongside* `Level Chunk` (0x21). The chunk packet itself now carries an NBT **heightmap** compound (keys `MOTION_BLOCKING` and `WORLD_SURFACE`, each a 9-bit-per-cell long-array) that was absent in 1.13. Source: `WorldPacketRewriter1_14.java` (full chunk handler and synthetic `LIGHT_UPDATE` injection) and `minecraft-data` `data/pc/1.14/protocol.json` field `heightmaps`.
2. **Block position bit-field reordered.** The 64-bit packed `Position` type reshuffled its bit layout. In 1.13.2 the 64 bits were `[x:26][y:12][z:26]` (Y in the middle). In 1.14 the layout is `[x:26][z:26][y:12]` (Y moved to the low bits). Source: `minecraft-data` top-level `types.position` in `1.13.2/protocol.json` vs `1.14/protocol.json`. ViaVersion remaps every position-carrying packet accordingly — `WorldPacketRewriter1_14.java` maps `Types.BLOCK_POSITION1_8``Types.BLOCK_POSITION1_14` on BLOCK_DESTRUCTION, BLOCK_ENTITY_DATA, BLOCK_EVENT, BLOCK_UPDATE, SET_DEFAULT_SPAWN_POSITION, and OPEN_SIGN_EDITOR.
3. **Villager trade list reworked.** The old 1.13 `bed` packet (which handled the player-sleeping animation) is gone; sleeping is now handled via entity data. Replacing it (and the old numeric-only trade list from 1.13's `MC|TrList` plugin channel) is the new `Merchant Offers` / `trade_list` packet (0x27). It carries per-trade fields: two input slots, one output slot, disabled flag, trade-use counters (`nbTradeUses`/`maximumNbTradeUses`), XP grant, special price, price multiplier, villager level, total experience, and `isRegularVillager` flag. Source: `minecraft-data` `data/pc/1.14/protocol.json` `packet_trade_list`; `ClientboundPackets1_14.java:63` (MERCHANT_OFFERS 0x27).
4. **New/removed packets.** Seven new clientbound Play packets, three new serverbound Play packets, one removed clientbound:
| Dir | 0x ID | Name | Status |
|-----|-------|------|--------|
| CB | 0x24 | `update_light` (LIGHT_UPDATE) | **NEW** |
| CB | 0x27 | `trade_list` (MERCHANT_OFFERS) | **NEW** |
| CB | 0x2D | `open_book` (OPEN_BOOK) | **NEW** |
| CB | 0x1F | `open_horse_window` (HORSE_SCREEN_OPEN) | **NEW** (split from OPEN_SCREEN) |
| CB | 0x40 | `update_view_position` (SET_CHUNK_CACHE_CENTER) | **NEW** |
| CB | 0x41 | `update_view_distance` (SET_CHUNK_CACHE_RADIUS) | **NEW** |
| CB | 0x50 | `entity_sound_effect` (SOUND_ENTITY) | **NEW** |
| CB | — | `bed` | **REMOVED** (→ entity data) |
| SB | 0x02 | `set_difficulty` (CHANGE_DIFFICULTY) | **NEW** |
| SB | 0x10 | `lock_difficulty` (LOCK_DIFFICULTY) | **NEW** |
| SB | 0x27 | `update_jigsaw_block` (SET_JIGSAW_BLOCK) | **NEW** |
Source: diff of `ClientboundPackets1_14.java` vs `ClientboundPackets1_13.java`; `ServerboundPackets1_14.java`; `minecraft-data` play packet maps for 1.13.2 and 1.14.
5. **Difficulty decoupled from Join Game / Respawn.** In 1.13.2 difficulty was a field inside `Login` (Join Game) and `Respawn` packets. In 1.14 it is stripped from both and sent as a separate `Change Difficulty` packet (0x0D, existing slot) which gains a new boolean `difficultyLocked` field. ViaVersion synthesizes this packet when downgrading: `WorldPacketRewriter1_14.java` in the LOGIN and RESPAWN handlers each read the old difficulty byte and create a synthetic `CHANGE_DIFFICULTY` packet with `difficultyLocked=false`. Source: `WorldPacketRewriter1_14.java`; `minecraft-data` `packet_difficulty` type (1.14 adds `difficultyLocked: bool`); `PlayerPacketRewriter1_14.java` comments ("Added in 19w11a").
6. **Login (Join Game) gains viewDistance.** The 1.14 `login` packet has a new `viewDistance: varint` field after `levelType`. Source: `minecraft-data` `data/pc/1.14/protocol.json` `packet_login`; `WorldPacketRewriter1_14.java` line `wrapper.write(Types.VAR_INT, WorldPacketRewriter1_14.SERVERSIDE_VIEW_DISTANCE)`.
7. **Map locked flag.** The `Map Item Data` packet gains a `locked: boolean` field (added in 19w02a, defaults false for old maps). Source: `WorldPacketRewriter1_14.java` MAP_ITEM_DATA handler.
8. **Massive packet ID renumbering.** With 7 new clientbound packets inserted throughout the ID space, virtually every Play clientbound packet above 0x13 has a different ID in 1.14 vs 1.13.2. Proxies and ViaVersion must remap all IDs. Notable shifts include `open_window` moving from 0x14 to 0x2E, `nbt_query_response` from 0x1D to 0x54 (jumping 55 slots), `sound_effect` from 0x4D to 0x51. Source: `minecraft-data` ID maps for 1.13.2 and 1.14 (full shift list available above in research data).
9. **Entity data: new Pose type, villager data restructure, new entities.** A new entity data type `Pose` (varint enum: Standing=0, FallFlying=1, Sleeping=2, Swimming=3, SpinAttack=4, Sneaking=5) replaces the old per-entity boolean flags for crouching/swimming/sleeping. Villager entity data (index 15) changes from a single profession integer to a compound `VillagerData` struct (villager type, profession, level). New entities added to the registry: Cat (split from Ocelot), Pillager, Ravager, Wandering Trader, Trader Llama, Fox. `PLAYER_SLEEP` clientbound packet is eliminated; sleeping is now communicated via entity metadata index 12 (`optionalBlockPosition`). Source: `EntityPacketRewriter1_14.java` (registerRewrites — filter for ENTITY, LIVING_ENTITY pose index, VILLAGER index 15 → VillagerData, OCELOT removals, ABSTRACT_RAIDER celebrating index); `EntityTypes1_14`.
10. **`nbt:compound_tag` argument type rename.** In command trees (`Declare Commands` / COMMANDS packet), the argument type `minecraft:nbt` was renamed to `minecraft:nbt_compound_tag`. Source: `Protocol1_13_2To1_14.java` CommandRewriter override of `handleArgumentType`.
11. **Fluid tags sent in Update Tags.** The `Update Tags` packet gains a fourth section for fluid tags (in addition to block/item/entity tags). Source: `Protocol1_13_2To1_14.java` tagRewriter handler; VV adds fluid tag registry type and appends entity tags.
---
## Play state — clientbound packet table (1.14, protocol 477)
Full list from `minecraft-data` `data/pc/1.14/protocol.json` `play.toClient` and `ClientboundPackets1_14.java`:
| ID | Name (minecraft-data) | ViaVersion name |
|----|----------------------|-----------------|
| 0x00 | spawn_entity | ADD_ENTITY |
| 0x01 | spawn_entity_experience_orb | ADD_EXPERIENCE_ORB |
| 0x02 | spawn_entity_weather | ADD_GLOBAL_ENTITY |
| 0x03 | spawn_entity_living | ADD_MOB |
| 0x04 | spawn_entity_painting | ADD_PAINTING |
| 0x05 | named_entity_spawn | ADD_PLAYER |
| 0x06 | animation | ANIMATE |
| 0x07 | statistics | AWARD_STATS |
| 0x08 | block_break_animation | BLOCK_DESTRUCTION |
| 0x09 | tile_entity_data | BLOCK_ENTITY_DATA |
| 0x0A | block_action | BLOCK_EVENT |
| 0x0B | block_change | BLOCK_UPDATE |
| 0x0C | boss_bar | BOSS_EVENT |
| 0x0D | difficulty | CHANGE_DIFFICULTY |
| 0x0E | chat | CHAT |
| 0x0F | multi_block_change | CHUNK_BLOCKS_UPDATE |
| 0x10 | tab_complete | COMMAND_SUGGESTIONS |
| 0x11 | declare_commands | COMMANDS |
| 0x12 | transaction | CONTAINER_ACK |
| 0x13 | close_window | CONTAINER_CLOSE |
| 0x14 | window_items | CONTAINER_SET_CONTENT |
| 0x15 | craft_progress_bar | CONTAINER_SET_DATA |
| 0x16 | set_slot | CONTAINER_SET_SLOT |
| 0x17 | set_cooldown | COOLDOWN |
| 0x18 | custom_payload | CUSTOM_PAYLOAD |
| 0x19 | named_sound_effect | CUSTOM_SOUND |
| 0x1A | kick_disconnect | DISCONNECT |
| 0x1B | entity_status | ENTITY_EVENT |
| 0x1C | explosion | EXPLODE |
| 0x1D | unload_chunk | FORGET_LEVEL_CHUNK |
| 0x1E | game_state_change | GAME_EVENT |
| 0x1F | open_horse_window (**NEW**) | HORSE_SCREEN_OPEN |
| 0x20 | keep_alive | KEEP_ALIVE |
| 0x21 | map_chunk | LEVEL_CHUNK |
| 0x22 | world_event | LEVEL_EVENT |
| 0x23 | world_particles | LEVEL_PARTICLES |
| 0x24 | update_light (**NEW**) | LIGHT_UPDATE |
| 0x25 | login | LOGIN |
| 0x26 | map | MAP_ITEM_DATA |
| 0x27 | trade_list (**NEW**) | MERCHANT_OFFERS |
| 0x28 | rel_entity_move | MOVE_ENTITY_POS |
| 0x29 | entity_move_look | MOVE_ENTITY_POS_ROT |
| 0x2A | entity_look | MOVE_ENTITY_ROT |
| 0x2B | entity | MOVE_ENTITY |
| 0x2C | vehicle_move | MOVE_VEHICLE |
| 0x2D | open_book (**NEW**) | OPEN_BOOK |
| 0x2E | open_window | OPEN_SCREEN |
| 0x2F | open_sign_entity | OPEN_SIGN_EDITOR |
| 0x30 | craft_recipe_response | PLACE_GHOST_RECIPE |
| 0x31 | abilities | PLAYER_ABILITIES |
| 0x32 | combat_event | PLAYER_COMBAT |
| 0x33 | player_info | PLAYER_INFO |
| 0x34 | face_player | PLAYER_LOOK_AT |
| 0x35 | position | PLAYER_POSITION |
| 0x36 | unlock_recipes | RECIPE |
| 0x37 | entity_destroy | REMOVE_ENTITIES |
| 0x38 | remove_entity_effect | REMOVE_MOB_EFFECT |
| 0x39 | resource_pack_send | RESOURCE_PACK |
| 0x3A | respawn | RESPAWN |
| 0x3B | entity_head_rotation | ROTATE_HEAD |
| 0x3C | select_advancement_tab | SELECT_ADVANCEMENTS_TAB |
| 0x3D | world_border | SET_BORDER |
| 0x3E | camera | SET_CAMERA |
| 0x3F | held_item_slot | SET_CARRIED_ITEM |
| 0x40 | update_view_position (**NEW**) | SET_CHUNK_CACHE_CENTER |
| 0x41 | update_view_distance (**NEW**) | SET_CHUNK_CACHE_RADIUS |
| 0x42 | scoreboard_display_objective | SET_DISPLAY_OBJECTIVE |
| 0x43 | entity_metadata | SET_ENTITY_DATA |
| 0x44 | attach_entity | SET_ENTITY_LINK |
| 0x45 | entity_velocity | SET_ENTITY_MOTION |
| 0x46 | entity_equipment | SET_EQUIPPED_ITEM |
| 0x47 | experience | SET_EXPERIENCE |
| 0x48 | update_health | SET_HEALTH |
| 0x49 | scoreboard_objective | SET_OBJECTIVE |
| 0x4A | set_passengers | SET_PASSENGERS |
| 0x4B | teams | SET_PLAYER_TEAM |
| 0x4C | scoreboard_score | SET_SCORE |
| 0x4D | spawn_position | SET_DEFAULT_SPAWN_POSITION |
| 0x4E | update_time | SET_TIME |
| 0x4F | title | SET_TITLES |
| 0x50 | entity_sound_effect (**NEW**) | SOUND_ENTITY |
| 0x51 | sound_effect | SOUND |
| 0x52 | stop_sound | STOP_SOUND |
| 0x53 | playerlist_header | TAB_LIST |
| 0x54 | nbt_query_response | TAG_QUERY |
| 0x55 | collect | TAKE_ITEM_ENTITY |
| 0x56 | entity_teleport | TELEPORT_ENTITY |
| 0x57 | advancements | UPDATE_ADVANCEMENTS |
| 0x58 | entity_update_attributes | UPDATE_ATTRIBUTES |
| 0x59 | entity_effect | UPDATE_MOB_EFFECT |
| 0x5A | declare_recipes | UPDATE_RECIPES |
| 0x5B | tags | UPDATE_TAGS |
Removed vs 1.13.2: `bed` (0x33 in 1.13.2) — replaced by entity data.
---
## Play state — serverbound packet table (1.14, protocol 477)
From `ServerboundPackets1_14.java` and `minecraft-data` `data/pc/1.14/protocol.json` `play.toServer`:
| ID | Name | Notes |
|----|------|-------|
| 0x00 | teleport_confirm | unchanged |
| 0x01 | query_block_nbt | unchanged (BLOCK_ENTITY_TAG_QUERY) |
| 0x02 | set_difficulty | **NEW** — CHANGE_DIFFICULTY |
| 0x03 | chat | unchanged |
| 0x04 | client_command | unchanged (CLIENT_COMMAND) |
| 0x05 | settings | unchanged (CLIENT_INFORMATION) |
| 0x06 | tab_complete | unchanged (COMMAND_SUGGESTION) |
| 0x07 | transaction | unchanged (CONTAINER_ACK) |
| 0x08 | enchant_item | unchanged (CONTAINER_BUTTON_CLICK) |
| 0x09 | window_click | unchanged (CONTAINER_CLICK) |
| 0x0A | close_window | unchanged (CONTAINER_CLOSE) |
| 0x0B | custom_payload | unchanged |
| 0x0C | edit_book | unchanged |
| 0x0D | query_entity_nbt | unchanged (ENTITY_TAG_QUERY) |
| 0x0E | use_entity | unchanged (INTERACT) |
| 0x0F | keep_alive | unchanged |
| 0x10 | lock_difficulty | **NEW** — LOCK_DIFFICULTY |
| 0x110x1C | movement packets | unchanged |
| 0x1D | crafting_book_data | unchanged (RECIPE_BOOK_UPDATE) |
| 0x1E | name_item | unchanged (RENAME_ITEM) |
| 0x1F | resource_pack_receive | unchanged (RESOURCE_PACK) |
| 0x20 | advancement_tab | unchanged (SEEN_ADVANCEMENTS) |
| 0x21 | select_trade | unchanged (SELECT_TRADE) |
| 0x22 | set_beacon_effect | unchanged (SET_BEACON) |
| 0x23 | held_item_slot | unchanged (SET_CARRIED_ITEM) |
| 0x24 | update_command_block | unchanged (SET_COMMAND_BLOCK) |
| 0x25 | update_command_block_minecart | unchanged (SET_COMMAND_MINECART) |
| 0x26 | set_creative_slot | unchanged (SET_CREATIVE_MODE_SLOT) |
| 0x27 | update_jigsaw_block | **NEW** — SET_JIGSAW_BLOCK |
| 0x28 | update_structure_block | unchanged (SET_STRUCTURE_BLOCK) |
| 0x29 | update_sign | unchanged (SIGN_UPDATE) |
| 0x2A | arm_animation | unchanged (SWING) |
| 0x2B | spectate | unchanged (TELEPORT_TO_ENTITY) |
| 0x2C | block_place | unchanged (USE_ITEM_ON) |
| 0x2D | use_item | unchanged |
ViaVersion cancels `CHANGE_DIFFICULTY` (0x02) and `LOCK_DIFFICULTY` (0x10) when downgrading to 1.13.2 (server doesn't understand them); also cancels `SET_JIGSAW_BLOCK` (0x27). Source: `Protocol1_13_2To1_14.java` lines 92-96.
---
## Packet format details — new packets
### Update Light (CB 0x24)
Introduced to decouple lighting from chunk data. Sent before or alongside Level Chunk for full chunk loads.
```
VarInt chunkX
VarInt chunkZ
VarInt skyLightMask // bitmask of sections with sky light (bits 0..17, sections -1..16)
VarInt blockLightMask // bitmask of sections with block light
VarInt emptySkyLightMask // sections where sky light should be set to all-zeros
VarInt emptyBlockLightMask
// Followed by skyLight arrays for each set bit in skyLightMask (2048 bytes each)
// Then blockLight arrays for each set bit in blockLightMask (2048 bytes each)
```
Source: `minecraft-data` `data/pc/1.14/protocol.json` `packet_update_light`; `WorldPacketRewriter1_14.java` LEVEL_CHUNK handler (synthetic `LIGHT_UPDATE` construction — 18-bit mask for 18 sections including sub-zero and above-255 virtual sections).
### Level Chunk (CB 0x21) — format change
In 1.13.2 the chunk packet carried light nibble arrays inside each ChunkSection. In 1.14 the light is removed from chunk sections and the packet gains an NBT `heightmaps` compound:
```
// 1.13.2 map_chunk:
Int32 x, z
Boolean groundUp
VarInt bitMap
Buffer chunkData // sections include sky/block light nibbles
NBT[] blockEntities
// 1.14 map_chunk:
Int32 x, z
Boolean groundUp
VarInt bitMap
NBT heightmaps // NEW: compound with MOTION_BLOCKING and WORLD_SURFACE long-arrays
Buffer chunkData // sections no longer carry light
NBT[] blockEntities
```
Source: `minecraft-data` `data/pc/1.13.2/protocol.json` vs `data/pc/1.14/protocol.json` `packet_map_chunk`; `WorldPacketRewriter1_14.java` (builds `heightmaps` CompoundTag with 9-bit compact long-arrays via `CompactArrayUtil.createCompactArray(9, 256, …)` and strips `section.setLight(null)`).
### Merchant Offers (CB 0x27) — new trade list
```
VarInt windowId
u8 count
[count] Trade:
Slot inputItem1
Slot outputItem
Boolean hasInputItem2
Slot? inputItem2 // present if hasInputItem2
Boolean tradeDisabled
Int32 nbTradeUses
Int32 maximumNbTradeUses
Int32 xp // XP granted to villager per trade
Int32 specialPrice // price adjustment (negative = discount)
Float32 priceMultiplier // multiplier for demand/reputation adjustment
VarInt villagerLevel
VarInt experience
Boolean isRegularVillager
```
Source: `minecraft-data` `data/pc/1.14/protocol.json` `packet_trade_list`.
### Update View Position (CB 0x40) and Update View Distance (CB 0x41) — new view management
```
// Update View Position (SET_CHUNK_CACHE_CENTER):
VarInt chunkX
VarInt chunkZ
// Update View Distance (SET_CHUNK_CACHE_RADIUS):
VarInt viewDistance
```
Both are new in 1.14 (added in 19w13a snapshots). ViaVersion synthesizes Update View Position from chunk coordinates when downgrading, and sends a fixed `SERVERSIDE_VIEW_DISTANCE = 64` for Update View Distance. Source: `WorldPacketRewriter1_14.java` `sendViewDistancePacket()` and chunk handler.
### Open Book (CB 0x2D) — new
```
VarInt hand // 0 = main hand, 1 = off hand
```
Source: `minecraft-data` `data/pc/1.14/protocol.json` `packet_open_book`.
### Open Horse Window (CB 0x1F) — split from Open Screen
Previously horse inventory was opened via the generic `Open Screen` packet with type `"EntityHorse"`. In 1.14 it has its own packet:
```
u8 windowId
VarInt nbSlots
Int32 entityId
```
ViaVersion converts the old `OPEN_SCREEN` with type `EntityHorse` into the new `HORSE_SCREEN_OPEN` packet. Source: `ItemPacketRewriter1_14.java` OPEN_SCREEN handler.
---
## Per-patch sub-sections
### Protocol 477 — 1.14 (2019-04-23)
Base release. All changes described above vs 1.13.2. Key ViaVersion package: `v1_13_2to1_14`.
Git log (top commits affecting `protocols/v1_13_2to1_14/`):
```
2e91b841b Automatically call mapTypes in entity rewriter
0121534e7 Deduplicate particle type fillers
8f8f5e72c Default rewriter registrations across protocols
a0b0ed4b6 Make <1.21.2 container id types consistent with Vanilla
c13b40a37 Add ParticleRewriter base
e436bbe37 Refactor dimension switch handling across all protocols
463381b84 Rename missing metadata references to entity data
```
Source: `git -C /tmp/mcproto-refs/ViaVersion log --oneline -- common/src/main/java/com/viaversion/viaversion/protocols/v1_13_2to1_14/` — most recent commits are refactors; the original 1.14 implementation predates the VV shallow-clone window.
Notable implementation notes from ViaVersion source:
- **Particle rewriter**: The 1.14 particle type encoding changes slightly; ViaVersion uses `ParticleType.Fillers.fill1_13_2(this, Types1_14.PARTICLE, true)` to remap particle IDs. Source: `Protocol1_13_2To1_14.java:106-107`.
- **Explosion negative-coordinate fix**: When downgrading 1.14→1.13.2, negative explosion coordinates are truncated to integer (a Mojang bug workaround). Source: `WorldPacketRewriter1_14.java` EXPLODE handler.
- **Health NaN fix**: The 1.14 health entity data can send `NaN` for living entities; ViaVersion optionally clamps to 1.0F. Source: `EntityPacketRewriter1_14.java` LIVING_ENTITY index 8 handler.
- **Arrow velocity separate packet**: In 1.14, velocity in `ADD_ENTITY` (Spawn Object) is ignored for items and arrows; ViaVersion emits a synthetic `SET_ENTITY_MOTION` packet. Source: `EntityPacketRewriter1_14.java` ADD_ENTITY handler.
---
### Protocol 480 — 1.14.1 (2019-05-13)
**What changed:** Villager entity data gained a new index. No packet added or removed — the packet set is identical to 477 (`Protocol1_14To1_14_1` maps `ClientboundPackets1_14 → ClientboundPackets1_14` with no new packet enum). Primary protocol-level change: entity data index 15 added to **both Villager and Wandering Trader** entity types.
From ViaVersion `v1_14to1_14_1`:
- `EntityPacketRewriter1_14_1.registerRewrites()`: `filter().type(EntityTypes1_14.VILLAGER).addIndex(15)` and `filter().type(EntityTypes1_14.WANDERING_TRADER).addIndex(15)` — an extra entity data slot inserted at index 15 for both entity types. Source: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_14to1_14_1/rewriter/EntityPacketRewriter1_14_1.java:79-80`.
- The `ADD_MOB` and `ADD_PLAYER` packets are registered to handle entity tracking but no field changes exist.
The `trade_list` (`MERCHANT_OFFERS`) packet is identical between 1.14 and 1.14.1 (confirmed: `minecraft-data` structures match exactly). Source: `data/pc/1.14/protocol.json` vs `data/pc/1.14.1/protocol.json` `packet_trade_list` — identical.
**Gameplay context:** 62 bug fixes, villager pathfinding/AI improvements, performance work. No new blocks or entities.
Git log for `v1_14to1_14_1/`: commits are all ViaVersion internal refactors (`bd4df2813`, `e965e9713`, `501f65e21`, `5286efde1`, `75d86851c`) — renaming, reformatting, no protocol logic changes.
---
### Protocol 485 — 1.14.2 (2019-05-27)
**What changed:** Version bump only at the protocol level; no packet structure changes vs 1.14.1. The `Protocol1_14_1To1_14_2` class is an empty `AbstractProtocol` subclass with no registered packet handlers. Source: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_14_1to1_14_2/Protocol1_14_1To1_14_2.java` — the entire class body has no `registerPackets()` override.
**Confirmed:** `minecraft-data` has no `data/pc/1.14.2/` directory (skipped — implies identical packet structure to 1.14.1).
**Gameplay context:** Lighting re-calculation when opening old worlds for the first time; debug screen shows server-side chunk counts; raiders restricted to fully-loaded chunks; 50+ bug fixes. No packet-level protocol change beyond the version number.
Git log for `v1_14_1to1_14_2/`: only cosmetic/rename commits (`cff9a8715`, `9f6e7fa4e`, `e965e9713`).
---
### Protocol 490 — 1.14.3 (2019-06-24)
**What changed:** One `MERCHANT_OFFERS` (`trade_list`) packet field added: `canRestock: boolean` appended to the packet's trailing fields (after `isRegularVillager`). ViaVersion inserts a synthetic `canRestock=isRegularVillager` when translating down from 1.14.3 to 1.14.2.
From `Protocol1_14_2To1_14_3.registerPackets()`:
```java
registerClientbound(ClientboundPackets1_14.MERCHANT_OFFERS, wrapper -> {
// … passthrough all trade fields …
boolean regularVillager = wrapper.passthrough(Types.BOOLEAN);
wrapper.write(Types.BOOLEAN, regularVillager); // new boolean added in pre-1
});
```
Source: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_14_2to1_14_3/Protocol1_14_2To1_14_3.java:33-53`.
Confirmed by `minecraft-data`: `data/pc/1.14.3/protocol.json` `packet_trade_list` has `canRestock: bool` as the last field; `data/pc/1.14/protocol.json` and `data/pc/1.14.1/protocol.json` do not.
**Gameplay context:** New gamerule `disableRaids`; protection enchantments mutually exclusive again; lantern placement on iron bars; item repair-by-crafting restored; villager panic behavior adjusted; 82 bug fixes.
Git log for `v1_14_2to1_14_3/`: internal refactors only — no protocol logic commits unique to this package.
---
### Protocol 498 — 1.14.4 (2019-07-19)
**What changed:** One new clientbound Play packet added: `acknowledge_player_digging` (`BLOCK_BREAK_ACK`) at ID **0x5C**.
```
// Acknowledge Player Digging (CB 0x5C):
Position location // block position (new 1.14 format: [x:26][z:26][y:12])
VarInt block // block state ID at that location
VarInt status // dig status: 0=started, 1=cancelled, 2=finished
Boolean successful // whether the dig was acknowledged as successful
```
Source: `minecraft-data` `data/pc/1.14.4/protocol.json` `packet_acknowledge_player_digging`; `ClientboundPackets1_14_4.java:116``BLOCK_BREAK_ACK; // 0x5C` is the only addition vs `ClientboundPackets1_14.java`.
ViaVersion `Protocol1_14_3To1_14_4` translates the `MERCHANT_OFFERS` packet to inject a `demand: i32` field (set to 0) into each trade entry:
```java
registerClientbound(ClientboundPackets1_14.MERCHANT_OFFERS, wrapper -> {
// … passthrough all existing fields …
wrapper.write(Types.INT, 0); // demand value added in pre5
});
```
Source: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_14_3to1_14_4/Protocol1_14_3To1_14_4.java:34-51`.
Confirmed by `minecraft-data`: `data/pc/1.14.4/protocol.json` `packet_trade_list` each trade entry has `demand: i32` after `priceMultiplier`; `data/pc/1.14.3/protocol.json` does not.
**Total 1.14.4 MERCHANT_OFFERS trade entry fields** (per-trade): `inputItem1`, `outputItem`, `inputItem2?`, `tradeDisabled`, `nbTradeUses`, `maximumNbTradeUses`, `xp`, `specialPrice`, `priceMultiplier`, **`demand`** → then packet-level: `villagerLevel`, `experience`, `isRegularVillager`, `canRestock`.
**Gameplay context:** Performance improvements (chunk loading at high speeds, leaf rendering); camera first-person pivot fix; `/reload` and `/forceload` accessible at permission level 2; villager behavior improvements; 54 bug fixes.
Git log for `v1_14_3to1_14_4/`: internal refactors only (`cff9a8715`, `9f6e7fa4e`, `75d86851c`, `5286efde1`, `501f65e21`, `e965e9713`); the functional `MERCHANT_OFFERS` and `BLOCK_BREAK_ACK` logic predates the shallow-clone window.
---
## Proxy and translation impact
**Version-negotiation:** Five distinct protocol numbers in a single minor release line (477/480/485/490/498) is unusual. A proxy supporting 1.14.x must negotiate all five and route each to the appropriate ViaVersion translation chain.
**Light packet ordering:** When a 1.14+ client connects to a 1.13.2 server via ViaVersion, the proxy must synthesize the `Update Light` packet from light data embedded in the chunk packet, and send it to the client before or alongside the `Level Chunk` packet. The ViaVersion `WorldPacketRewriter1_14` does this by intercepting the 1.13 `LEVEL_CHUNK` packet, stripping light from each section (`section.setLight(null)` after building the synthetic light packet), and sending the light packet immediately before the chunk. Source: `WorldPacketRewriter1_14.java` LEVEL_CHUNK handler.
**Block position format:** Any proxy bridging 1.13.2 ↔ 1.14+ must reorder every `Position` field in every packet that carries one. The bit layout changed from `[x:26][y:12][z:26]` (1.13.2) to `[x:26][z:26][y:12]` (1.14+). Failure to remap results in silently wrong Y coordinates (shifted 14 bits left, producing wildly out-of-range block Y values). ViaVersion applies `BLOCK_POSITION1_8 → BLOCK_POSITION1_14` on every positional packet. Source: `WorldPacketRewriter1_14.java`, `PlayerPacketRewriter1_14.java`.
**Chunk center tracking:** 1.14 clients require `Update View Position` (SET_CHUNK_CACHE_CENTER) to know which chunk column they are centered on; without it the client shows a rendering artifact with nearby chunks not loading. ViaVersion emits a synthetic SET_CHUNK_CACHE_CENTER whenever a chunk arrives that is more than `SERVERSIDE_VIEW_DISTANCE` (64) columns from the last known center. Source: `WorldPacketRewriter1_14.java` chunk handler `entityTracker.isForceSendCenterChunk()` check.
**MERCHANT_OFFERS evolution across patches:** The trade list packet changed three times across the 1.14.x line. A proxy bridging between 1.14.x sub-versions must account for each field addition in sequence:
| Protocol | MERCHANT_OFFERS field added |
|----------|-----------------------------|
| 477 | Base: inputItem1/outputItem/inputItem2/tradeDisabled/nbTradeUses/maximumNbTradeUses/xp/specialPrice/priceMultiplier/villagerLevel/experience/isRegularVillager |
| 480 | No change to packet (entity data index 15 change for villager/wandering-trader) |
| 485 | No change |
| 490 | `canRestock: bool` appended after isRegularVillager |
| 498 | `demand: i32` appended inside each trade entry after priceMultiplier |
**Entity data: villager profession mapping.** When downgrading villager entity data from 1.14 to 1.13.2, ViaVersion must map the new `VillagerData` struct (type, profession, level) back to the old single-integer profession. The mapping is: farmer→5, librarian→9, cleric→4 (priest), armorer→1 (blacksmith), butcher→2, nitwit→11, none→0. This is lossy (the new villager type and level fields have no 1.13.2 equivalent). Source: `EntityPacketRewriter1_14.java` `getNewProfessionId()`.
**Ocelot/Cat split.** 1.14 splits the Ocelot into Ocelot and Cat as separate entity types with different IDs. When downgrading from 1.14 a proxy must either map Cat back to Ocelot (ViaVersion default, with config option `translateOcelotToCat`) or spawn a pig as fallback. Source: `EntityPacketRewriter1_14.java` `onMappingDataLoaded()` and the 1.13→1.14 entity type registry.
**BLOCK_BREAK_ACK (1.14.4 only):** The new 0x5C packet in protocol 498 has no counterpart in 497 and below. A proxy must suppress or synthesize it appropriately. ViaVersion's `Protocol1_14_3To1_14_4` registers a new `ClientboundPackets1_14_4` enum that adds `BLOCK_BREAK_ACK` as the only addition beyond the 1.14.3 packet set. Source: `ClientboundPackets1_14_4.java:116`.
---
## Summary of VERIFY flags
<!-- VERIFY --> Release dates for 1.14.11.14.4 cross-checked only against minecraft.wiki article headers as fetched 2026-06-19; authoritative dates confirmed for 1.14 (2019-04-23) from wiki. Patch dates (May 13, May 27, June 24, July 19) treated as accurate per wiki.
<!-- VERIFY --> The `entity_sound_effect` packet (0x50 in 1.14) — confirmed present in `minecraft-data` `data/pc/1.14/protocol.json` and `ClientboundPackets1_14.java:104` (SOUND_ENTITY 0x50) but not in `ClientboundPackets1_13.java`; however exact field structure not deeply inspected — likely `entityId: VarInt, soundId: VarInt, category: VarInt, volume: Float, pitch: Float`.
<!-- VERIFY --> The villager entity data index 15 added in 1.14.1 (`addIndex(15)` in `EntityPacketRewriter1_14_1`) — confirmed in VV source, but the semantic meaning of this index (<!-- VERIFY --> likely the `Villager XP` or second VillagerData sub-field) is not fully confirmed from available sources.
+269
View File
@@ -0,0 +1,269 @@
# 1.15.x — Buzzy Bees
**Protocols:** 573 (1.15), 575 (1.15.1), 578 (1.15.2)
**Release dates:** 1.15 — 2019-12-10 · 1.15.1 — 2019-12-17 · 1.15.2 — 2020-01-21
**Minecraft wiki release articles:**
- <https://minecraft.wiki/w/Java_Edition_1.15> (fetched 2026-06-19) — release date 2019-12-10, protocol 573, data version 2225, resource/data pack format 5
- <https://minecraft.wiki/w/Java_Edition_1.15.1> (fetched 2026-06-19) — release date 2019-12-17, protocol 575, data version 2227
- <https://minecraft.wiki/w/Java_Edition_1.15.2> (fetched 2026-06-19) — release date 2020-01-21, protocol 578, data version 2230
**Protocol version numbers confirmed:** `ProtocolVersion.java` lines 68-70 — `register(573, "1.15")`, `register(575, "1.15.1")`, `register(578, "1.15.2")``/tmp/mcproto-refs/ViaVersion/api/src/main/java/com/viaversion/viaversion/api/protocol/version/ProtocolVersion.java`; cross-checked against `/tmp/mcproto-refs/minecraft-data/data/pc/1.15/version.json` (573), `1.15.1/version.json` (575), `1.15.2/version.json` (578).
**ViaVersion packages:**
- `v1_14_4to1_15``/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_14_4to1_15/`
- `v1_15to1_15_1``…/protocols/v1_15to1_15_1/Protocol1_15To1_15_1.java`
- `v1_15_1to1_15_2``…/protocols/v1_15_1to1_15_2/Protocol1_15_1To1_15_2.java`
**minecraft-data sources:** `data/pc/1.14.4/`, `data/pc/1.15/`, `data/pc/1.15.1/`, `data/pc/1.15.2/``/tmp/mcproto-refs/minecraft-data/`
---
## Headline changes — Buzzy Bees
1.15 ("Buzzy Bees", 2019-12-10) is a **minor protocol bump** — the gameplay focus (bees, beehives, honey blocks, honeycomb) required only a small number of wire-level changes. The entity registry gained one new mob (Bee), the chunk packet gained 3D biome storage, the Login and Respawn packets gained a hashed seed field, the Spawn Living Entity and Spawn Player packets dropped inline entity metadata, and particle coordinates upgraded from f32 to f64. One packet (Acknowledge Player Digging) was moved from the end of the clientbound ID space to near the front, shifting all intervening IDs by one. No packets were added or removed overall.
The resource pack and data pack format both bumped to **5** in 1.15. Performance was a significant focus of this release: Mojang described the generation changes as a "massive upgrade", and floating-point precision for entity and particle positions was increased from 32-bit to 64-bit to fix rendering artefacts at large distances from world origin.
Sources: <https://minecraft.wiki/w/Java_Edition_1.15> (fetched 2026-06-19); ViaVersion `v1_14_4to1_15` package (see below).
---
## Protocol changes vs 1.14.4 (573 vs 490), by state
Source: diff of `ClientboundPackets1_14_4` (`v1_14_3to1_14_4/packet/`) against `ClientboundPackets1_15` (`v1_14_4to1_15/packet/ClientboundPackets1_15.java`); protocol.json diffs across `minecraft-data/data/pc/1.14.4/` vs `data/pc/1.15/`; `WorldPacketRewriter1_15.java` and `EntityPacketRewriter1_15.java`; `EntityTypes1_15.java`.
**Packet count:** 93 clientbound play, 46 serverbound play — identical to 1.14.4. Net change: **0 packets added, 0 removed**.
### Handshake / Status / Login / Configuration
No changes to handshaking, status, or login state packet set or structure.
### Play — clientbound packet ID reordering
`Acknowledge Player Digging` moved from **0x5C** (its position in 1.14.4) to **0x08** (its new position in 1.15). This inserts it between `Statistics` (0x07) and what was previously `Block Break Animation`, shifting every packet from old 0x08 through old 0x5B up by one slot. The packet count stays at 93 — this is a reorder, not an addition.
| New ID (1.15) | Old ID (1.14.4) | Packet |
|---|---|---|
| 0x08 | 0x5C | `acknowledge_player_digging` (BLOCK_BREAK_ACK) — **moved** |
| 0x09 | 0x08 | `block_break_animation` |
| 0x0A | 0x09 | `tile_entity_data` |
| … | … | (all packets 0x080x5B each shift +1) |
| 0x5C | 0x5B | `tags` |
Source: `minecraft-data` packet-ID maps `data/pc/1.14.4/protocol.json` vs `data/pc/1.15/protocol.json` (confirmed by comparing the ordinal ordering in `ClientboundPackets1_15.java` lines `BLOCK_BREAK_ACK // 0x08`, `BLOCK_DESTRUCTION // 0x09` against `ClientboundPackets1_14_4.java` where `BLOCK_DESTRUCTION // 0x08` was at 0x08).
### Play — structurally changed clientbound packets
Six packets changed field layout. No serverbound packets changed structure.
#### 1. Login (CB 0x26 in 1.15, was 0x25)
New fields added between `Dimension` and `Max Players`:
| Field | Type | Notes |
|---|---|---|
| Entity ID | i32 | unchanged |
| Gamemode | u8 | unchanged |
| Dimension | i32 | unchanged |
| **Hashed Seed** | **i64** | **new in 1.15** — SHA-256 of the world seed, lower 64 bits; used by client for biome noise <!-- VERIFY: exact hash algorithm from wiki --> |
| Max Players | u8 | unchanged |
| Level Type | String | unchanged |
| View Distance | VarInt | unchanged |
| Reduce Debug Info | bool | unchanged |
| **Enable Respawn Screen** | **bool** | **new in 1.15** — when false, client respawns immediately without the death screen; ViaVersion injects `!Via.getConfig().is1_15InstantRespawn()` |
Source: `minecraft-data/data/pc/1.15/protocol.json` `packet_login` field list; `EntityPacketRewriter1_15.java` lines registering `LOGIN` with `wrapper.write(Types.LONG, 0L)` (seed) and `wrapper.write(Types.BOOLEAN, !Via.getConfig().is1_15InstantRespawn())` (respawn screen).
#### 2. Respawn (CB 0x3B in 1.15, was 0x3A)
New field inserted between `Dimension` and `Gamemode`:
| Field | Type | Notes |
|---|---|---|
| Dimension | i32 | unchanged |
| **Hashed Seed** | **i64** | **new in 1.15** — same value as in Login |
| Gamemode | u8 | unchanged |
| Level Type | String | unchanged |
Source: `minecraft-data/data/pc/1.15/protocol.json` `packet_respawn`; `EntityPacketRewriter1_15.java` `RESPAWN` handler `wrapper.write(Types.LONG, 0L)`.
#### 3. Level Chunk / Map Chunk (CB 0x22 in 1.15, was 0x21)
Biome data moved from a flat 256-int array (one int per column, 16×16) to a **1024-int array** (four ints per 4×4×4 biome cube, 4×4×64 = 1024 entries covering the full 256-block height in 4-block increments). The biomes field is present only when `groundUp = true` (full chunk):
| Field | Type | Notes |
|---|---|---|
| X | i32 | unchanged |
| Z | i32 | unchanged |
| Ground-Up | bool | unchanged |
| Primary Bitmask | VarInt | unchanged |
| Heightmaps | NBT | unchanged |
| **Biomes** | **i32[1024]** | **new field** — present only if Ground-Up; replaces the implicit 256-byte array baked into chunk data in 1.14.4 |
| Data length | VarInt | unchanged |
| Data | byte[] | unchanged |
| Block Entities | NBT[] | unchanged |
The ViaVersion `WorldPacketRewriter1_15.java` translates from 1.14.4 to 1.15 by converting the old 256-entry column biome array: for each 4×4 horizontal sub-area, it reads the biome at the "middle" column (`x=(j<<2)+2, z=(i<<2)+2`) and replicates it across all 64 Y-layers. `ChunkType1_15.java` (in the ViaVersion API) confirms the 1024-int `biomeData` read loop.
Source: `WorldPacketRewriter1_15.java` `register()` method — `getBlockRewriter().registerLevelChunk(…ChunkType1_14.TYPE, ChunkType1_15.TYPE, …)` with the biome-upscale lambda; `ChunkType1_15.java` `read()``int[] biomeData = fullChunk ? new int[1024] : null; for (int i = 0; i < 1024; i++) biomeData[i] = input.readInt();`; `minecraft-data/data/pc/1.15/protocol.json` `packet_map_chunk``"biomes": ["switch", {"compareTo": "groundUp", "fields": {"true": ["array", {"count": 1024, "type": "i32"}]}}]`.
#### 4. Level Particles / World Particles (CB 0x24 in 1.15, was 0x23)
Position coordinates upgraded from **f32 to f64**:
| Field | Type 1.14.4 | Type 1.15 | Notes |
|---|---|---|---|
| Particle ID | i32 | i32 | unchanged |
| Long Distance | bool | bool | unchanged |
| X | **f32** | **f64** | precision upgrade |
| Y | **f32** | **f64** | precision upgrade |
| Z | **f32** | **f64** | precision upgrade |
| Offset X/Y/Z | f32 | f32 | unchanged |
| Particle Data | f32 | f32 | unchanged |
| Particle Count | i32 | i32 | unchanged |
| Data | particleData | particleData | unchanged |
Source: `WorldPacketRewriter1_15.java``map(Types.FLOAT, Types.DOUBLE)` for fields 2, 3, 4; `minecraft-data/data/pc/1.15/protocol.json` `packet_world_particles``"x": "f64"` vs `"x": "f32"` in 1.14.4.
#### 5. Spawn Entity Living / Add Mob (CB 0x03)
Entity metadata **removed from the spawn packet itself**. In 1.14.4, `spawn_entity_living` included a trailing `entityMetadata` field. In 1.15, metadata is no longer sent inline with the spawn — it must arrive separately via `Set Entity Data` (0x44).
| Field | 1.14.4 | 1.15 |
|---|---|---|
| Entity ID | VarInt | VarInt |
| Entity UUID | UUID | UUID |
| Type | VarInt | VarInt |
| X/Y/Z | f64 | f64 |
| Yaw/Pitch/Head Pitch | i8 | i8 |
| Velocity X/Y/Z | i16 | i16 |
| **Metadata** | **entityMetadata** | **(removed)** |
ViaVersion bridges this by reading the metadata from the 1.14.4 server's spawn packet, sending the spawn packet first, then synthesising and sending a `SET_ENTITY_DATA` packet if the metadata list is non-empty. See `EntityPacketRewriter1_15.java` `sendEntityDataPacket()` method.
Source: `minecraft-data/data/pc/1.14.4/protocol.json` `packet_spawn_entity_living` vs `data/pc/1.15/protocol.json`; `EntityPacketRewriter1_15.java` `registerPackets()``ADD_MOB` handler reads `Types1_14.ENTITY_DATA_LIST` then calls `sendEntityDataPacket()`.
#### 6. Spawn Player / Named Entity Spawn / Add Player (CB 0x05)
Same metadata removal as Spawn Living Entity above. In 1.14.4, `named_entity_spawn` had a trailing `entityMetadata` field; 1.15 drops it, with metadata delivered via a subsequent `Set Entity Data` packet.
| Field | 1.14.4 | 1.15 |
|---|---|---|
| Entity ID | VarInt | VarInt |
| Player UUID | UUID | UUID |
| X/Y/Z | f64 | f64 |
| Yaw/Pitch | i8 | i8 |
| **Metadata** | **entityMetadata** | **(removed)** |
Source: `minecraft-data/data/pc/1.14.4/protocol.json` `packet_named_entity_spawn` vs `data/pc/1.15/protocol.json`; `EntityPacketRewriter1_15.java` `ADD_PLAYER` handler calls `sendEntityDataPacket(wrapper, entityId)`.
### Play — entity metadata changes
Two metadata index changes in 1.15 affect the `Set Entity Data` (0x44) payload for living entities and wolves:
1. **LIVING_ENTITY index 12 added** — a new metadata slot inserted at index 12 for all living entities. ViaVersion `EntityPacketRewriter1_15.java:131`: `filter().type(EntityTypes1_15.LIVING_ENTITY).addIndex(12)`. <!-- VERIFY: confirm what this slot encodes (likely bed-sleeping state based on 1.14.4 additions context) -->
2. **WOLF index 18 removed** — a metadata slot dropped from wolves. ViaVersion `EntityPacketRewriter1_15.java:132`: `filter().type(EntityTypes1_15.WOLF).removeIndex(18)`. <!-- VERIFY: confirm what this slot encoded in 1.14.4 -->
### New entity type: Bee (entity type ID 4)
Bee was inserted at **entity type ID 4** in the registry (alphabetically between `bat` = 3 and `blaze` = 5 in 1.15's ordering). All entity IDs ≥ 4 in 1.14.4 are shifted up by one. ViaVersion `EntityPacketRewriter1_15.java:158`: `public int newEntityId(final int id) { return id >= 4 ? id + 1 : id; // 4 = bee }`.
Bee is an `ABSTRACT_ANIMAL` subtype (`EntityTypes1_15.java``BEE(ABSTRACT_ANIMAL)`). Confirmed in `minecraft-data/data/pc/1.15.2/entities.json`: `internalId=4, id=4, name="bee"`.
### New tags registered in 1.15
ViaVersion `Protocol1_14_4To1_15.java` `onMappingDataLoaded()` registers new empty tags that must exist in 1.15 but did not exist in 1.14.4:
- Block tags added: `minecraft:bee_growables`, `minecraft:beehives`
- Item tag added: `minecraft:lectern_books`
- Entity tag added: `minecraft:beehive_inhabitors`
- Block tag removed from translation: `minecraft:dirt_like` (was in 1.14.4; dropped in 1.15)
These flow through the `UPDATE_TAGS` (0x5C) packet.
### New particles (1.15)
Three bee-related particles added to the particle registry (IDs confirmed from `minecraft-data/data/pc/1.15/particles.json`):
| ID | Name |
|---|---|
| 58 | `dripping_honey` |
| 59 | `falling_honey` |
| 60 | `landing_honey` |
Also `falling_nectar` (ID present in the 1.15 list). Total particle count increased from ~58 to 62. <!-- VERIFY: exact 1.14.4 particle count — 1.14.4 particles.json not present in the minecraft-data snapshot, count inferred from the 1.15 list minus bee additions -->
### New sounds (1.15)
18 new sound events added (from diff of `minecraft-data/data/pc/1.14.4/sounds.json` vs `data/pc/1.15/sounds.json`):
- **Bee entity sounds:** `entity.bee.death`, `entity.bee.hurt`, `entity.bee.loop`, `entity.bee.loop_aggressive`, `entity.bee.pollinate`, `entity.bee.sting`
- **Beehive block sounds:** `block.beehive.drip`, `block.beehive.enter`, `block.beehive.exit`, `block.beehive.shear`, `block.beehive.work`
- **Honey block sounds:** `block.honey_block.break`, `block.honey_block.fall`, `block.honey_block.hit`, `block.honey_block.place`, `block.honey_block.slide`, `block.honey_block.step`
- **Item:** `item.honey_bottle.drink`
Sounds flow through the `CUSTOM_SOUND` (0x1A) / `SOUND` (0x52) packets; these are registry additions only, no packet structure change.
### Block and item mappings
New blocks/items (beehive, bee_nest, honey_block, honeycomb_block, honeycomb, honey_bottle) were added, requiring updated block-state and item ID mappings. ViaVersion carries these in `mappings-1.14to1.15.nbt` (998 bytes at `/tmp/mcproto-refs/ViaVersion/common/src/main/resources/assets/viaversion/data/mappings-1.14to1.15.nbt`). Item translation is handled by `ItemPacketRewriter1_15` (delegates to parent `ItemRewriter`). <!-- VERIFY: exact new block-state ID count for 1.15 vs 1.14.4 -->
---
## Per-patch sub-sections
### 1.15 → 1.15.1 (protocol 573 → 575)
**Zero protocol changes.** ViaVersion `Protocol1_15To1_15_1.java` is a minimal stub — it extends `AbstractProtocol` with identical clientbound/serverbound packet class references (`ClientboundPackets1_15` used for both old and new) and registers no packet handlers:
```java
public class Protocol1_15To1_15_1 extends AbstractProtocol<
ClientboundPackets1_15, ClientboundPackets1_15,
ServerboundPackets1_14, ServerboundPackets1_14> {
public Protocol1_15To1_15_1() {
super(ClientboundPackets1_15.class, ClientboundPackets1_15.class, ...);
}
}
```
`minecraft-data` diff of `data/pc/1.15/protocol.json` vs `data/pc/1.15.1/protocol.json`: no packet additions, removals, ID changes, or field structure changes in any state (handshaking, login, play).
The protocol number bumped to 575 for compliance purposes. Gameplay changes: chunk rendering performance optimization (especially for chunks with many block states), dolphin spawning in bubble columns fixed, invalid biome ID network handling improved, 10 bugs total. Data version: 2225 → 2227.
ViaVersion git log for `protocols/v1_15to1_15_1/`: only cosmetic commits — `cff9a8715 [ci skip] Update copyright header`, `9f6e7fa4e [ci skip] Update copyright header`, `e965e9713 Package/class renames and moves`.
Source: `Protocol1_15To1_15_1.java`; `minecraft-data` protocol.json diff; <https://minecraft.wiki/w/Java_Edition_1.15.1> (fetched 2026-06-19).
### 1.15.1 → 1.15.2 (protocol 575 → 578)
**Zero protocol changes.** `Protocol1_15_1To1_15_2.java` is identical in structure to the 1.15.1 stub — no registered handlers, same packet classes.
`minecraft-data` diff of `data/pc/1.15.1/protocol.json` vs `data/pc/1.15.2/protocol.json`: no packet additions, removals, ID changes, or field structure changes.
Gameplay changes: two new gamerules (`doPatrolSpawning`, `doTraderSpawning`), bees no longer aggro when hive/nest broken with Silk Touch, status effects now stack properly when overwritten by higher-amplifier effect, bee nest spawn rates tuned for flower forests and birch variants, 35 bugs fixed. Data version: 2227 → 2230.
ViaVersion git log for `protocols/v1_15_1to1_15_2/`: only cosmetic commits — `cff9a8715`, `9f6e7fa4e`, `e965e9713`.
Source: `Protocol1_15_1To1_15_2.java`; `minecraft-data` protocol.json diff; <https://minecraft.wiki/w/Java_Edition_1.15.2> (fetched 2026-06-19).
---
## Proxy / forwarding & translation impact
For a proxy bridging 1.14.4 ↔ 1.15 clients, the translation work is moderate but confined to specific packets:
1. **Packet ID table** — every clientbound play ID from 0x08 upward has shifted by +1 in 1.15. A 1.14.4 client must receive the old IDs; a 1.15 client expects the new ones. This is the most pervasive change.
2. **Login + Respawn** — must inject `hashedSeed` (i64, can be 0 when downgrading) and `enableRespawnScreen` (bool) when serving 1.15 clients from a 1.14.4 server. ViaVersion injects `0L` for the seed and makes the respawn screen configurable (`is1_15InstantRespawn()`).
3. **Chunk biome data** — when sending chunks to a 1.15 client, the 256-column biome array must be converted to 1024-entry 3D format. ViaVersion does this by sampling the "centre" column of each 4×4 horizontal cell and replicating across 64 Y layers (`WorldPacketRewriter1_15.java` biome-upscale lambda).
4. **Particle position precision**`Level Particles` X/Y/Z must be widened from f32 to f64 for 1.15 clients. ViaVersion `WorldPacketRewriter1_15.java`: `map(Types.FLOAT, Types.DOUBLE)` for all three position fields.
5. **Spawn packet metadata stripping** — 1.15 spawn packets (`Add Mob`, `Add Player`) no longer carry inline metadata. When bridging a 1.14.4 server → 1.15 client, ViaVersion reads the metadata from the spawn packet and sends a synthetic `Set Entity Data` immediately after. The inverse (1.15 server → 1.14.4 client) requires re-attaching the `Set Entity Data` payload back into the spawn packet.
6. **Entity type ID shift** — Bee inserted at ID 4; all IDs ≥ 4 from 1.14.4 are +1 in 1.15. Must be remapped in all entity spawn packets.
7. **Entity metadata index shifts** — LIVING_ENTITY index 12 added, WOLF index 18 dropped. Any `Set Entity Data` packet containing metadata for living entities or wolves must have indices adjusted.
8. **Block/item/sound/particle ID remapping** — new bee-related IDs in all registries; carried by ViaVersion's `mappings-1.14to1.15.nbt` mapping table.
**1.15.1 and 1.15.2:** no translation work beyond the protocol version handshake number. Both patch releases are protocol-identical to 1.15 in structure; ViaVersion treats them as pass-through (empty protocol stubs).
+314
View File
@@ -0,0 +1,314 @@
# Java Edition 1.16 — Nether Update
| Release | Protocol | Release Date | Data Version |
|---------|----------|--------------|--------------|
| 1.16 | 735 | 2020-06-23 | 2566 |
| 1.16.1 | 736 | 2020-06-24 | 2567 <!-- VERIFY --> |
| 1.16.2 | 751 | 2020-08-11 | 2578 |
| 1.16.3 | 753 | 2020-09-10 | 2580 <!-- VERIFY --> |
| 1.16.4 | 754 | 2020-11-02 | 2584 |
| 1.16.5 | 754 | 2021-01-15 | 2586 <!-- VERIFY --> |
Sources: minecraft.wiki/w/Java_Edition_1.16 (fetched 2026-06-19), minecraft.wiki/w/Java_Edition_1.16.2 (fetched 2026-06-19), minecraft.wiki/w/Java_Edition_1.16.4 (fetched 2026-06-19); `/tmp/mcproto-refs/minecraft-data/data/pc/*/version.json`; `ViaVersion/api/.../ProtocolVersion.java:7175`.
---
## Headline Changes (Nether Update)
1.16 overhauled the Nether completely: four new biomes (Crimson Forest, Warped Forest, Soul Sand Valley, Basalt Deltas), four new mobs (Piglin, Hoglin, Zoglin, Strider), and the Netherite equipment tier. The most consequential **protocol change** was replacing the dimension system's hardcoded `int` enum with a **Codec NBT tag** sent at login — the server now declares all available dimensions at runtime, enabling data-pack-defined custom dimensions. The chat text-component format gained **full RGB hex colour** support (`"color":"#rrggbb"`), and the Login Success packet changed the UUID wire type from a plain String to a proper binary UUID.
---
## Protocol Changes vs 1.15.2
### Login state
**Login Success (0x02 C→client)**
In 1.15.2 the `uuid` field was `STRING` — a plain text UUID like `"550e8400-e29b-41d4-a716-446655440000"`.
In 1.16 it became a binary `UUID` (two `Long`s, big-endian, 16 bytes). ViaVersion translates the type on downgrade by parsing the string with `UUID.fromString()` and writing the binary form.
Source: `Protocol1_15_2To1_16.java:8387` (ViaVersion) — `UUID uuid = UUID.fromString(wrapper.read(Types.STRING)); wrapper.write(Types.UUID, uuid);`; minecraft-data `data/pc/1.15.2/protocol.json` (uuid type: `string`) vs `data/pc/1.16/protocol.json` (uuid type: `UUID`).
Also: ViaVersion introduced `ClientboundBaseProtocol1_16.java` which overrides `passthroughUUID` to use `Types.UUID` instead of a string form, affecting all base login handling from 1.16 onwards.
### Play state — clientbound
#### 0x25 Login (Join Game) — major restructure
The Login/Join Game packet grew substantially. Diff against 1.15.2:
| Field | 1.15.2 | 1.16 |
|-------|--------|------|
| `entityId` | Int | Int |
| `gameMode` | UnsignedByte | UnsignedByte |
| `previousGameMode` | *(absent)* | Byte (1 = none) |
| `worldNames` | *(absent)* | Array\<String\> — list of all registered world identifiers |
| `dimensionCodec` | *(absent)* | **NBT CompoundTag** — full registry of dimension types and biomes |
| `dimension` | **Int** (1/0/1 enum) | **String** resource key (`"minecraft:overworld"` etc.) |
| `worldName` | *(absent)* | String — current world identifier |
| `hashedSeed` | Long | Long |
| `maxPlayers` | UnsignedByte | UnsignedByte |
| `levelType` | String | *(removed)* |
| `viewDistance` | VarInt | VarInt |
| `reducedDebugInfo` | Boolean | Boolean |
| `enableRespawnScreen` | Boolean | Boolean |
| `isDebug` | *(absent)* | Boolean |
| `isFlat` | *(absent)* | Boolean |
The `dimensionCodec` NBT tag has the key `"dimension"` mapping to a `ListTag<CompoundTag>`. Each dimension entry contains fields such as `piglin_safe`, `natural`, `ambient_light`, `infiniburn`, `respawn_anchor_works`, `has_skylight`, `bed_works`, `has_raids`, `logical_height`, `shrunk`, `ultrawarm`, `has_ceiling`, and optionally `fixed_time`. ViaVersion synthesizes this tag when downgrading from 1.16 to 1.15.2 via `DimensionRegistries1_16.java` (four entries: overworld, overworld_caves, the_nether, the_end).
Sources: `EntityPacketRewriter1_16.java:133157` (ViaVersion); `minecraft-data/data/pc/1.16/protocol.json` (packet_login); `DimensionRegistries1_16.java` (full NBT structure).
#### 0x3A Respawn — dimension field changed
| Field | 1.15.2 | 1.16 |
|-------|--------|------|
| `dimension` | Int (1/0/1 enum) | String (resource key) |
| `worldName` | *(absent)* | String |
| `hashedSeed` | Long | Long |
| `gamemode` | UnsignedByte | UnsignedByte |
| `previousGamemode` | *(absent)* | Byte |
| `levelType` | String | *(removed)* |
| `isDebug` | *(absent)* | Boolean |
| `isFlat` | *(absent)* | Boolean |
| `copyMetadata` | *(absent)* | Boolean (keep player attributes on respawn) |
Source: `EntityPacketRewriter1_16.java:113131`; `minecraft-data/data/pc/1.16/protocol.json` (packet_respawn vs 1.15.2).
#### 0x0E Chat (clientbound) — sender UUID added
The Chat packet in 1.16 gained a `sender` UUID field (after the existing `message` string and `position` byte). ViaVersion fills it with the nil UUID `00000000-0000-0000-0000-000000000000` for system messages when downgrading.
Source: `Protocol1_15_2To1_16.java:125135`; `minecraft-data/data/pc/1.16/protocol.json` (packet_chat sender field type: UUID).
#### 0x00 Spawn Entity — absorbed lightning bolt (ADD_GLOBAL_ENTITY removed)
`packet_spawn_entity_weather` (0x2C in 1.15.2) was removed. Lightning bolts are now spawned via the regular `Spawn Entity` (0x00) packet with an entity type of `minecraft:lightning_bolt`. ViaVersion handles this by converting the old `ADD_GLOBAL_ENTITY` packet (type byte `1` = lightning) to `ADD_ENTITY` with a synthesised UUID and zero velocity.
Source: `EntityPacketRewriter1_16.java:85108`; `minecraft-data` packet list diff (packet_spawn_entity_weather absent in 1.16).
#### Light Update (0x24) — new boolean field
A new Boolean field "take neighbour's light into account" was added to the Light Update packet. ViaVersion writes `true` for this field when downgrading.
Source: `WorldPacketRewriter1_16.java:5965`.
#### Chunk data — heightmap padding changed
The heightmap `LongArray` encoding in Chunk Data changed: 1.15 used compact arrays without per-element padding (values could span two longs), but 1.16 added per-element padding (each value is contained within a single long, with padding bits at the end of each long). ViaVersion re-packs the heightmap when downgrading.
Source: `WorldPacketRewriter1_16.java:4657`.
#### Block entities — UUID fields changed from String to int\[\]
Two block-entity NBT format changes:
- `minecraft:conduit`: `target_uuid` (String) → `Target` (IntArray of 4 ints)
- `minecraft:skull`: `Owner.Id` (String) → `Owner.Id` (IntArray); key `Owner` renamed to `SkullOwner`
Source: `WorldPacketRewriter1_16.java:76118`.
### Play state — serverbound
#### 0x0F Generate Structure (new)
New packet for generating Jigsaw structures in-game. Fields: `location` (BlockPosition), `levels` (VarInt), `keepJigsaws` (Boolean). ViaVersion cancels it when downgrading to 1.15.2.
Source: `minecraft-data/data/pc/1.16/protocol.json` (packet_generate_structure); `Protocol1_15_2To1_16.java:202203`.
#### 0x05 Client Settings — new fields
`CLIENT_INFORMATION` in 1.16 added fields (specifically `mainHand`); ViaVersion cancels `JIGSAW_GENERATE` and `SET_JIGSAW_BLOCK` packets on downgrade.
#### 0x0E Interact Entity — new sneak field
The Interact packet gained a trailing Boolean field indicating whether the player is sneaking. ViaVersion reads and discards it on downgrade.
Source: `Protocol1_15_2To1_16.java:137152`.
#### 0x1A Player Abilities (serverbound) — fields removed
In 1.16 the client no longer sends `flyingSpeed` and `walkingSpeed` in the Player Abilities packet — only the flags byte remains. ViaVersion re-adds them from a `PlayerAbilitiesProvider` when downgrading.
Source: `Protocol1_15_2To1_16.java:194199`.
### Status state
**Status Response** — player-sample name line-break no longer allowed. Line-break characters in player sample names are split into separate entries.
Source: `Protocol1_15_2To1_16.java:89122`.
### Chat component — RGB hex colour support
In snapshot 20w17a (part of the 1.16 development cycle) the text-component `"color"` field was extended to accept 24-bit RGB hex strings of the form `"#rrggbb"` in addition to the 16 named colours (`"red"`, `"blue"`, etc.). This is a JSON-level change; no wire-format packet change accompanies it — the component is still serialised as a JSON string in the `Chat` packet. Older clients that don't understand `"#rrggbb"` will ignore or error on the colour.
Source: minecraft.wiki/w/Raw_JSON_text_format (fetched 2026-06-19); `ComponentRewriter1_16.java` processes component text but does not translate RGB colours, confirming ViaVersion does not downgrade them. <!-- VERIFY that ViaVersion makes no attempt to map hex colours to nearest named colour on downgrade -->
### Attribute identifiers renamed
All attribute keys were renamed from camelCase to snake_case namespaced identifiers (e.g. `generic.maxHealth``minecraft:generic.max_health`). ViaVersion maps these in `AttributeMappings1_16.java` and `UPDATE_ATTRIBUTES` rewriter.
Source: `EntityPacketRewriter1_16.java:159197`; `AttributeMappings1_16.java`.
---
## Per-Patch Sub-sections
### 1.16 — Protocol 735 (2020-06-23)
Initial release of the Nether Update. All changes above (dimension codec, RGB, sender UUID, lightning bolt, heightmap padding) introduced here. The release was reuploaded ~6 hours after initial publication to address a Realms connectivity regression (MC-191138), but the hotfix failed and 1.16.1 followed the next day.
Source: minecraft.wiki/w/Java_Edition_1.16.
### 1.16.1 — Protocol 736 (2020-06-24)
Hotfix release. **No protocol-level packet changes.** The protocol number incremented from 735→736, making 1.16 and 1.16.1 mutually incompatible even though no packet structure changed. ViaVersion's `Protocol1_16To1_16_1` is an essentially empty protocol class that registers no packet remappers (inherits the same `ClientboundPackets1_16`/`ServerboundPackets1_16` enums for both sides).
Fixes: Slime block arrow launch (MC-110792), crossbow third-person animation (MC-146743), Endermen hostility (MC-186546), Realms crash (MC-191138).
Source: minecraft.wiki/w/Java_Edition_1.16.1; `Protocol1_16To1_16_1.java` (empty class body).
### 1.16.2 — Protocol 751 (2020-08-11)
Significant protocol update despite sharing the same major version. Key changes:
#### Join Game — isHardcore extracted + dimension field type changed
The `isHardcore` flag was separated from the `gameMode` byte (previously hardcoded as bit 0x08 of the gamemode value) into its own Boolean field. `maxPlayers` also changed from `UnsignedByte` to `VarInt`. Most critically, the `dimension` field changed again:
| Field | 1.16 | 1.16.2 |
|-------|------|--------|
| `isHardcore` | *(packed in gameMode byte)* | **Boolean** (separate) |
| `gameMode` | UnsignedByte (incl. hardcore bit) | Byte (without hardcore bit) |
| `dimensionCodec` | NBT (registry) | NBT (registry, updated format) |
| `dimension` | **String** (resource key) | **NBT CompoundTag** (full dimension data inline) |
| `maxPlayers` | UnsignedByte | **VarInt** |
In 1.16.2 the current dimension's full data is sent inline in the `dimension` field (a `CompoundTag`) rather than just a resource-key string referencing the registry. The `dimensionCodec` NBT registry was also updated.
ViaVersion converts from 1.16.2 back to 1.16 by reading the NBT dimension data from the registry mapping (`MappingData1_16_2.getDimensionDataMap()`), discarding the 1.16.2-style NBT, and writing the resource-key string.
Source: `EntityPacketRewriter1_16_2.java:4170`; `minecraft-data/data/pc/1.16.2/protocol.json` (packet_login); `WorldPacketRewriter1_16_2.java`.
#### Respawn — dimension field also became NBT
The `dimension` field in Respawn changed from String to CompoundTag, parallel to Login.
Source: `EntityPacketRewriter1_16_2.java:7379`.
#### Section Blocks Update (0x3B) — replaces Multi Block Change (0x0F)
`multi_block_change` was replaced by `section_blocks_update` and moved from packet ID 0x0F to 0x3B, causing all packets from 0x10 through 0x3A in 1.16 to shift down by one slot (0x0F0x3A in 1.16.2).
Old format (1.16): `chunkX` (Int) + `chunkZ` (Int) + array of records per block `{horizontalPos:u8, y:u8, blockId:VarInt}`.
New format (1.16.2): A single packed Long encoding `chunkX:22 | chunkZ:22 | chunkY:20` (the chunk section rather than chunk column), a Boolean `notTrustEdges`, then an array of packed VarLongs each encoding `blockStateId:52 | localX:4 | localZ:4 | localY:4`. This is more efficient and per-section rather than per-column.
ViaVersion converts by splitting the old per-column records into per-section `SECTION_BLOCKS_UPDATE` packets.
Source: `WorldPacketRewriter1_16_2.java:3974`; `ClientboundPackets1_16_2.java:83` (SECTION_BLOCKS_UPDATE at 0x3B); `minecraft-data/data/pc/1.16.2/protocol.json` (packet_section_blocks_update vs packet_multi_block_change).
#### Recipe Book — split into two serverbound packets
`crafting_book_data` (0x1E) was split into two packets:
- `RECIPE_BOOK_CHANGE_SETTINGS` (0x1E) — book type, open state, filter state
- `RECIPE_BOOK_SEEN_RECIPE` (0x1F) — single recipe identifier
All serverbound packets at 0x1F and above shifted up by one slot.
Source: `Protocol1_16_1To1_16_2.java:5978`; `ServerboundPackets1_16_2.java:5455`.
#### Entity type additions
New entity type `Piglin Brute` added. Piglin entity metadata indices 15 and 16 swapped between 1.16 and 1.16.2.
Source: `EntityPacketRewriter1_16_2.java:8693`.
#### Tags — `furnace_materials` removed
The block/item tag `minecraft:furnace_materials` was removed.
Source: `Protocol1_16_1To1_16_2.java:85`.
### 1.16.3 — Protocol 753 (2020-09-10)
Minimal release. The wiki states "the only difference between the two [1.16.3-rc1 and 1.16.3] is a change in the data and protocol versions." No packet-structure changes. ViaVersion's `Protocol1_16_2To1_16_3` is an empty class that registers no remappers.
Fixes: Nether mob pathfinding, baby piglin item duplication.
Source: minecraft.wiki/w/Java_Edition_1.16.3; `Protocol1_16_2To1_16_3.java` (empty class body).
### 1.16.4 — Protocol 754 (2020-11-02)
#### Edit Book serverbound — hand field changed
The `Edit Book` serverbound packet (0x0C/0x1E depending on version) changed how the hand is encoded. In 1.16.4 the hand is now a VarInt slot index (40 = offhand, otherwise mainhand), whereas before it was a hand enum VarInt (0=main, 1=off). ViaVersion converts slot 40 → hand 1 (offhand), everything else → 0 (mainhand).
Source: `Protocol1_16_3To1_16_4.java:3344`.
#### Social Interactions screen
A new in-game social interactions screen was added (opens with P by default) allowing players to suppress chat from specific players. This is a client-side UI feature with no new protocol packets; it operates on the existing Chat packet sender UUID field (added in 1.16).
Source: minecraft.wiki/w/Java_Edition_1.16.4.
#### Snapshot high-bit scheme introduced
Starting with **1.16.4-pre1**, Minecraft development releases began using protocol version numbers with bit 30 set (`1 << 30 = 0x40000000`). The release version of a snapshot's corresponding full release keeps the same protocol number as the previous compatible release in cases of wire compatibility.
For example, snapshots leading to 1.16.4 used versions like `0x40000001`, `0x40000002`, etc. (incrementing by 1 per snapshot), while the full 1.16.4 release used 754. The ViaVersion implementation encodes this as `(1 << 30) | snapshotVersion` in `ProtocolVersion.java:305`.
This convention has been used for all snapshot/pre-release versions ever since.
Source: minecraft.wiki/w/Java_Edition_1.16.4 ("New network protocol scheme, with a high bit (bit 30) set for snapshots"); `ProtocolVersion.java:305` (`return (1 << 30) | snapshotVersion`); minecraft.wiki/w/Protocol_version (fetched 2026-06-19).
### 1.16.5 — Protocol 754 (2021-01-15)
Shares protocol 754 with 1.16.4; 1.16.4 clients can connect to 1.16.5 servers and vice versa. The update addressed server crash bugs (MC-203337: `IllegalStateException` in POI data). No protocol-level changes; ViaVersion has no separate protocol class for 1.16.5 and registers it as a sub-version of 754.
Source: minecraft.wiki/w/Java_Edition_1.16.5; `ProtocolVersion.java:75` (`register(754, "1.16.4-1.16.5", new SubVersionRange("1.16", 4, 5))`).
---
## Summary: Packet ID Renumbering in 1.16.2
The removal of `multi_block_change` from slot 0x0F and its replacement with `section_blocks_update` at 0x3B meant that all 1.16 clientbound play packets between 0x10 and 0x3A shifted down by one ID in 1.16.2. Any hardcoded packet ID table for 1.16 is incorrect for 1.16.2.
Notable shifts (1.16 ID → 1.16.2 ID):
| Packet | 1.16 | 1.16.2 |
|--------|------|--------|
| `multi_block_change` | 0x0F | *(removed / renamed to section_blocks_update at 0x3B)* |
| `tab_complete` | 0x10 | 0x0F |
| `declare_commands` | 0x11 | 0x10 |
| `login` (Join Game) | 0x25 | 0x24 |
| `respawn` | 0x3A | 0x39 |
| `section_blocks_update` | *(absent)* | 0x3B |
Source: `minecraft-data/data/pc/1.16/protocol.json` vs `data/pc/1.16.2/protocol.json` packet ID mappings; `ClientboundPackets1_16.java` vs `ClientboundPackets1_16_2.java`.
---
## Proxy / Translation Impact
### Dimension codec — proxies must maintain per-session registry
Before 1.16, proxies could route players between servers without caring about dimension state in the Join Game packet (dimension was a fixed enum). From 1.16 onwards:
1. The proxy must read the `dimensionCodec` NBT from every backend's Login response and cache it per player.
2. On dimension change (Respawn packet), the proxy must update its cached notion of the current dimension.
3. If a ViaVersion-managed proxy downgrades a 1.16 client to a pre-1.16 backend, it must synthesise the dimension codec from scratch (using `DimensionRegistries1_16`).
4. In 1.16.2 the current dimension's NBT data is sent inline in both Login and Respawn; the proxy must parse and relay that tag accurately or player loading breaks.
### Login Success UUID type change
Pre-1.16 proxies that parse Login Success as a plain string UUID will misread the packet after 1.16. The 16-byte binary UUID is not a valid UTF-8 string; the proxy must switch UUID handling based on protocol version (≥735 = binary UUID).
### Section Blocks Update (1.16.2+) — packet format incompatibility
The new section-relative, VarLong-packed format of `section_blocks_update` is completely incompatible with the old `multi_block_change` format. Any protocol-translation layer must reconstruct the records from scratch when crossing the 1.16/1.16.2 boundary.
### Snapshot high-bit (1.16.4+ clients)
Development clients connecting to proxies send protocol versions with bit 30 set. A proxy that expects protocol versions ≤ 1000 will misidentify a snapshot client as an unknown/future version. Proxies must mask out bit 30 to identify the base release version, then handle snapshot clients as the corresponding release version or reject them gracefully.
+299
View File
@@ -0,0 +1,299 @@
# Java Edition 1.17 — Caves & Cliffs Part I
| Release | Protocol | Release Date | Data Version |
|---------|----------|--------------|--------------|
| 1.17 | 755 | 2021-06-08 | 2724 |
| 1.17.1 | 756 | 2021-07-06 | 2730 |
Sources: minecraft.wiki/w/Java_Edition_1.17 (fetched 2026-06-19); minecraft.wiki/w/Java_Edition_1.17.1 (fetched 2026-06-19); `ViaVersion/common/.../protocols/v1_16_4to1_17/` + `v1_17to1_17_1/`; `minecraft-data/data/pc/1.17/protocol.json`, `entities.json`, `loginPacket.json`; `minecraft-data/data/pc/1.17.1/protocol.json`.
---
## Headline Changes (Caves & Cliffs Part I)
1.17 was the first half of the Caves & Cliffs split — the world-height expansion (Y -64 to 320) shipped in 1.18; what 1.17 delivered was the **groundwork** for it. The dimension codec in LOGIN gained two new mandatory NBT fields (`min_y`, `height`) and the chunk/light formats were updated to support variable-height sections. Three new mobs were added (axolotl, glow squid, goat), shifting all existing entity type IDs. The most sweeping protocol-visible change was **packet explosion-splitting**: the single `SET_TITLES` packet (6 actions via a VarInt discriminant) was split into 6 independent packets; similarly `SET_BORDER` (6 actions) became 6 packets; `PLAYER_COMBAT` (3 actions) became 3 packets. The `CONTAINER_ACK` / `TRANSACTION` packet and the abstract `MOVE_ENTITY` parent were removed. A `PING` / `PONG` round-trip pair was added. Tags now carry a generic resource-location prefix, and a new `GAME_EVENT` tag type was introduced.
---
## Protocol Changes vs 1.16.4 (754 → 755)
### Play state — Clientbound (new / removed / restructured)
**Packet-split: SET_TITLES → 6 packets**
1.16 had a single `SET_TITLES` (0x4F) with a leading `VarInt` action discriminant (0=title, 1=subtitle, 2=actionbar, 3=animation, 4=clear, 5=reset). In 1.17 this became six independent packets:
| 1.17 ID | Name | Notes |
|---------|------|-------|
| 0x10 | `CLEAR_TITLES` | carries a `Boolean` reset-times field |
| 0x41 | `SET_ACTION_BAR_TEXT` | action-bar chat component |
| 0x5A | `SET_TITLES_ANIMATION` | fade-in / stay / fade-out ticks |
| 0x59 | `SET_TITLE_TEXT` | title component |
| 0x57 | `SET_SUBTITLE_TEXT` | subtitle component |
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:110134`; `v1_16_4to1_17/packet/ClientboundPackets1_17.java:88114`; minecraft-data `1.17/protocol.json` 0x10/0x41/0x57/0x59/0x5A.
**Packet-split: SET_BORDER → 6 packets**
1.16 had `SET_BORDER` (0x3D) with 6 action types. In 1.17 these became:
| 1.17 ID | Name |
|---------|------|
| 0x20 | `INITIALIZE_BORDER` |
| 0x42 | `SET_BORDER_CENTER` |
| 0x43 | `SET_BORDER_LERP_SIZE` |
| 0x44 | `SET_BORDER_SIZE` |
| 0x45 | `SET_BORDER_WARNING_DELAY` |
| 0x46 | `SET_BORDER_WARNING_DISTANCE` |
Sources: `v1_16_4to1_17/rewriter/WorldPacketRewriter1_17.java:4256`; `v1_16_4to1_17/packet/ClientboundPackets1_17.java:56/9096`.
**Packet-split: PLAYER_COMBAT → 3 packets**
1.16 had `PLAYER_COMBAT` (0x31) with action types 0=enter, 1=end, 2=kill. In 1.17:
| 1.17 ID | Name |
|---------|------|
| 0x33 | `PLAYER_COMBAT_END` |
| 0x34 | `PLAYER_COMBAT_ENTER` |
| 0x35 | `PLAYER_COMBAT_KILL` |
Sources: `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:148164`; `v1_16_4to1_17/packet/ClientboundPackets1_17.java:7577`.
**New packet: ADD_VIBRATION_SIGNAL (0x05)**
Sculk sensors (command-only in 1.17) transmit vibration signals via this new packet. Inserted at 0x05, shifting all former 0x05+ clientbound IDs up by one.
Sources: `v1_16_4to1_17/packet/ClientboundPackets1_17.java:29`; minecraft-data `1.17/protocol.json` 0x05 `sculk_vibration_signal`.
**New packet: PING (0x30)**
Added alongside the serverbound `PONG` (0x1D) to support a ping/pong round-trip independent of the `KEEP_ALIVE` flow. Sent as a single `Int` ID; client echoes it in `PONG`.
Sources: `v1_16_4to1_17/packet/ClientboundPackets1_17.java:72`; `v1_16_4to1_17/packet/ServerboundPackets1_17.java:53`.
**Removed: CONTAINER_ACK / TRANSACTION (0x11 in 1.16)**
The `TRANSACTION` (also known as `CONTAINER_ACK`) packet was removed. Servers no longer send transaction-confirmation packets for inventory clicks; the mechanism was replaced by a state ID approach in 1.17.1 (see §1.17.1 below).
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:2729` (commit `27033e929`); minecraft-data 1.16 has `0x12 transaction`, 1.17 does not.
**Removed: MOVE_ENTITY (0x2A in 1.16 → absent in 1.17)**
The parent abstract packet `MOVE_ENTITY` (0x2A, used as a no-op stand-in for the position-only move that was never actually used) was removed from the ID list. `MOVE_ENTITY_POS` (0x29), `MOVE_ENTITY_POS_ROT` (0x2A), and `MOVE_ENTITY_ROT` (0x2B) remain.
Sources: `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:167` ("The parent class of the other entity move packets that is never actually used has finally been removed from the id list"); VV comment `cancelClientbound(ClientboundPackets1_16_2.MOVE_ENTITY)`.
**Removed/renamed: REMOVE_ENTITIES → REMOVE_ENTITY (0x3A)**
In 1.16 `REMOVE_ENTITIES` (0x36) sent a `VarInt`-prefixed array of entity IDs in a single packet. In 1.17 this was replaced by `REMOVE_ENTITY` (0x3A) which carries a **single `VarInt` entity ID** per packet — one remove packet per entity.
Sources: `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:7386` (sends one `REMOVE_ENTITY` per ID from old array); `v1_16_4to1_17/packet/ClientboundPackets1_17.java:84`.
**Field changes: RESOURCE_PACK (0x3C)**
Two new fields added at the end: `Required` (Boolean) and `Prompt` (Optional Chat, nullable). ViaVersion injects these from config when downgrading.
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:8792`.
**Field changes: MAP_ITEM_DATA (0x27)**
`Tracking position` boolean removed. Marker array now encoded as optional (Boolean prefix then count if true) instead of a simple VarInt count.
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:94108`.
**Field changes: EXPLODE (0x1C)**
`Collection length` (count of blocks affected) changed from `Int` to `VarInt`.
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:136145`.
**Field changes: SET_DEFAULT_SPAWN_POSITION (0x4B)**
New `Angle` field (Float) added after the block position. Mojang initially forgot to write this to the buffer, so ViaVersion hard-codes `0f` when downgrading.
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:147153` (comment: "which Mojang just forgot to write to the buffer, lol").
**Field changes: UPDATE_ATTRIBUTES (0x63)**
Collection length changed from `Int` to `VarInt`.
Sources: `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:126132`.
**Field changes: PLAYER_POSITION (0x38)**
New `Dismount vehicle` Boolean field appended.
Sources: `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:134146`.
---
### Play state — Serverbound (new / removed)
| Change | 1.16 (0x__) | 1.17 (0x__) | Notes |
|--------|-------------|-------------|-------|
| Removed | TRANSACTION 0x07 | — | Client-side ACK for inventory transactions; removed with CONTAINER_ACK |
| Removed | CRAFTING_BOOK_DATA 0x1E | — | Collapsed into two packets below |
| Added | — | PONG 0x1D | Echo for server-sent PING |
| Added | — | RECIPE_BOOK 0x1E | Recipe book settings (replaces part of crafting_book_data) |
| Added | — | DISPLAYED_RECIPE 0x1F | "Display recipe" action (replaces part of crafting_book_data) |
Sources: minecraft-data `1.16/protocol.json` SB vs `1.17/protocol.json` SB; `v1_16_4to1_17/packet/ServerboundPackets1_17.java:53/58/59`.
**Field changes: CLIENT_INFORMATION (0x05)**
`Text filtering` Boolean field removed (client to server).
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:155166` (`read(Types.BOOLEAN)` — drops the field when translating down).
---
### Login (JOIN_GAME / LOGIN packet, 0x26 C→client)
The `LOGIN` packet gained two new top-level fields compared to 1.16:
- **`isHardcore`** (Boolean) — added as the second field after `Entity ID`. Absent in the 1.16 LOGIN packet (confirmed: `minecraft-data/data/pc/1.16/loginPacket.json` has no `isHardcore` key; `1.17/loginPacket.json` has it). ViaVersion maps it at `EntityPacketRewriter1_17.java:92`.
- **`Simulation Distance`** — added in 1.18, not 1.17. <!-- VERIFY: confirm simulation distance field was not present in 1.17 LOGIN -->
More significantly, the **dimension codec NBT** structure changed:
| Field | 1.16 | 1.17 |
|-------|------|------|
| Codec registry key | `"dimension"` (flat list) | `"minecraft:dimension_type"` (named registry) + `"minecraft:worldgen/biome"` |
| Dimension element `"name"` | present | removed |
| Dimension element `"shrunk"` | present | removed |
| Dimension element `"coordinate_scale"` | absent | added |
| Dimension element `"effects"` | absent | added (sky/fog rendering type) |
| Dimension element `"min_y"` | absent | added (always 0 in 1.17; expanded to -64 in 1.18) |
| Dimension element `"height"` | absent | added (always 256 in 1.17; expanded to 384 in 1.18) |
The **biome registry** (`minecraft:worldgen/biome`) is now part of the codec compound, transmitted inline in LOGIN. In 1.16 it was not present in the codec.
Sources: minecraft-data `1.17/loginPacket.json` vs `1.16/loginPacket.json` (deep diff, path `.dimensionCodec.value.*`); `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:196199` (`addNewDimensionData` adds `min_y=0`, `height=256`).
---
### Chunk format (LEVEL_CHUNK, 0x22)
1.17 eliminated partial-chunk ("non-full chunk") packets. All chunks are now sent as full chunks. The chunk section bitmask changed from a plain `Int` to a `BitSet` (serialized as `Long[]`). This is groundwork for the expanded world height in 1.18 — a BitSet can represent more than 16 sections.
ViaVersion: non-full chunks arriving from a 1.16 server are converted into `SECTION_BLOCKS_UPDATE` packets because the chunk payload cannot be retransmitted without full section data.
Sources: `v1_16_4to1_17/rewriter/WorldPacketRewriter1_17.java:102122` (chunk type switch `ChunkType1_16_2``ChunkType1_17`, BitSet mask).
---
### Light update format (LIGHT_UPDATE, 0x25)
Light section masks changed from `VarInt` bitmasks to `Long[]` BitSets (same direction as chunk masks). Each sky/block light mask is now serialized as a long array. Each light array is now preceded by its count (as a VarInt) before the byte arrays.
Sources: `v1_16_4to1_17/rewriter/WorldPacketRewriter1_17.java:5899` (converts VarInt bitmasks to `long[]` BitSets, writes count prefix for light array lists).
---
### Tags (UPDATE_TAGS, 0x66)
Tags are now **generically written with resource location keys**. In 1.16 the server sent 4 fixed tag types (block, item, fluid, entity) with no type identifier. In 1.17 each tag group is prefixed with its registry resource location (e.g. `"minecraft:block"`, `"minecraft:item"`, `"minecraft:fluid"`, `"minecraft:entity_type"`, `"minecraft:game_event"`), and the count of registry types is sent first as a VarInt. A fifth **Game Event** tag type (`minecraft:game_event`) was added.
Sources: `v1_16_4to1_17/Protocol1_16_4To1_17.java:6685`; `v1_16_4to1_17/Protocol1_16_4To1_17.java:181` (`addEmptyTags(RegistryType.GAME_EVENT, ...)`).
---
### Entity data (metadata)
**New index 7 — Ticks Frozen** added to the `ENTITY` base class (all entities). Index 7 is a new VarInt field representing how many ticks the entity has been in powder snow.
**Pose enum** gained a new value `LONG_JUMP` at index 6 (shifting `FALL_FLYING` from 6 to 7).
**Shulker** lost entity data index 17 (`Attachment position`, an optional BlockPos).
Sources: `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:171188`.
---
### Entity type IDs (ADD_ENTITY / ADD_MOB)
Five entities were added, inserted alphabetically, shifting most existing entity type IDs upward:
| Entity | New ID (1.17) | Notes |
|--------|---------------|-------|
| `axolotl` | 3 | New mob; inserted before `bat` (formerly 3) |
| `glow_item_frame` | 32 | New entity; subtype of item_frame |
| `glow_squid` | 33 | New mob; subtype of squid |
| `goat` | 34 | New mob |
| `marker` | 49 | New marker entity for command/datapack use |
All entities formerly with ID ≥ 3 shift; e.g. `bat` 3→4, `player` 106→111, `fishing_bobber` 107→112. Total entity types: 108 (1.16.2) → 113 (1.17).
Sources: minecraft-data `1.17/entities.json` vs `1.16.2/entities.json` (ID diff); `ViaVersion/api/.../entities/EntityTypes1_17.java:144/127/128/154/43`.
**Item frame rotation encoding change**
The `ADD_ENTITY` packet for item frames changed: 1.16 clients read rotation from the `data` field; 1.17 clients read it from `yaw`/`pitch`. ViaVersion sends a follow-up `MOVE_ENTITY_ROT` packet after `ADD_ENTITY` to supply the rotation when downgrading.
Sources: `v1_16_4to1_17/rewriter/EntityPacketRewriter1_17.java:4971`.
---
## 1.17.1 — Protocol 755 → 756 (2021-07-06)
1.17.1 was a small bug-fix release. Only a handful of protocol changes:
### REMOVE_ENTITY → REMOVE_ENTITIES (0x3A)
**Reverted.** 1.17 (755) changed the entity-remove packet from an array (1.16) to single-entity (1.17). 1.17.1 reverted this: the packet at 0x3A now carries a **`VarInt[]` array** of entity IDs again (same logical shape as 1.16's array, but with `VarInt` prefix length rather than `Int`).
This is reflected in the minecraft-data names: 1.17 has `destroy_entity` (0x3A), 1.17.1 has `entity_destroy` (0x3A). The ViaVersion translator converts the single-ID 1.17 server packet into the array wrapper for 1.17.1 clients.
Sources: `v1_17to1_17_1/Protocol1_17To1_17_1.java:4751` ("Aaaaand back to an array again!"); `v1_17to1_17_1/packet/ClientboundPackets1_17_1.java:82` (`REMOVE_ENTITIES`); minecraft-data `1.17/protocol.json` vs `1.17.1/protocol.json` (0x3A name diff).
### CONTAINER_SET_SLOT and CONTAINER_SET_CONTENT — State ID added
Both `CONTAINER_SET_SLOT` (0x16) and `CONTAINER_SET_CONTENT` (0x14) gained a new `State ID` field (`VarInt`). The client echoes this value in `CONTAINER_CLICK` to let the server detect desync.
- `CONTAINER_SET_SLOT`: state ID inserted between container ID and slot ID.
- `CONTAINER_SET_CONTENT`: state ID inserted after container ID; item array length encoding changed from `Short` to `VarInt`; a new `Carried item` field (the cursor item, `Slot`) was appended.
Sources: `v1_17to1_17_1/rewriter/ItemPacketRewriter1_17_1.java:4765` (writes `Types.VAR_INT, 0` as placeholder state ID); commit `ddbe8ef0f`.
### CONTAINER_CLICK — State ID consumed
Serverbound `CONTAINER_CLICK` (0x08) gained a `State ID` VarInt field (read and discarded by ViaVersion when forwarding to older servers).
Sources: `v1_17to1_17_1/rewriter/ItemPacketRewriter1_17_1.java:6783`.
### EDIT_BOOK — Format restructured
Serverbound `EDIT_BOOK` (0x0B) changed format: previously an item (writable book with NBT pages), now sends pages as a raw `VarInt` count + `String[]` list plus an optional `Boolean`-gated title. ViaVersion reconstructs the old NBT item format from the new wire format.
Sources: `v1_17to1_17_1/Protocol1_17To1_17_1.java:5395`; commit `ddbe8ef0f`.
### Enchantment level cap exposed
Items with enchantment levels outside [0, 255] were crashing 1.17 clients. 1.17.1 caps display. ViaVersion's downgrade handler hides out-of-range enchantments in lore when translating to older clients.
Sources: `v1_17to1_17_1/rewriter/ItemPacketRewriter1_17_1.java:88103`; commit `2c30a2748`.
---
## Proxy / Translation Impact
**For ViaVersion (older client → 1.17+ server):**
1. **Entity type ID remapping** — every `ADD_ENTITY`/`ADD_MOB` entity type ID must be remapped. Proxies must maintain a full 1.16↔1.17 entity type translation table. All IDs ≥ 3 shifted.
2. **Packet fan-out**`SET_TITLES`, `SET_BORDER`, `PLAYER_COMBAT` must be split from the old multiplexed format on downgrade (1.17 server → 1.16 client) or collapsed on upgrade.
3. **REMOVE_ENTITIES ↔ REMOVE_ENTITY** — single-ID vs array format must be translated in both directions. Additionally 1.17.1 reverted back to array, so the proxy must handle three forms across the 755→756 boundary.
4. **Dimension codec injection**`min_y` and `height` fields must be injected for 1.17-aware clients even when the server runs 1.16. ViaVersion injects `{min_y: 0, height: 256}` unconditionally.
5. **Chunk format** — 1.17 chunk type uses BitSet masks; a proxy bridging a 1.16 server must rewrite the chunk bitmask from `Int` to `Long[]` and vice versa. Non-full chunks from 1.16 servers must be converted to multi-block-change sequences.
6. **Light update** — mask encoding (VarInt → Long[] BitSet) must be translated.
7. **Tags** — the generic resource-location prefix and game-event registry type must be stripped/injected when bridging 1.16↔1.17.
8. **RESOURCE_PACK**`Required` + `Prompt` fields must be added (or stripped) at the boundary.
9. **Entity data index 7 (Ticks Frozen)** — must be injected (as 0) for all entities when serving 1.17 clients from a 1.16 source, and stripped in the other direction.
10. **State IDs (1.17.1)**`CONTAINER_SET_SLOT` / `CONTAINER_SET_CONTENT` / `CONTAINER_CLICK` all carry an extra VarInt state ID in 1.17.1 that must be stripped (downstream) or injected as 0 (upstream) when crossing the 755/756 boundary.
ViaVersion commit highlights for this boundary:
- `aa22ca1d5` — item-frame pitch/yaw→data rotation change
- `27033e929` — drop CONTAINER_ACK (transaction) tracking
- `501f65e21` — Mojang-mapped packet/entity type renames
- `ddbe8ef0f` — CONTAINER_SET_CONTENT/SLOT state IDs (1.17.1)
- `2c30a2748` — out-of-bounds enchantment lore display (1.17.1)
+393
View File
@@ -0,0 +1,393 @@
# Java Edition 1.18.x — Protocol Deep-Dive
| Release | Protocol # | Release date | minecraft-data dir | ViaVersion package (into this version) |
|---|---|---|---|---|
| 1.18 | **757** | 2021-11-30 | `data/pc/1.18` | `v1_17_1to1_18` |
| 1.18.1 | **757** | 2021-12-10 | `data/pc/1.18.1` | *(same protocol; no VV package)* |
| 1.18.2 | **758** | 2022-02-28 | `data/pc/1.18.2` | `v1_18to1_18_2` |
Protocol numbers confirmed from minecraft-data `version.json` for each dir (`1.18`→757, `1.18.1`→757, `1.18.2`→758) and ViaVersion source. **1.18 and 1.18.1 share protocol 757** — 1.18.1 was a critical security fix (Log4j RCE, fog) that did not touch the wire format. Release dates from minecraft.wiki release articles (fetched 2026-06-19): [1.18](https://minecraft.wiki/w/Java_Edition_1.18), [1.18.1](https://minecraft.wiki/w/Java_Edition_1.18.1), [1.18.2](https://minecraft.wiki/w/Java_Edition_1.18.2). ViaVersion source at `/tmp/mcproto-refs/ViaVersion/`.
---
## Headline — Caves & Cliffs Part II: world height expansion
1.18 ("**Caves & Cliffs Part II**", 2021-11-30) completed the biome and terrain overhaul begun in 1.17. The defining change is **world height expansion**: the overworld now spans **Y = 64 to Y = 319** (384 blocks total), up from 0255 (256 blocks). This is not just a gameplay detail — it fundamentally restructures the chunk data packet:
- The chunk **now carries 24 sections** (384 ÷ 16) instead of 16.
- **Biome data moved out of the flat array at chunk level** and into a **per-section paletted container** alongside block states, matching the per-section structure of block data. In 1.17 biomes were a flat `varint[]` at the top of the chunk packet; in 1.18 each section contains its own 4×4×4 biome palette.
- The **Light Update and Chunk Data packets were merged** into a single `LEVEL_CHUNK_WITH_LIGHT` (0x22), collapsing what had been two separate packets into one.
- The **section presence bitmask was removed**; all sections are always serialised, even if empty (filled with the single-value palette of air).
The actual *number* of packet types is nearly unchanged from 1.17.1 — one new packet (`SET_SIMULATION_DISTANCE`) and one merge (`LEVEL_CHUNK_WITH_LIGHT`) are the whole structural delta. The heavy cost is in the **chunk encoding / decoding work**, not in new protocol concepts.
Source: minecraft.wiki [Java Edition 1.18](https://minecraft.wiki/w/Java_Edition_1.18) (fetched 2026-06-19); ViaVersion `v1_17_1to1_18/`; minecraft-data `data/pc/1.17.1` vs `data/pc/1.18`.
---
## Protocol changes: 1.17.1 (756) → 1.18 (757)
### New packet: SET_SIMULATION_DISTANCE (0x57)
1.18 introduced a distinction between **view distance** (chunks loaded around the player) and **simulation distance** (chunks in which game logic runs). A new `SET_SIMULATION_DISTANCE` packet carries a single `varint distance` field.
minecraft-data confirms: `packet_simulation_distance` exists in `1.18` types but not in `1.17.1``data/pc/1.18/protocol.json` play toClient types. This insertion at 0x57 pushed all subsequent clientbound IDs up by one: the IDs from 0x57 onward in 1.18 are one higher than in 1.17.1.
```
// SET_SIMULATION_DISTANCE (0x57, clientbound, Play)
VarInt simulationDistance
```
ViaVersion: `Protocol1_17_1To1_18.java` line 5775 (serverbound `CLIENT_INFORMATION` handler) synthesises the simulation distance from view distance since the older client doesn't send it. `EntityPacketRewriter1_18.java` line 5758 writes `simulationDistance` (duplicating the view-distance value) into the `LOGIN` packet.
Source: `v1_17_1to1_18/Protocol1_17_1To1_18.java:63-75`; `v1_17_1to1_18/rewriter/EntityPacketRewriter1_18.java:55-59`; minecraft-data `data/pc/1.18/protocol.json`.
### Serverbound CLIENT_INFORMATION: new field + rename
The `CLIENT_SETTINGS` serverbound packet gained one field and had one renamed:
| Field | 1.17.1 | 1.18 |
|---|---|---|
| locale (string) | ✓ | ✓ |
| viewDistance (i8) | ✓ | ✓ |
| chatFlags (varint) | ✓ | ✓ |
| chatColors (bool) | ✓ | ✓ |
| skinParts (u8) | ✓ | ✓ |
| mainHand (varint) | ✓ | ✓ |
| `disableTextFiltering` (bool) | ✓ (inverted name) | — |
| `enableTextFiltering` (bool) | — | ✓ (renamed) |
| `enableServerListing` (bool) | — | **added** |
Source: minecraft-data `data/pc/1.17.1/protocol.json` vs `data/pc/1.18/protocol.json` `packet_settings`; ViaVersion `Protocol1_17_1To1_18.java:63-75` strips `enableServerListing` when translating 1.18 clients toward a 1.17 server (the `read(Types.BOOLEAN)` at line 73 drops it).
### BLOCK_ENTITY_DATA: action field type change (u8 → varint)
The `BLOCK_ENTITY_DATA` clientbound packet changed its `action` field type from `unsigned byte` to `varint`:
```
// BLOCK_ENTITY_DATA (0x0A, clientbound, Play) — 1.17.1
BlockPos location
u8 action // <-- byte
OptNBT nbtData
// BLOCK_ENTITY_DATA (0x0A, clientbound, Play) — 1.18
BlockPos location
VarInt action // <-- varint
OptNBT nbtData
```
Source: minecraft-data `data/pc/1.17.1/protocol.json` vs `data/pc/1.18/protocol.json` `packet_tile_entity_data`; ViaVersion `WorldPacketRewriter1_18.java:56-64` reads the incoming 1.17.1 `UNSIGNED_BYTE` and writes `VAR_INT`.
### Chunk packet restructure: LEVEL_CHUNK_WITH_LIGHT (0x22)
This is the core change. In 1.17.1 there were two separate packets:
- `LEVEL_CHUNK` — chunk data (block states + biomes + block entities, no light)
- `LIGHT_UPDATE` — light data for a chunk column (separate timing)
In 1.18 these are **merged** into `LEVEL_CHUNK_WITH_LIGHT` (0x22). The light data is appended directly after the chunk section data within the same packet. This is why ViaVersion must **buffer the LIGHT_UPDATE**, cache it keyed by (chunkX, chunkZ), then emit it when the corresponding `LEVEL_CHUNK` arrives (or vice versa — see `ChunkLightStorage`).
**Wire layout of `LEVEL_CHUNK_WITH_LIGHT` (1.18, 0x22):**
```
Int chunkX
Int chunkZ
NBT heightMaps
VarInt dataLength // byte count of the sections buffer
[sections] <ySectionCount sections, each:>
Short nonAirBlocksCount
[blockPalette] paletted container (blocks)
[biomePalette] paletted container (biomes) // NEW in 1.18
VarInt blockEntityCount
[blockEntities] <each: packed_xz (u8) + y (i16) + typeId (varint) + NBT>
// Light data (was separate LIGHT_UPDATE in 1.17.1):
Boolean trustEdges
LongArray skyLightMask
LongArray blockLightMask
LongArray emptySkyLightMask
LongArray emptyBlockLightMask
VarInt skyLightArrayCount
[byte[2048]] per section
VarInt blockLightArrayCount
[byte[2048]] per section
```
Source: `ChunkType1_18.java:49-67` (read), `ChunkType1_18.java:71-86` (write); `ChunkSectionType1_18.java:51-54` (section read); `WorldPacketRewriter1_18.java:105-196`.
**Comparison with 1.17.1 LEVEL_CHUNK wire layout:**
```
Int chunkX
Int chunkZ
LongArray sectionsMask // which of the 16 sections are present
NBT heightMaps
VarInt[] biomeData // flat array (1024 ints = 4×4×4 × 16 sections) -- GONE in 1.18
VarInt dataLength
[sections] only sections where bit set in mask:
Short nonAirBlocksCount
[blockPalette] paletted container (blocks only; no biome palette here)
NBT[] blockEntities // full NBT per entity -- changed in 1.18
```
Source: `ChunkType1_17.java:49-72` (read).
**Key structural differences:**
| Aspect | 1.17.1 (756) | 1.18 (757) |
|---|---|---|
| World height | Y 0255, 16 sections | Y 64319, 24 sections |
| Section presence | bitmask (BitSet from LongArray) | **all sections always present** |
| Biome data | flat `VarInt[]` of 1024 at chunk level | **per-section 4×4×4 paletted container** |
| Block entities | full NBT array | packed `xz (u8) + y (i16) + typeId (varint) + NBT` |
| Light | separate `LIGHT_UPDATE` packet | **merged into chunk packet** |
| Section count | 16 (overworld) | **24 (overworld, +2 for border sections in light)** |
Source: `ChunkType1_17.java`, `ChunkType1_18.java`, `ChunkSectionType1_18.java`; ViaVersion `v1_17_1to1_18/`.
### Biome palette (per-section, 4×4×4)
In 1.18 each section in the chunk carries its own biome data as a **paletted container** using the same palette encoding as block states. The container covers a 4×4×4 sub-grid (64 biome cells per section), using the `PaletteType.BIOMES` type with `ChunkSection.BIOME_SIZE = 64`.
The palette format is identical to block palettes (`PaletteType1_18`):
- **bits-per-value = 0** → single-value palette (one VarInt entry, zero-length data array) — used when an entire section is one biome
- **13 bpv** → indirect palette (VarInt array of biome IDs + compact long array)
- **direct/global** → compact long array, no palette array
The `highestBitsPerValue` for biomes is 3 in 1.18 (global palette otherwise). <!-- VERIFY: exact threshold for global biome palette bits in 1.18 -->
Source: `PaletteType1_18.java:38-171`; `ChunkSection.java:37` (`BIOME_SIZE = 4*4*4`); `PaletteType.java:27` (`BIOMES(ChunkSection.BIOME_SIZE, 3)`); `WorldPacketRewriter1_18.java:146-155` (ViaVersion filling biome palette from old flat array).
### World height in the dimension type NBT
The server communicates world height to clients via the **dimension type NBT** sent in the `LOGIN` and `RESPAWN` packets. The dimension type includes two fields:
- `height` (int) — total block height of the dimension
- `min_y` (int) — minimum Y coordinate (negative for the expanded overworld: 64)
ViaVersion reads both to determine section count and minimum Y:
```java
// EntityRewriter.java:512-526
CompoundTag registryData = wrapper.get(Types.NAMED_COMPOUND_TAG, nbtIndex);
NumberTag height = registryData.getNumberTag("height");
// height >> 4 = section count (e.g. 384 >> 4 = 24)
tracker.setCurrentWorldSectionHeight(blockHeight >> 4);
NumberTag minY = registryData.getNumberTag("min_y");
tracker.setCurrentMinY(minY.asInt()); // -64 for overworld
```
The section count `tracker.currentWorldSectionHeight()` is then passed to `ChunkType1_17` / `ChunkType1_18` constructors at runtime so chunk reading is dimension-aware — not hardcoded to 24.
Source: `EntityRewriter.java:508-531`; `EntityPacketRewriter1_18.java:43-68` (LOGIN handler calling `worldDataTrackerHandler(1)`).
### Light update caching
Because 1.17.1 sends light and chunk data as separate packets, ViaVersion must buffer one while waiting for the other. The `ChunkLightStorage` per-connection object:
- On `LIGHT_UPDATE` for an **unloaded chunk**: cancels the packet (does not forward it) and caches the light data keyed by `(chunkX, chunkZ)`.
- On `LIGHT_UPDATE` for an **already-loaded chunk**: passes through (and optionally caches, controlled by `cache-1_17-light` config).
- On `LEVEL_CHUNK`: looks up the cached light, appends it to the merged `LEVEL_CHUNK_WITH_LIGHT`, then removes it from the cache.
- On `FORGET_LEVEL_CHUNK`: clears both the cached light and the loaded-chunk marker.
Source: `ChunkLightStorage.java:28-68`; `WorldPacketRewriter1_18.java:66-103` (LIGHT_UPDATE handler), `105-196` (LEVEL_CHUNK → LEVEL_CHUNK_WITH_LIGHT).
### LOGIN packet: simulationDistance added
The `LOGIN` clientbound packet gained one field between 1.17.1 and 1.18:
```
// 1.17.1 LOGIN (0x26) // 1.18 LOGIN (0x26)
Int entityId Int entityId
Bool isHardcore Bool isHardcore
Byte gameMode Byte gameMode
Byte previousGameMode Byte previousGameMode
String[] worldNames String[] worldNames
NBT dimensionCodec NBT dimensionCodec
NBT dimension NBT dimension
String worldName String worldName
Long hashedSeed Long hashedSeed
VarInt maxPlayers VarInt maxPlayers
VarInt viewDistance VarInt viewDistance
VarInt simulationDistance // NEW
Bool reducedDebugInfo Bool reducedDebugInfo
Bool enableRespawnScreen Bool enableRespawnScreen
Bool isDebug Bool isDebug
Bool isFlat Bool isFlat
```
Source: minecraft-data `data/pc/1.17.1/protocol.json` vs `data/pc/1.18/protocol.json` `packet_login`; `EntityPacketRewriter1_18.java:43-68`.
### Particle: barrier (id 2) → block_marker (id 3)
A small entity-data / particle change: the `barrier` particle (ID 2 in 1.17) became the `block_marker` particle in 1.18 with an embedded block state, and what was `block_marker` (ID 3) got a different ID. ViaVersion maps these:
```java
// EntityPacketRewriter1_18.java:86-92
if (particle.id() == 2) { // Barrier
particle.setId(3); // Block marker
particle.add(Types.VAR_INT, 7754); // Barrier block state
} else if (particle.id() == 3) { // Light block
particle.add(Types.VAR_INT, 7786); // Light block state
}
```
Source: `v1_17_1to1_18/rewriter/EntityPacketRewriter1_18.java:85-93`.
### Tag renames
One block tag was renamed between 1.17 and 1.18:
- `minecraft:lava_pool_stone_replaceables``minecraft:lava_pool_stone_cannot_replace`
Source: `Protocol1_17_1To1_18.java:82` (`tagRewriter.renameTag`).
---
## 757 — 1.18 (2021-11-30) and 1.18.1 (2021-12-10)
**1.18** shipped on 2021-11-30. It is the full Caves & Cliffs Part II release: expanded world height, noise caves, the new terrain generator, new mountain sub-biomes (Meadow, Grove, Snowy Slopes, Jagged Peaks, Frozen Peaks, Stony Peaks), and the 3D biome system decoupled from terrain generation. All protocol changes vs 1.17.1 are documented in the section above.
**1.18.1** shipped 2021-12-10, ten days later, as an emergency patch addressing:
- Critical **Log4j2 RCE vulnerability** (CVE-2021-44228 / Log4Shell) exploitable via in-game chat messages logged by Log4j on the server side
- Fog rendering change: fog now applies cylindrically (not spherically) and starts farther from the player
- Eight bug fixes
**Protocol 757 is unchanged between 1.18 and 1.18.1.** The Log4j fix is entirely server-side (library update); no wire-format changes. There is no ViaVersion translation package between 1.18 and 1.18.1.
Source: minecraft.wiki [Java Edition 1.18.1](https://minecraft.wiki/w/Java_Edition_1.18.1) (fetched 2026-06-19); minecraft-data `data/pc/1.18.1/version.json` (`version: 757`).
---
## 758 — 1.18.2 (2022-02-28)
1.18.2 shipped 2022-02-28, bumping the protocol from 757 to **758**. The gameplay additions are modest (custom world-gen tag expansion, `/placefeature` command, new structure and biome tags), and the **protocol wire changes are minimal**:
### Effect ID type: byte → varint (UPDATE_MOB_EFFECT and REMOVE_MOB_EFFECT)
The only structural packet change from 1.18 to 1.18.2 is that the `effectId` field in two packets changed type from `byte` (i8) to `varint`:
**UPDATE_MOB_EFFECT (0x65 in 1.18):**
```
// 1.18 (757) // 1.18.2 (758)
VarInt entityId VarInt entityId
byte effectId // <-- i8 VarInt effectId // <-- varint
byte amplifier byte amplifier
VarInt duration VarInt duration
byte hideParticles byte hideParticles
```
**REMOVE_MOB_EFFECT (0x3B in 1.18):**
```
// 1.18 (757) // 1.18.2 (758)
VarInt entityId VarInt entityId
byte effectId // <-- i8 VarInt effectId // <-- varint
```
ViaVersion maps both in `Protocol1_18To1_18_2.java`:
```java
// Protocol1_18To1_18_2.java:45-58
registerClientbound(ClientboundPackets1_18.UPDATE_MOB_EFFECT, new PacketHandlers() {
public void register() {
map(Types.VAR_INT); // Entity id
map(Types.BYTE, Types.VAR_INT); // Effect id byte→varint
}
});
registerClientbound(ClientboundPackets1_18.REMOVE_MOB_EFFECT, new PacketHandlers() {
public void register() {
map(Types.VAR_INT); // Entity id
map(Types.BYTE, Types.VAR_INT); // Effect id byte→varint
}
});
```
Source: `v1_18to1_18_2/Protocol1_18To1_18_2.java:45-58`; minecraft-data `data/pc/1.18/protocol.json` vs `data/pc/1.18.2/protocol.json` `packet_entity_effect` + `packet_remove_entity_effect`.
### LOGIN / RESPAWN: `infiniburn` tag prefix added
Dimension type NBT now requires the `infiniburn` block tag to be prefixed with `#`:
```java
// Protocol1_18To1_18_2.java:86-91
private void addTagPrefix(CompoundTag tag) {
final Tag infiniburnTag = tag.get("infiniburn");
if (infiniburnTag instanceof final StringTag infiniburn) {
infiniburn.setValue("#" + infiniburn.getValue());
}
}
```
This is applied to all dimension entries in the registry NBT (in `LOGIN`) and to the current dimension NBT in `RESPAWN`. The `#` prefix marks it as a tag reference rather than a direct block ID.
Source: `v1_18to1_18_2/Protocol1_18To1_18_2.java:61-91`.
### Tags: `fall_damage_resetting` block tag added
A new block tag `minecraft:fall_damage_resetting` is injected into `UPDATE_TAGS` with a fixed list of block IDs:
```java
// Protocol1_18To1_18_2.java:42
tagRewriter.addTagRaw(RegistryType.BLOCK, "minecraft:fall_damage_resetting",
169, 257, 680, 713, 714, 715, 716, 859, 860, 696, 100);
```
Source: `v1_18to1_18_2/Protocol1_18To1_18_2.java:41-43`.
### No packet additions or removals in 758
Minecraft-data confirms: the clientbound packet list in `data/pc/1.18/protocol.json` and `data/pc/1.18.2/protocol.json` are identical in packet names and IDs. The serverbound list is also unchanged. The protocol bump is driven by the effect-ID type change alone (plus the NBT / tag-data changes that don't alter packet structure).
---
## Proxy / translation impact
### What proxies must do for height-aware chunk translation
A proxy bridging 1.17.1 clients to a 1.18 server (or vice-versa) faces the most expensive chunk translation in the 1.17/1.18 era:
1. **Section count change**: must expand/shrink the section array from 16 to 24 (or back). For 1.17.1 clients receiving a 1.18 chunk, the 8 extra sections (Y = 64 to 1) are simply not representable — ViaVersion does not send those sections to old clients; they appear as void. <!-- VERIFY: exact ViaVersion handling of below-zero sections for 1.17.1 clients -->
2. **Biome re-encoding**: The 1.17.1 biome format is a flat `VarInt[]` of 1024 entries (4×4×4 per section × 16 sections). The 1.18 format is per-section paletted containers. ViaVersion's conversion:
- Receiving a 1.17.1 chunk: reads the flat biome array, slices it into 16-entry groups (one per section × 64 biome cells), and creates a `DataPaletteImpl` per section — `WorldPacketRewriter1_18.java:146-155`.
- The 8 extra sections for Y < 0 are padded with biome ID 0 (fallback for invalid/missing): `biome = biomeData[biomeArrayIndex]; biomePalette.setIdAt(biomeIndex, biome != -1 ? biome : 0)`.
3. **Light buffering**: Two packets become one. The proxy must buffer light packets until the corresponding chunk packet arrives (or vice-versa), then emit the merged `LEVEL_CHUNK_WITH_LIGHT`. ViaVersion's `ChunkLightStorage` implements this — `ChunkLightStorage.java:28-68`, `WorldPacketRewriter1_18.java:66-103`.
4. **Block entity format**: 1.17.1 sends block entities as raw NBT with `x`/`y`/`z`/`id` fields inside. 1.18 sends a packed struct: `packedXZ (u8) = (x & 15) << 4 | (z & 15)`, `y (i16)`, `typeId (varint)`. ViaVersion converts: `WorldPacketRewriter1_18.java:109-128`.
5. **Section presence bitmask**: In 1.17.1 null sections are skipped via bitmask; in 1.18 every section must be present in the serialised form (using single-value palette for air). ViaVersion fills null sections: `WorldPacketRewriter1_18.java:133-143`.
6. **`simulationDistance` injection**: When a 1.18 server sends `LOGIN`, ViaVersion injects a `simulationDistance` field (copied from `viewDistance`) for old 1.17.1 clients that don't know about it — `EntityPacketRewriter1_18.java:57-58`. In the reverse direction (serverbound), `CLIENT_INFORMATION` from a 1.17.1 client drops the `enableServerListing` bool that 1.18 expects — `Protocol1_17_1To1_18.java:63-74`.
Source: all paths above in `v1_17_1to1_18/`.
### 757 → 758 proxy impact (minimal)
For a proxy bridging protocol 757 to 758:
- **Effect ID**: remap `effectId` in `UPDATE_MOB_EFFECT` and `REMOVE_MOB_EFFECT` between i8 and varint. Simple field-type swap, no data loss (effect IDs fit in a byte).
- **NBT prefix**: add/strip `#` on the `infiniburn` string tag in `LOGIN` registry and `RESPAWN` dimension NBT.
- **Tags**: inject or strip the `fall_damage_resetting` block tag.
- No chunk re-encoding needed; chunk format is identical between 757 and 758.
Source: `v1_18to1_18_2/Protocol1_18To1_18_2.java:39-91`.
---
## ViaVersion git log summary
```
# v1_17_1to1_18 package (selected):
e15fc5953 Trim slightly more fastutil, use its interfaces for the long collections
8ee5d7e68 Add missing biome name translations in 1.17.1->1.18 (#4597)
12c773ede Add missing translatable mappings (#4542)
b8a170873 Add more info to missing light data warning
c13b40a37 Add ParticleRewriter base (#4203)
bd4df2813 Refactor protocols to match template module (#3842)
5286efde1 Move type instances out of its enclosing class
# v1_18to1_18_2 package (selected):
cff9a8715 [ci skip] Update copyright header
91f31b578 Clean up tags rewriting (#3856)
501f65e21 Packet and entity type renames
e965e9713 Package/class renames and moves
```
Source: `git -C /tmp/mcproto-refs/ViaVersion log --oneline -- common/.../v1_17_1to1_18/` and `common/.../v1_18to1_18_2/`, each `| head -20`.
+278
View File
@@ -0,0 +1,278 @@
# Java Edition 1.19.x — Protocol Deep-Dive
| Release | Protocol # | Release date | minecraft-data dir | ViaVersion package (into this version) |
|---|---|---|---|---|
| 1.19 | **759** | 2022-06-07 | `data/pc/1.19` | `v1_18_2to1_19` |
| 1.19.1 | **760** | 2022-07-27 | *(no separate dir; = 1.19.2)* | `v1_19to1_19_1` |
| 1.19.2 | **760** | 2022-08-05 | `data/pc/1.19.2` | `v1_19to1_19_1` |
| 1.19.3 | **761** | 2022-12-07 | `data/pc/1.19.3` | `v1_19_1to1_19_3` |
| 1.19.4 | **762** | 2023-03-14 | `data/pc/1.19.4` | `v1_19_3to1_19_4` |
Protocol numbers cross-checked against minecraft-data `protocolVersions.json` (`1.19`→759 dataVersion 3105, `1.19.1`→760 dv 3117, `1.19.2`→760 dv 3120, `1.19.3`→761 dv 3218, `1.19.4`→762 dv 3337) and each dir's `version.json`. **1.19.1 and 1.19.2 share protocol 760** — the wire format is identical; 1.19.2 was a same-week bug-fix that did not bump the protocol. Release dates from minecraft.wiki release articles (fetched 2026-06-19): [1.19](https://minecraft.wiki/w/Java_Edition_1.19), [1.19.1](https://minecraft.wiki/w/Java_Edition_1.19.1), [1.19.3](https://minecraft.wiki/w/Java_Edition_1.19.3), [1.19.4](https://minecraft.wiki/w/Java_Edition_1.19.4). ViaVersion source at `/tmp/mcproto-refs/ViaVersion/`.
---
## Headline — The Wild Update + Secure Chat
1.19 ("**The Wild Update**", 2022-06-07) shipped the Deep Dark biome + Warden, Ancient Cities, the Mangrove Swamp, and the Allay/Frog mobs ([minecraft.wiki/w/Java_Edition_1.19](https://minecraft.wiki/w/Java_Edition_1.19), fetched 2026-06-19). But on the wire, **the dominant story of the whole 1.19 line is chat signing / Secure Chat** — not gameplay. The wiki states plainly that as of 1.19, "Chat messages between players, as well as chat from the `/say`, `/msg`, `/teammsg`, and `/me` commands, are now cryptographically signed", and players receive a Mojang-provided key pair on startup ([1.19 article](https://minecraft.wiki/w/Java_Edition_1.19), fetched 2026-06-19).
The reason this release line carries **four protocol numbers** (759 → 760 → 761 → 762) is that the signing design **churned three times**:
- **759 (1.19):** v1 — client signs each chat message with a per-message signature, key delivered in Login Start; signed/unsigned content split; chat preview.
- **760 (1.19.1/1.19.2):** v2 — the **player-reporting** rework. A signature *chain* is introduced (each message signs over the preceding signature + a set of last-seen message acknowledgements), so message order is cryptographically verifiable for reports.
- **761 (1.19.3):** **partial revert + re-architecture.** Chat preview removed entirely; per-message "header" packets gone; the chain is rebuilt around a per-session ID + monotonic message index (`MessageLink`); system messages are no longer in the signed path; signing becomes more conditional on `enforce-secure-profile`. The disguised-chat packet is introduced for server-authored messages that need chat-type decoration without a signature.
- **762 (1.19.4):** refinements only — no change to the signing payload; clients reset their secure-chat session state on the Login (Play) packet.
This is the most complicated signing story in the protocol's history; the per-patch sub-sections below document each model precisely from ViaVersion source.
---
## Login-phase: the session profile public key (759760 only)
Secure Chat needs the client to prove ownership of a Mojang-signed key pair. In **1.191.19.2** that key is delivered **during the Login state**, appended to **Login Start (Serverbound `HELLO`)**, and it is what later chat-message signatures are verified against. This is the protocol's first use of a player-supplied public key in login. Full field layout and the Encryption-Response salt/signature variant are in [../05-login-encryption.md](../05-login-encryption.md) §2 and §5; summarised here for the signing context:
- **Login Start gains an optional `Signature` (profile key) container** (1.191.19.2): `Timestamp` (i64 key-expiry), `Public Key` (DER `SubjectPublicKeyInfo`), and a Mojang `Signature` over them. ViaVersion's type is `Types.OPTIONAL_PROFILE_KEY`. In 1.19.1+ Login Start also carries an optional `Player UUID`.
- `v1_18_2to1_19/Protocol1_18_2To1_19.java:278-284` — serverbound `HELLO` maps the name and **reads-and-discards** `OPTIONAL_PROFILE_KEY` (ViaVersion's own clients downstream don't sign).
- `v1_19to1_19_1/Protocol1_19To1_19_1.java:231-248` — serverbound `HELLO` reads the incoming profile key but **substitutes ViaVersion's own `ChatSession1_19_0.getProfileKey()`** when a chat session is configured, then reads the optional UUID (`Types.OPTIONAL_UUID`).
- **Encryption Response salt/signature variant** (1.191.19.2): when the client holds a profile key it replies to Encryption Request with `salt + signature` instead of the encrypted verify-token nonce; the server verifies it against the profile key. ViaVersion swaps this back to a plain nonce when its downstream client has no key:
- `v1_18_2to1_19/Protocol1_18_2To1_19.java:286-307` (the salt branch, `// 🧂`).
- `v1_19to1_19_1/Protocol1_19To1_19_1.java:264-283`.
- **761 (1.19.3) moves the key out of Login Start.** The profile key is no longer sent at login; instead the *active* key is sent in-Play via the new `CHAT_SESSION_UPDATE` packet (see §761). At login 1.19.3 only carries name + optional UUID:
- `v1_19_1to1_19_3/Protocol1_19_1To1_19_3.java:284-294` — serverbound `HELLO` writes `OPTIONAL_PROFILE_KEY` from the chat session (or null) but the *client* no longer supplies one; `:295-322` handles the Encryption-Response salt/signature using the in-Play `ChatSession1_19_1` key.
> Cross-reference: [../05-login-encryption.md](../05-login-encryption.md) lines 3553 (Login Start `Signature` container, present 1.191.19.2, removed 1.19.3) and 100123 (Encryption Response profile-key form).
---
## 759 — 1.19 (2022-06-07): chat signing v1
**Model.** Each player chat message is individually signed by the client. There is **no chain** yet: the signature covers a fixed-width metadata block plus the (canonicalised) decorated message JSON. The server delivers player chat to recipients in a dedicated, signed `PLAYER_CHAT` packet distinct from `SYSTEM_CHAT`.
**Signature payload (1.19, `ChatSession1_19_0`).** `v1_18_2to1_19` is a *down*-translation (1.18.2-side ↔ 1.19-side), and ViaVersion's own clients don't sign, so the canonical signing payload lives in `ChatSession1_19_0.signChatMessage`:
```java
// api/.../signature/storage/ChatSession1_19_0.java:42-51
byte[] data = new byte[32]; // big-endian
buffer.putLong(metadata.salt()); // 8B salt
buffer.putLong(sender.MSB).putLong(sender.LSB); // 16B sender UUID
buffer.putLong(metadata.timestamp().getEpochSecond); // 8B timestamp (seconds)
signer.accept(data);
signer.accept(GsonUtil.sort(content.decorated()) // canonical-sorted JSON of
.toString().getBytes(UTF_8)); // the decorated component
```
So the v1 signed bytes are `salt(8) ‖ senderUUID(16) ‖ timestampSeconds(8) ‖ sortedDecoratedJSON`, signed `SHA256withRSA` with the player's private key.
**Serverbound chat packets (1.19, `ServerboundPackets1_19`):**
| Packet | ID | Fields (1.19) |
|---|---|---|
| `CHAT_COMMAND` | 0x03 | command String, timestamp i64, salt i64, VarInt array of `{argumentName, signature[]}`, `signedPreview` bool |
| `CHAT` | 0x04 | message String, timestamp i64, salt i64, signature byte[], `signedPreview` bool |
| `CHAT_PREVIEW` | 0x05 | preview request (queryId + text) |
Source: `v1_18_2to1_19/packet/ServerboundPackets1_19.java:27-29`. ViaVersion (translating *up* from a 1.18 server that has no signing) simply **reads and drops** all the signing fields and **cancels `CHAT_PREVIEW`**: `Protocol1_18_2To1_19.java:225-254`.
**Clientbound chat packets (1.19, `ClientboundPackets1_19`):**
| Packet | ID | Notes |
|---|---|---|
| `CHAT_PREVIEW` | 0x0C | server-side preview response |
| `PLAYER_CHAT` | 0x30 | signed player message: `signedContent` (Component), `unsignedContent` (opt Component), chat-type VarInt, sender UUID, sender name, team name, **timestamp i64, salt i64, signature byte[]** |
| `SERVER_DATA` | 0x3F | MOTD/icon/`previewsChat` |
| `SET_DISPLAY_CHAT_PREVIEW` | 0x4B | toggles preview UI |
| `SYSTEM_CHAT` | 0x5F | unsigned system/overlay message (Component + type VarInt) |
Source: `v1_18_2to1_19/packet/ClientboundPackets1_19.java:36,72,87,99,119`. Note the **signed/unsigned content split** in `PLAYER_CHAT` — a 1.19 novelty: the server can show an unsigned, server-decorated rendering while still carrying the original signed text for reporting.
ViaVersion from a 1.18 server: there is no signed player chat to translate *up*, so 1.18's `CHAT` (0x0F) is rewritten into `SYSTEM_CHAT` (0x5F) — *every* incoming message becomes a system message ("we don't want to analyze and remove player names"), which sidesteps signing entirely: `Protocol1_18_2To1_19.java:212-223`.
**ViaVersion package/commits.** Package `common/.../protocols/v1_18_2to1_19/` (`Protocol1_18_2To1_19.java` + `packet/{Clientbound,Serverbound}Packets1_19.java` + `provider/AckSequenceProvider.java` + `storage/{NonceStorage1_19,SequenceStorage,DimensionRegistryStorage}.java`). `git log --oneline -- common/.../v1_18_2to1_19` (top relevant): `ab3927dff` "Implement our own hash writing", `32e51b52a` "Cleanup LOGIN/STATUS packet handlers (#4113)", `c5756fe45` "Rename Position to BlockPosition", `501f65e21` "Packet and entity type renames", `e965e9713` "Package/class renames and moves". <!-- VERIFY: no surviving commit subject names the original 759 chat-signing implementation; it predates the renames captured in this shallow-unshallowed log -->
---
## 760 — 1.19.1 / 1.19.2 (2022-07-27 / 2022-08-05): player reporting + signature chain v2
**Headline.** 1.19.1 added the **player-reporting** system: players can report abusive chat, multiple messages per report, with categories (harassment, hate speech, etc.); reported players can be banned from online play/Realms after review. Messages that are unsigned or server-tampered are now flagged "Not Secure"/"Modified". `enforce-secure-profile` now **defaults to true** on dedicated servers. Crucially: "The order of chat messages are now cryptographically verified" — the chain. ([minecraft.wiki/w/Java_Edition_1.19.1](https://minecraft.wiki/w/Java_Edition_1.19.1), fetched 2026-06-19.) **1.19.2 is wire-identical (still protocol 760)** — a bug-fix that did not change the format.
**Model — the v2 signature chain (`ChatSession1_19_1`).** Each message now signs over the **preceding message's signature** (the "header") plus the body (which itself folds in **last-seen message acknowledgements**). This is what makes message *order* tamper-evident for reporting.
```java
// api/.../signature/storage/ChatSession1_19_1.java:43-52
MessageHeader header = new MessageHeader(this.precedingSignature, sender);
MessageBody body = new MessageBody(content, timestamp, salt, lastSeenMessages);
header.update(signer); // preceding-sig ‖ senderUUID
body.update(signer); // SHA-256 hash of body, see below
this.precedingSignature = signature; // chain advances
```
Header bytes (`chain/v1_19_1/MessageHeader.java:38-44`): `precedingSignature (if present) ‖ senderUUID`.
Body — the body is **SHA-256-hashed first**, then the digest is fed to the signer (`chain/v1_19_1/MessageBody.java:53-75`). The pre-hash buffer is:
```
salt(i64) ‖ timestampSeconds(i64) ‖ plainContent(UTF-8) ‖ 0x46
‖ [if decorated] sortedDecoratedJSON(UTF-8)
‖ for each lastSeenMessage: 0x46 ‖ uuidMSB(i64) ‖ uuidLSB(i64) ‖ signatureBytes
```
`0x46` (= byte 70, `HASH_SEPARATOR_BYTE`) delimits sections. So v2 differs from v1 in three ways: (a) a `SHA-256` pre-hash of the body, (b) inclusion of the **plain** content plus a separator before the optional decorated JSON, and (c) the **last-seen acknowledgement set** is part of the signed body.
**Serverbound chat (1.19.1, `ServerboundPackets1_19_1`):**
| Packet | ID | Change vs 1.19 |
|---|---|---|
| `CHAT_ACK` | 0x03 | **new** — acknowledge last-seen messages out of band |
| `CHAT_COMMAND` | 0x04 | +`lastSeenMessages` array + optional `lastReceivedMessage` appended |
| `CHAT` | 0x05 | +`lastSeenMessages` array + optional `lastReceivedMessage` |
| `CHAT_PREVIEW` | 0x06 | unchanged role |
Source: `v1_19to1_19_1/packet/ServerboundPackets1_19_1.java:27-30`. The IDs all shift by one to make room for `CHAT_ACK` at 0x03. ViaVersion's signing handler on the serverbound `CHAT`/`CHAT_COMMAND` calls `chatSession.signChatMessage(metadata, decoratableMessage)` and writes the resulting signature + `signedPreview = decoratableMessage.isDecorated()`, then reads (and drops, for the upstream 1.19-side) the `PLAYER_MESSAGE_SIGNATURE_ARRAY` last-seen and `OPTIONAL_PLAYER_MESSAGE_SIGNATURE`: `Protocol1_19To1_19_1.java:112-193`. `CHAT_ACK` is cancelled outbound: `:194`.
**Clientbound chat (1.19.1, `ClientboundPackets1_19_1`):**
| Packet | ID | Change vs 1.19 |
|---|---|---|
| `CUSTOM_CHAT_COMPLETIONS` | 0x15 | **new** |
| `DELETE_CHAT` | 0x18 | **new** — server retracts a message by signature |
| `PLAYER_CHAT_HEADER` | 0x32 | **new** — standalone signed header (preceding-sig + body hash) for messages whose body the client already has |
| `PLAYER_CHAT` | 0x33 | reworked: carries the last-seen context + filter mask |
| `SERVER_DATA` | 0x42 | +`enforcesSecureChat` bool appended |
| `SET_DISPLAY_CHAT_PREVIEW` | 0x4E | unchanged role |
| `SYSTEM_CHAT` | 0x62 | type field becomes an `overlay` bool downstream |
Source: `v1_19to1_19_1/packet/ClientboundPackets1_19_1.java:45,48,74,75,90,122`. ViaVersion, lacking a way to faithfully reproduce the 760 signed chat onto a 759 client, **collapses `PLAYER_CHAT` (760) back into `SYSTEM_CHAT` (759)** — re-decorating the message via the chat-type registry rather than forwarding signatures: `Protocol1_19To1_19_1.java:85-111` + the `decorateChatMessage`/`translatabaleComponentFromTag` helpers (`:321-417`). It also injects the `enforcesSecureChat` flag into `SERVER_DATA` from its own config: `:221-229` (`create(Types.BOOLEAN, Via.getConfig().enforceSecureChat())`).
**Velocity-forwarding wrinkle.** Because the profile key now travels in Velocity modern forwarding, a 760 key would reach a server expecting a 759 key; ViaVersion rewrites the `velocity:player_info` forwarding-version byte down to 1 in the login `CUSTOM_QUERY`: `Protocol1_19To1_19_1.java:284-308`.
**ViaVersion package/commits.** Package `common/.../protocols/v1_19to1_19_1/` (`Protocol1_19To1_19_1.java` + `data/{ChatDecorationResult,ChatRegistry1_19_1}.java` + `storage/{ChatTypeStorage,NonceStorage1_19_1}.java`). `git log --oneline -- …/v1_19to1_19_1`: `fe9ca4992` "Update mcstructs", `29f299d88` "Update MCStructs to 3.0.0 (#4422)", `32e51b52a` "Cleanup LOGIN/STATUS packet handlers (#4113)", `b1f64fd08` "Use enhanced switches in more places (#4043)". <!-- VERIFY: the original 760 chain/reporting implementation commit subject is not in the captured log (predates the renames) -->
---
## 761 — 1.19.3 (2022-12-07): partial revert + chain v3
**Headline.** 1.19.3 **removed chat preview entirely** ("Removed chat preview"); deleted messages now show a "deleted by the server" placeholder for ≥3s; the "Modified" tag stops appearing for style-only server edits. ([minecraft.wiki/w/Java_Edition_1.19.3](https://minecraft.wiki/w/Java_Edition_1.19.3), fetched 2026-06-19.) The wiki release article does not narrate the wire-level chat-security rework — that detail comes from ViaVersion source below.
**What was reverted / re-architected at the wire level:**
1. **Chat preview gone.** All preview packets are dropped: ViaVersion cancels `CHAT_PREVIEW`, `SET_DISPLAY_CHAT_PREVIEW`, `PLAYER_CHAT_HEADER`, and `DELETE_CHAT` when translating a 1.19.1 server down: `v1_19_1to1_19_3/Protocol1_19_1To1_19_3.java:325-328`. The standalone `PLAYER_CHAT_HEADER` packet (0x32 in 760) is **gone** from the 761 clientbound set.
2. **`signedPreview` bool removed** from serverbound `CHAT`/`CHAT_COMMAND`; the message format is acknowledgement-based instead.
3. **Last-seen moves to an offset + bitset** (`ACKNOWLEDGED_BIT_SET`) rather than a full signature array on every message — far more compact: serverbound `CHAT` (0x05) ends with `offset VarInt` + `ACKNOWLEDGED_BIT_SET`: `Protocol1_19_1To1_19_3.java:230-270`; `CHAT_COMMAND` (0x04) likewise `:176-229`.
4. **New `CHAT_SESSION_UPDATE` (serverbound 0x20)** — the player's profile key + a per-session ID are announced *in-Play* via this packet, no longer in Login Start. ViaVersion cancels it (its downstream clients have no key): `Protocol1_19_1To1_19_3.java:324`.
5. **`DISGUISED_CHAT` (clientbound 0x18) introduced** for server-authored messages that need chat-type decoration but **no signature** (e.g. `/say`, command output) — separating "decorated but unsigned" from genuinely signed `PLAYER_CHAT`. ViaVersion rewrites a 1.19.1 `PLAYER_CHAT` *down* into `DISGUISED_CHAT`: `Protocol1_19_1To1_19_3.java:128-174`.
6. **System messages out of the signed path.** Combined with `DISGUISED_CHAT`, system/`/say`-style output no longer rides the signature chain; only genuine player chat is signed, and signing is optional unless `enforce-secure-profile` forces it.
**Model — the v3 chain (`ChatSession1_19_3`).** The chain is rebuilt around a **per-session random `sessionId` + a monotonic message index** (`MessageLink`), replacing the v2 "preceding-signature header":
```java
// api/.../signature/storage/ChatSession1_19_3.java:37-54
private final UUID sessionId = UUID.randomUUID();
private MessageLink link = new MessageLink(uuid, sessionId); // index 0
...
signer.accept(Ints.toByteArray(1)); // a constant version/prefix int = 1
messageLink.update(signer); // senderUUID ‖ sessionId ‖ index(i32)
messageBody.update(signer); // see below
```
`MessageLink.update` (`chain/v1_19_3/MessageLink.java:45-49`): `senderUUID ‖ sessionId(UUID) ‖ index(i32)`, with `index` incrementing per message (`next()`, capped at `Integer.MAX_VALUE`).
`MessageBody.update` (`chain/v1_19_3/MessageBody.java:46-57`) is now fed to the signer **raw (no intermediate SHA-256 of the whole body)**:
```
salt(i64) ‖ timestampSeconds(i64) ‖ contentLength(i32) ‖ content(UTF-8)
‖ lastSeenCount(i32) ‖ for each lastSeen: signatureBytes
```
Differences from v2: (a) the `MessageHeader{precedingSig, sender}` is replaced by `MessageLink{sender, sessionId, index}` — order is now proven by a session-scoped counter rather than a literal back-link to the previous signature; (b) a constant `int 1` is prefixed; (c) the body uses a **length-prefixed** plain content and last-seen list (only the **signatureBytes**, not the UUID, per acknowledged message) and **drops the decorated JSON** from the signed payload entirely; (d) the body is no longer pre-hashed by the chain code itself.
**Serverbound chat (1.19.3, `ServerboundPackets1_19_3`):**
| Packet | ID | Change vs 1.19.1 |
|---|---|---|
| `CHAT_ACK` | 0x03 | now carries an offset/count, not a full array <!-- VERIFY: exact CHAT_ACK 761 field layout not read from enum --> |
| `CHAT_COMMAND` | 0x04 | argument sigs use `SIGNATURE_BYTES`; ends with `offset` + `ACKNOWLEDGED_BIT_SET` |
| `CHAT` | 0x05 | optional `SIGNATURE_BYTES`, ends with `offset` + `ACKNOWLEDGED_BIT_SET` |
| `CHAT_SESSION_UPDATE` | 0x20 | **new** — profile key + session id |
Source: `v1_19_1to1_19_3/packet/ServerboundPackets1_19_3.java:27,28,29,56`. ViaVersion's serverbound `CHAT` handler signs with `chatSession.signChatMessage(metadata, decoratableMessage, messagesStorage.lastSignatures())` when a session exists, else writes empty signature: `Protocol1_19_1To1_19_3.java:230-270`.
**Clientbound chat (1.19.3, `ClientboundPackets1_19_3`):**
| Packet | ID | Change vs 1.19.1 |
|---|---|---|
| `CUSTOM_CHAT_COMPLETIONS` | 0x14 | renumbered |
| `DELETE_CHAT` | 0x16 | retained (signature-based delete), preview-deletes gone |
| `DISGUISED_CHAT` | 0x18 | **new** — decorated-but-unsigned |
| `PLAYER_CHAT` | 0x31 | reworked: `previousSignature` opt + `PLAYER_MESSAGE_SIGNATURE` + last-seen + filter mask |
| `SERVER_DATA` | 0x41 | MOTD becomes mandatory Component; icon becomes byte[] (1.19.4 change) |
| `SYSTEM_CHAT` | 0x60 | renumbered; system messages now strictly unsigned |
Source: `v1_19_1to1_19_3/packet/ClientboundPackets1_19_3.java:44,46,48,73,89,120`. `PLAYER_CHAT_HEADER` and the two preview packets are **absent** from this enum.
**Acknowledgement bookkeeping.** ViaVersion maintains a `ReceivedMessagesStorage`: every incoming signed `PLAYER_CHAT` is recorded, and after 64 unacknowledged it auto-sends a `CHAT_ACK`: `Protocol1_19_1To1_19_3.java:135-148`.
**ViaVersion package/commits.** Package `common/.../protocols/v1_19_1to1_19_3/` (`Protocol1_19_1To1_19_3.java` + `storage/{NonceStorage1_19_3,ReceivedMessagesStorage}.java` + rewriters). `git log --oneline -- …/v1_19_1to1_19_3` (relevant): `3eec520eb` "Send enable features packet after the play login packet in 1.19.1->1.19.3 (#4205)", `3caaed00d` "Write enabled features as string array", `815ec24af` "Remove removed registries from command arguments", `ab3927dff` "Implement our own hash writing", `32e51b52a` "Cleanup LOGIN/STATUS packet handlers (#4113)". <!-- VERIFY: the original 761 chat-rework commit subject is not in the captured log -->
---
## 762 — 1.19.4 (2023-03-14): refinements (no signing-payload change)
**Headline.** 1.19.4 added Display entities, Interaction entities, armour trims, archaeology (brush + suspicious sand), the Cherry Grove biome, the Sniffer, and `/ride` + `/damage`. ([minecraft.wiki/w/Java_Edition_1.19.4](https://minecraft.wiki/w/Java_Edition_1.19.4), fetched 2026-06-19.)
**Chat / signing:** **the signed-message payload did not change from 761.** The only chat-security change is procedural: "Clients now reset their secure chat session state when receiving the login packet" ([1.19.4 article](https://minecraft.wiki/w/Java_Edition_1.19.4), fetched 2026-06-19) — i.e. the `MessageLink` index/session resets on a (re)Login, so a dimension change / server-switch starts a fresh chain. ViaVersion's `v1_19_3to1_19_4` package contains **no chat-signing handler at all**; its `registerPackets` only touches `COMMANDS`, `UPDATE_MOB_EFFECT` (infinite-duration 1 sentinel), and `SERVER_DATA` (MOTD → mandatory Component, icon String → `OPTIONAL_BYTE_ARRAY_PRIMITIVE`): `v1_19_3to1_19_4/Protocol1_19_3To1_19_4.java:60-105`. The `CHAT`/`PLAYER_CHAT`/`DISGUISED_CHAT` packets pass through structurally unchanged at 762 (no rewriter registered for them).
**ViaVersion package/commits.** Package `common/.../protocols/v1_19_3to1_19_4/`. `git log --oneline -- …/v1_19_3to1_19_4`: `b4d8fe6ba` "Display infinite potion effect duration for 1.19.4+…(#4953)", `97aff00ce` "Fill registries used by delayed item loading", `c13b40a37` "Add ParticleRewriter base (#4203)", `501f65e21` "Packet and entity type renames". No chat-signing commits — consistent with "no payload change at 762".
---
## Signature-chain evolution (759 → 761)
```mermaid
flowchart TD
subgraph v1["759 (1.19) — ChatSession1_19_0"]
A1["per-message signature<br>NO chain"]
A2["payload: salt ‖ senderUUID ‖ timestamp ‖ sortedDecoratedJSON"]
A1 --> A2
end
subgraph v2["760 (1.19.1/1.19.2) — ChatSession1_19_1"]
B1["chain via PRECEDING signature<br>(MessageHeader)"]
B2["header: precedingSig ‖ senderUUID"]
B3["body (SHA-256 hashed): salt ‖ ts ‖ plain ‖ 0x46 [‖ decoratedJSON]<br>‖ for each lastSeen: 0x46 ‖ uuid ‖ sig"]
B1 --> B2 --> B3
end
subgraph v3["761 (1.19.3) — ChatSession1_19_3"]
C1["chain via sessionId + index<br>(MessageLink)"]
C2["prefix int 1 ‖ link: senderUUID ‖ sessionId ‖ index"]
C3["body: salt ‖ ts ‖ len ‖ content ‖ lastSeenCount ‖ sigs<br>(no decoratedJSON, no decorated path)"]
C1 --> C2 --> C3
end
v1 -->|"+ player reporting,<br>last-seen acks"| v2
v2 -->|"preview removed,<br>header→sessionId+index,<br>DISGUISED_CHAT split"| v3
```
Sources: `ChatSession1_19_0.java:42-51`, `ChatSession1_19_1.java:43-52` + `chain/v1_19_1/{MessageHeader,MessageBody}.java`, `ChatSession1_19_3.java:37-54` + `chain/v1_19_3/{MessageLink,MessageBody}.java`.
---
## Proxy / translation impact
A proxy or version-translation layer sitting across the 1.19 line must understand **four mutually-incompatible chat shapes**, because each protocol bump moved the signature fields and the chain rules:
1. **All four signing models coexist in the wild.** Vanilla 1.19, 1.19.1/.2, 1.19.3, and 1.19.4 clients each speak a different chat-packet shape. A proxy that translates between them (ViaVersion) cannot just renumber packets — it must **re-sign or strip-and-downgrade** the message:
- Down-translating signed chat to an older format usually means **discarding the signature and re-rendering as a system/disguised message** (ViaVersion does exactly this: 760→759 collapses `PLAYER_CHAT``SYSTEM_CHAT` (`Protocol1_19To1_19_1.java:85-111`); 761→760 turns `PLAYER_CHAT``DISGUISED_CHAT` (`Protocol1_19_1To1_19_3.java:128-174`)).
- When the proxy itself holds the player's key (a configured `ChatSession`), it **forges valid signatures** for the target format (`signChatMessage(...)` in each protocol's serverbound `CHAT`/`CHAT_COMMAND` handler).
2. **Risk: relaying stripped signatures to an `enforce-secure-profile` server.** If `enforce-secure-profile=true` (default since 1.19.1), a server **rejects** unsigned or invalidly-signed player chat. A proxy that strips signatures while down/upgrading — without re-signing — will have its players' chat dropped or the connection kicked. ViaVersion mitigates by (a) substituting its own `ChatSession` key in Login Start (759/760) / `CHAT_SESSION_UPDATE` (761), and (b) re-signing per target format. A proxy that does **not** hold the key can only forward to non-enforcing servers, or must route player chat as system/disguised (unsigned) messages — which the target may flag "Not Secure" or reject.
3. **`enforcesSecureChat` flag must be synthesised.** `SERVER_DATA` gained the `enforcesSecureChat` bool at 760; a proxy bridging from an older server has to fabricate it (ViaVersion sources it from `Via.getConfig().enforceSecureChat()`: `Protocol1_19To1_19_1.java:227`).
4. **Last-seen / acknowledgement bookkeeping is mandatory at 760+.** Because the chain folds in last-seen signatures, a translating proxy must track received `PLAYER_CHAT` signatures and emit `CHAT_ACK`/offset+bitset acknowledgements, or the server's chain validation desyncs (ViaVersion's `ReceivedMessagesStorage`, auto-`CHAT_ACK` at 64 unacked: `Protocol1_19_1To1_19_3.java:135-148`).
5. **Login-phase profile key handling differs 759/760 vs 761.** A proxy must read/strip/substitute the Login-Start profile key and the Encryption-Response salt/signature variant for 759/760, but switch to the in-Play `CHAT_SESSION_UPDATE` path for 761+. Velocity modern-forwarding carries the key too, so the forwarding-version byte may need clamping (`Protocol1_19To1_19_1.java:284-308`). See [../05-login-encryption.md](../05-login-encryption.md) §2/§5.
6. **762 is cheap.** No signing change; a proxy only resets per-session chat state on Login and handles the `SERVER_DATA` MOTD/icon type change.
---
## Sources
- minecraft.wiki release articles (fetched 2026-06-19): [1.19](https://minecraft.wiki/w/Java_Edition_1.19), [1.19.1](https://minecraft.wiki/w/Java_Edition_1.19.1), [1.19.3](https://minecraft.wiki/w/Java_Edition_1.19.3), [1.19.4](https://minecraft.wiki/w/Java_Edition_1.19.4).
- ViaVersion `/tmp/mcproto-refs/ViaVersion/``common/.../protocols/{v1_18_2to1_19, v1_19to1_19_1, v1_19_1to1_19_3, v1_19_3to1_19_4}/` (Protocol classes + packet enums) and `api/.../minecraft/signature/` (`ChatSession1_19_0/1_19_1/1_19_3` + `chain/v1_19_1/{MessageHeader,MessageBody}` + `chain/v1_19_3/{MessageLink,MessageBody}`). Git logs per package as cited.
- minecraft-data `/tmp/mcproto-refs/minecraft-data/data/pc/``1.19`,`1.19.2`,`1.19.3`,`1.19.4` `version.json` + `common/protocolVersions.json` (protocol numbers + dataVersions).
- Cross-link: [../05-login-encryption.md](../05-login-encryption.md) (profile keys in Login Start; Encryption Response salt/signature form).
```
+365
View File
@@ -0,0 +1,365 @@
# Java Edition 1.20.x — Protocol Deep-Dive
| Release | Protocol # | Release date | ViaVersion package (bump *into* this) | minecraft-data dir |
|---|---|---|---|---|
| 1.20 | **763** | 2023-06-07 | `v1_19_4to1_20` (762→763) | `data/pc/1.20` |
| 1.20.1 | **763** | 2023-06-12 <!-- VERIFY: exact 1.20.1 date not fetched from wiki --> | *(same protocol as 1.20; no bump)* | `data/pc/1.20.1` |
| 1.20.2 | **764** | 2023-09-21 | `v1_20to1_20_2` (763→764) — **adds Configuration state** | `data/pc/1.20.2` |
| 1.20.3 | **765** | 2023-12-05 | `v1_20_2to1_20_3` (764→765) | `data/pc/1.20.3` |
| 1.20.4 | **765** | 2023-12-07 | *(same protocol as 1.20.3; no bump)* | `data/pc/1.20.4` |
| 1.20.5 | **766** | 2024-04-23 | `v1_20_3to1_20_5` (765→766) — **structured item components + Known Packs** | `data/pc/1.20.5` |
| 1.20.6 | **766** | 2024-04-29 <!-- VERIFY: exact 1.20.6 date not fetched from wiki --> | *(same protocol as 1.20.5; no bump)* | `data/pc/1.20.6` |
Sources: minecraft.wiki release articles ([1.20](https://minecraft.wiki/w/Java_Edition_1.20) fetched 2026-06-19, [1.20.2](https://minecraft.wiki/w/Java_Edition_1.20.2), [1.20.3](https://minecraft.wiki/w/Java_Edition_1.20.3), [1.20.4](https://minecraft.wiki/w/Java_Edition_1.20.4), [1.20.5](https://minecraft.wiki/w/Java_Edition_1.20.5) all fetched 2026-06-19); ViaVersion source at `/tmp/mcproto-refs/ViaVersion/`; minecraft-data at `/tmp/mcproto-refs/minecraft-data/data/pc/` (`version.json` numbers cross-checked: 1.20/1.20.1=763, 1.20.2=764, 1.20.3/1.20.4=765, 1.20.5/1.20.6=766).
> Protocol numbers verified directly from minecraft-data `version.json` files and ViaVersion's `MappingDataBase(...)` constructor arguments in each `Protocol*.java`.
---
## Headline — Trails & Tales, plus the two biggest connection-layer changes since the Netty rewrite
1.20 the *release* ("Trails & Tales", 2023-06-07) is a content update — cherry wood, the archaeology system (brush + suspicious sand/gravel + pottery sherds), the Sniffer and Camel mobs, bamboo wood set, hanging signs, smithing-template **armor trims**, and the chiseled bookshelf ([minecraft.wiki/w/Java_Edition_1.20](https://minecraft.wiki/w/Java_Edition_1.20), fetched 2026-06-19). But protocol-wise, base 1.20 (763) is a *small* bump. The 1.20 **line** is remembered instead for two structural protocol mechanics that landed in its patch releases and reshaped how every proxy on the planet works:
- **The Configuration connection state (1.20.2 / protocol 764).** A brand-new network state was inserted **between Login and Play**. Registry data, tags, feature flags and resource-pack negotiation all moved out of the Play-state "Join Game" packet into this dedicated phase. Login no longer flows straight into Play — the client sends a **Login Acknowledged** packet to end Login, runs the Configuration handshake, then sends **Finish Configuration** to enter Play. See [`../06-configuration.md`](../06-configuration.md) and [`../02-connection-lifecycle.md`](../02-connection-lifecycle.md) §5b7 for the topical treatment.
- **Structured item data components (1.20.5 / protocol 766).** The free-form `tag` NBT compound that had ridden along with every item stack since the dawn of the protocol was replaced by **typed structured components** — a map of `minecraft:<component>` keys, each with its own wire type, e.g. `wooden_pickaxe[damage=23]` ([minecraft.wiki/w/Java_Edition_1.20.5](https://minecraft.wiki/w/Java_Edition_1.20.5), fetched 2026-06-19). The same release added the **Known Packs** handshake to Configuration so server and client can agree on which registry entries are already known client-side.
The other two bumps are smaller: 763 (cherry/archaeology content, minimal wire change), and 765 (text components serialised as NBT instead of JSON strings; multi-resource-pack push/pop).
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
Note over C,S: protocol < 764 (1.20 / 1.20.1) — Login flows straight into Play
C->>S: Login Start
S-->>C: Login Success (Game Profile)
Note over C,S: client switches to PLAY immediately
S-->>C: Join Game (registries + tags + features inline)
Note over C,S: protocol >= 764 (1.20.2+) — Configuration inserted between Login and Play
C->>S: Login Start
S-->>C: Login Success (Game Profile)
C->>S: Login Acknowledged
Note over C,S: state = CONFIGURATION
S-->>C: Registry Data / Update Tags / Update Enabled Features
S-->>C: Resource Pack (optional) / Custom Payload (brand)
S-->>C: Finish Configuration
C->>S: Finish Configuration (ack)
Note over C,S: state = PLAY
S-->>C: Join Game (now lean — IDs/dimensions only)
```
---
## 763 — 1.20 / 1.20.1 (2023-06-07)
**ViaVersion package:** `protocols/v1_19_4to1_20/``Protocol1_19_4To1_20.java`.
The single most telling fact: this package **reuses the 1.19.4 packet enums on both sides**. Its class signature is
```java
// Protocol1_19_4To1_20.java:39
public final class Protocol1_19_4To1_20 extends
AbstractProtocol<ClientboundPackets1_19_4, ClientboundPackets1_19_4,
ServerboundPackets1_19_4, ServerboundPackets1_19_4>
```
i.e. clientbound-unmapped == clientbound-mapped == `ClientboundPackets1_19_4`, and likewise serverbound. There is **no `ClientboundPackets1_20` / `ServerboundPackets1_20` enum** in the package at all (`ls v1_19_4to1_20/` → only `Protocol1_19_4To1_20.java` + `rewriter/`). So 763 introduced **zero packet additions, removals, or ID renumberings** in any state. minecraft-data agrees: `data/pc/1.20/protocol.json` has the same top-level state set as 1.19.4 (`handshaking/status/login/play`, **no configuration**) and a near-identical packet count.
What 763 *did* change is content-side mapping data and a few in-place field handlers:
- **`PLAYER_COMBAT_END` / `PLAYER_COMBAT_KILL`** — a leading entity-ID `VarInt` field was dropped from each (the duration/killer fields shifted). ViaVersion handles this in-place. Source: `Protocol1_19_4To1_20.java:5666`.
- **Block/item/entity mapping refresh** — new cherry & bamboo blocks, the Sniffer entity, suspicious sand/gravel ("brushable") block entities, hanging-sign and decorated-pot block entities, and the **armor-trim** smithing items are all handled purely as remapped registry IDs through `BlockPacketRewriter1_20`, `ItemPacketRewriter1_20`, `EntityPacketRewriter1_20` plus the `1.19.4``1.20` mapping files (`MappingDataBase("1.19.4", "1.20")`, `Protocol1_19_4To1_20.java:41`). No new packet types were needed for any of this content.
- The `ItemPacketRewriter1_20` only re-touches existing packets (`OPEN_SIGN_EDITOR`, `SIGN_UPDATE`, `LEVEL_CHUNK_WITH_LIGHT`, `LIGHT_UPDATE`, `SECTION_BLOCKS_UPDATE`, `UPDATE_RECIPES`) — source `ItemPacketRewriter1_20.java:72,76,84,95,101,115`.
ViaVersion git log for `v1_19_4to1_20` (`git -C /tmp/mcproto-refs/ViaVersion log --oneline -- common/…/v1_19_4to1_20`):
| Commit | Subject |
|---|---|
| `0121534e7` | Deduplicate particle type fillers |
| `8f8f5e72c` | Default rewriter registrations across protocols |
| `bfaf7a58e` | Move block entity handling to block rewriter (fixes new-instance pass-through on type-id change) |
| `ac3362f95` | Don't add damage types twice in 1.19.4→1.20 (#4389) |
| `c13b40a37` | Add ParticleRewriter base (#4203) |
| `501f65e21` | Packet and entity type renames (Mojang-mapped names) |
| `e965e9713` | Package/class renames and moves |
**1.20.1** carried the same protocol number 763 with no ViaVersion package of its own — it was a server-side bugfix release. Packet set identical to 1.20. (`data/pc/1.20.1/version.json``{"minecraftVersion":"1.20.1","version":763,"majorVersion":"1.20"}`.)
---
## 764 — 1.20.2 (2023-09-21) — **the Configuration state** (the headline)
**ViaVersion package:** `protocols/v1_20to1_20_2/``Protocol1_20To1_20_2.java` (+ `storage/ConfigurationState.java`, the config packet enums). This bump finally gives the package its own packet enums (`ClientboundPackets1_20_2` / `ServerboundPackets1_20_2`) **and** a brand-new set of Configuration-state enums.
### What the Configuration state is
Mojang inserted a fourth long-lived connection state. From the wiki: *"Configuration phase automatically starts after login phase (i.e. after client account has been verified) and lasts until the player joins the world (play phase)… Clients can stay in configuration phase indefinitely — it's up to server to release it to the world… Servers can also request clients to re-enter the configuration phase after it has entered the play phase."* ([minecraft.wiki/w/Java_Edition_1.20.2](https://minecraft.wiki/w/Java_Edition_1.20.2), fetched 2026-06-19.)
minecraft-data captures the structural shift exactly: `data/pc/1.20/protocol.json` top-level states are `handshaking/status/login/play`, while `data/pc/1.20.2/protocol.json` adds a fifth: `handshaking/status/login/configuration/play`. Verified by reading the top-level keys of each file.
### How Login → Config → Play changed
The old flow was: server sends `LOGIN_FINISHED` (a.k.a. Game Profile / Login Success), client flips straight to **Play**, server sends a fat Join Game packet carrying the dimension registry, tags, and enabled features inline.
The new flow (protocol ≥ 764):
1. Server sends **Login Success** (`ClientboundLoginPackets.LOGIN_FINISHED`, login ID `0x02`; aliased `GAME_PROFILE`. Source: `protocols/base/ClientboundLoginPackets.java:26,32`).
2. Client replies **Login Acknowledged** (`ServerboundLoginPackets.LOGIN_ACKNOWLEDGED`, login ID `0x03`. Source: `protocols/base/ServerboundLoginPackets.java:27`) — this is the new packet that *ends* the Login state.
3. Connection enters **CONFIGURATION**. Server pushes registry data, tags, features and (optionally) a resource pack, then **Finish Configuration**; the client acks with its own **Finish Configuration**.
4. Only then does the connection enter **PLAY**, and the now-lean Join Game packet arrives.
Per the wiki, the following moved *out of* Play and into Configuration: registry-data configuration, enabled-features setup, and server resource-pack negotiation ("the player is no longer in world when answering prompts"). Custom-payload, tag updates, and ping/keep-alive exist in *both* states. ([minecraft.wiki/w/Java_Edition_1.20.2](https://minecraft.wiki/w/Java_Edition_1.20.2), fetched 2026-06-19; cross-checked [Java Edition protocol/Registry data](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Registry_Data), accessed 2026-06-19.)
### Configuration-state packet tables (protocol 764)
**Clientbound (server → client), Configuration state**`ClientboundConfigurationPackets1_20_2.java`:
| ID | Name |
|---|---|
| 0x00 | `CUSTOM_PAYLOAD` |
| 0x01 | `DISCONNECT` |
| 0x02 | `FINISH_CONFIGURATION` |
| 0x03 | `KEEP_ALIVE` |
| 0x04 | `PING` |
| 0x05 | `REGISTRY_DATA` |
| 0x06 | `RESOURCE_PACK` |
| 0x07 | `UPDATE_ENABLED_FEATURES` |
| 0x08 | `UPDATE_TAGS` |
Source: `v1_20to1_20_2/packet/ClientboundConfigurationPackets1_20_2.java:2432` (IDs are enum `ordinal()`).
**Serverbound (client → server), Configuration state**`ServerboundConfigurationPackets1_20_2.java`:
| ID | Name |
|---|---|
| 0x00 | `CLIENT_INFORMATION` |
| 0x01 | `CUSTOM_PAYLOAD` |
| 0x02 | `FINISH_CONFIGURATION` |
| 0x03 | `KEEP_ALIVE` |
| 0x04 | `PONG` |
| 0x05 | `RESOURCE_PACK` |
Source: `v1_20to1_20_2/packet/ServerboundConfigurationPackets1_20_2.java:2530`.
### How ViaVersion fakes the Configuration state for a 1.20 (763) server
This is the most instructive part of the whole release for proxy authors. A 1.20.2 *client* expects the Configuration handshake; a 1.20 *server* knows nothing about it. ViaVersion bridges the gap with a small state machine in `ConfigurationState` (`storage/ConfigurationState.java`) whose `BridgePhase` enum has four values:
```java
// ConfigurationState.java:167169
public enum BridgePhase {
NONE, PROFILE_SENT, CONFIGURATION, REENTERING_CONFIGURATION
}
```
The bridge works like this (`Protocol1_20To1_20_2.java`):
- On the server's `LOGIN_FINISHED` (Game Profile), Via sets `BridgePhase.PROFILE_SENT` and forces the *server-side* tracked state to PLAY (the old server is about to start sending Play packets). Source: `Protocol1_20To1_20_2.java:141144`.
- The client then sends `LOGIN_ACKNOWLEDGED`. Via **cancels** it (the old server can't parse it), sets `BridgePhase.CONFIGURATION`, and flushes any packets it had queued. Source: `Protocol1_20To1_20_2.java:146155`.
- While in `PROFILE_SENT`, every clientbound Play packet the old server emits is **queued**, not forwarded, until the client has transitioned into Configuration — see the override in `transform(...)` at `Protocol1_20To1_20_2.java:247327`, which queues packets (`addClientboundPacketToQueue`) or remaps a handful (`CUSTOM_PAYLOAD`, `DISCONNECT`, `KEEP_ALIVE`, `PING`, `UPDATE_ENABLED_FEATURES`, `UPDATE_TAGS`) to their Configuration-state counterparts.
- Via synthesises the whole Configuration sequence itself in `sendConfigurationPackets(...)`: it sends a `REGISTRY_DATA` packet built from the dimension registry it captured, replays tags (or an empty `UPDATE_TAGS` so later protocols can append), optionally re-sends the last resource pack, then sends `FINISH_CONFIGURATION` and flips server state back to PLAY. Source: `Protocol1_20To1_20_2.java:329373`.
- The client's serverbound `CLIENT_INFORMATION` (settings) is captured during Configuration and **re-sent** later as the Play-state settings packet, because the client only sends it once per connection. Source: `Protocol1_20To1_20_2.java:167184` + `ConfigurationState.java:171187`.
- The client's Configuration-state `CUSTOM_PAYLOAD` / `KEEP_ALIVE` / `PONG` are mapped to their Play equivalents and **queued** until the server's listener is in PLAY. Source: `Protocol1_20To1_20_2.java:187189`.
- Login `HELLO` (serverbound name) gained an `OPTIONAL_UUID` field in 1.20.2; Via converts the plain `UUID` a 1.20 client sends. Source: `Protocol1_20To1_20_2.java:128133`.
- The config-phase serverbound packet queue is bounded (default 1000 packets / 1 MiB) and over-budget connections are disconnected. Source: `ConfigurationState.java:3738,8191`.
ViaVersion git log for `v1_20to1_20_2` (selected, from `git -C … log --oneline -- common/…/v1_20to1_20_2`):
| Commit | Subject |
|---|---|
| `9a8a01b75` | Make config limits configurable via system properties |
| `3e8682f94` | Limit config phase packet queue |
| `6ce21135f` | Make `AbstractProtocol#registerFinishConfiguration` obsolete (#4853) |
| `8d3c36de0` | Send empty tags packet if not sent early enough when leaving config stage |
| `5772ee4a9` | Only send tags early for 1.20.5+ clients, track early send properly in 1.20→1.20.2 |
| `7f06b0345` | Remove `StorableObject#clearOnServerSwitch` (#4583) |
| `ceb1cffb0` | Cancel "message not delivered" messages (fixes #3438) |
| `32e51b52a` | Cleanup LOGIN/STATUS packet handlers (#4113) |
Other 764 Play-state field changes handled in the same file: `SET_DISPLAY_OBJECTIVE` slot widened `Byte``VarInt` (`Protocol1_20To1_20_2.java:123126`); `CUSTOM_PAYLOAD` content sanitised softly (`:84,231245`); `UPDATE_ENABLED_FEATURES` is cancelled in Play and re-emitted in Config (`:194,310`).
---
## 765 — 1.20.3 / 1.20.4 (2023-12-05 / 2023-12-07)
**ViaVersion package:** `protocols/v1_20_2to1_20_3/``Protocol1_20_2To1_20_3.java`, with its own `ClientboundPackets1_20_3` / `ServerboundPackets1_20_3` and `ClientboundConfigurationPackets1_20_3`.
This is a medium bump, dominated by two themes:
### 1. Text components serialise as NBT, not JSON strings
The headline 765 change: text/chat components are now sent over the wire as **NBT** rather than JSON strings ([minecraft.wiki/w/Java_Edition_1.20.3](https://minecraft.wiki/w/Java_Edition_1.20.3), fetched 2026-06-19 — *"Chat components now serialize to NBT when sent over network"*; plain-text components serialise as a bare string instead of `{"text":"…"}`). ViaVersion does this conversion in `convertComponent` / `convertOptionalComponent`, which read a `Types.COMPONENT` (JSON) and write a `Types.TRUSTED_TAG` (NBT):
```java
// Protocol1_20_2To1_20_3.java:330336
private void convertComponent(final PacketWrapper wrapper) {
wrapper.write(Types.TRUSTED_TAG, ComponentUtil.jsonToTag(wrapper.read(Types.COMPONENT)));
}
```
It is applied across a long list of packets carrying components: `DISCONNECT` (both Play and Configuration states — note `ClientboundConfigurationPackets1_20_2.DISCONNECT` at `:229`), `SERVER_DATA`, `SET_ACTION_BAR_TEXT`, `SET_TITLE_TEXT`, `SET_SUBTITLE_TEXT`, `DISGUISED_CHAT`, `SYSTEM_CHAT`, `OPEN_SCREEN`, `TAB_LIST`, `PLAYER_COMBAT_KILL`, `PLAYER_INFO_UPDATE` (display name), `BOSS_EVENT`, `PLAYER_CHAT`, `SET_OBJECTIVE`, `SET_PLAYER_TEAM`, `UPDATE_ADVANCEMENTS`, `COMMAND_SUGGESTIONS`, `MAP_ITEM_DATA`. Source: `Protocol1_20_2To1_20_3.java:229262, 98254`.
### 2. Multi-resource-pack push/pop
Servers can now apply **multiple** resource packs, each identified by a UUID, and un-apply them individually. The old single `RESOURCE_PACK` packet (Play and Config) splits into `RESOURCE_PACK_PUSH` + `RESOURCE_PACK_POP`. ViaVersion maps the old packet to a push, synthesising a UUID from the URL via `UUID.nameUUIDFromBytes(url)`, and prepends a pop with `OPTIONAL_UUID = null` to drop prior packs. Source: `Protocol1_20_2To1_20_3.java:231,294,312328`. The serverbound resource-pack status enum gained new action values (downloaded / invalid-url / failed-reload / discarded); Via folds the new statuses back onto the old set (`:297310`). Resource packs are *also* no longer dropped on entering Configuration ([wiki 1.20.3](https://minecraft.wiki/w/Java_Edition_1.20.3), fetched 2026-06-19).
### Other 765 changes (from source)
- **`SET_SCORE` / `RESET_SCORE` split** — a `SET_SCORE` with action 1 ("reset") becomes a dedicated `RESET_SCORE` packet; scores gain an optional display component + number-format fields. Source: `Protocol1_20_2To1_20_3.java:7997`. Number formats (styled / fixed / blank) are new scoreboard display options ([wiki 1.20.3](https://minecraft.wiki/w/Java_Edition_1.20.3)).
- **`SET_JIGSAW_BLOCK`** gained selection-priority + placement-priority `VarInt`s (Via strips them downward). Source: `:113122`.
- **`CONTAINER_SLOT_STATE_CHANGED`** is a new serverbound packet (Via cancels it downward). Source: `:77`.
- The Configuration-state packet set is shared with 764 except resource-pack: `createPacketTypesProvider()` maps `ClientboundConfigurationPackets1_20_2``ClientboundConfigurationPackets1_20_3`. Source: `:380388`.
**1.20.4** shares protocol 765 with 1.20.3 — it was a one-bug hotfix (MC-267185, decorated pots deleting items on reload) with no protocol change ([minecraft.wiki/w/Java_Edition_1.20.4](https://minecraft.wiki/w/Java_Edition_1.20.4), fetched 2026-06-19). No separate ViaVersion package.
ViaVersion git log for `v1_20_2to1_20_3` (selected):
| Commit | Subject |
|---|---|
| `ab3927dff` | Implement our own hash writing |
| `bf84eb014` | Add separate config option for text component conversion errors |
| `6ad9a7190` | Print book conversion errors to default logger in 1.20.2→1.20.3 (#4158) |
| `2841bf304` | Add option to hide scoreboard numbers (#4122) |
| `2e91b841b` | Automatically call mapTypes in entity rewriter |
---
## 766 — 1.20.5 / 1.20.6 (2024-04-23 / 2024-04-29) — **structured item components + Known Packs**
**ViaVersion package:** `protocols/v1_20_3to1_20_5/``Protocol1_20_3To1_20_5.java`, with `data/`, `storage/`, the `*Packets1_20_5` enums and the big `StructuredDataConverter.java`. This is by far the heaviest bump of the 1.20 line (minecraft-data packet-name count jumps from ~911 at 1.20.3 to ~1066 at 1.20.5).
### 1. Structured item data components (typed `StructuredDataKey`)
Mojang replaced the single free-form `tag` NBT compound on every item stack with a **map of typed components**. From the wiki: *"Unstructured NBT data attached to stacks of items (`tag` field) has been replaced with structured 'components'."* Components are `minecraft:<component_name>` and render as `wooden_pickaxe[damage=23]`; legacy `{...}` custom NBT survives only inside `minecraft:custom_data` ([minecraft.wiki/w/Java_Edition_1.20.5](https://minecraft.wiki/w/Java_Edition_1.20.5), fetched 2026-06-19).
ViaVersion models each component as a **`StructuredDataKey<T>`** — a typed key pairing an identifier string with a wire `Type<T>`:
```java
// api/.../minecraft/data/StructuredDataKey.java:109
public record StructuredDataKey<T>(String identifier, Type<T> type) { }
// :111115 examples
public static final StructuredDataKey<CompoundTag> CUSTOM_DATA = new StructuredDataKey<>("custom_data", Types.COMPOUND_TAG);
public static final StructuredDataKey<Integer> MAX_STACK_SIZE = new StructuredDataKey<>("max_stack_size", Types.VAR_INT);
public static final StructuredDataKey<Integer> DAMAGE = new StructuredDataKey<>("damage", Types.VAR_INT);
public static final StructuredDataKey<Unbreakable> UNBREAKABLE1_20_5 = new StructuredDataKey<>("unbreakable", Unbreakable.TYPE);
```
The full 1.20.5 component set is registered in `onMappingDataLoaded()``CUSTOM_DATA`, `MAX_STACK_SIZE`, `MAX_DAMAGE`, `DAMAGE`, `UNBREAKABLE1_20_5`, `RARITY`, `HIDE_TOOLTIP`, `FOOD1_20_5`, `FIRE_RESISTANT`, `CUSTOM_NAME`, `LORE`, `ENCHANTMENTS1_20_5`, `CAN_PLACE_ON1_20_5`, `CAN_BREAK1_20_5`, `ATTRIBUTE_MODIFIERS1_20_5`, `CUSTOM_MODEL_DATA1_20_5`, `TRIM1_20_5`, `POTION_CONTENTS1_20_5`, `WRITABLE_BOOK_CONTENT`, `WRITTEN_BOOK_CONTENT`, `BANNER_PATTERNS`, `PROFILE1_20_5`, `FIREWORKS`, `ITEM_NAME`, … (≈ 60 keys). Source: `Protocol1_20_3To1_20_5.java:282302`.
**Downgrade to NBT (the proxy-critical bit):** to serve a 1.20.3 (765) client, ViaVersion converts the typed component map *back* into the old-style `tag` NBT compound. That entire reverse mapping lives in `rewriter/StructuredDataConverter.java` (a large per-component switch importing every `…item.data.*` class). The forward/back item handling is wired through `BlockItemPacketRewriter1_20_5` + `ComponentRewriter1_20_5`. Source: `v1_20_3to1_20_5/rewriter/StructuredDataConverter.java` (class at `:74`), `Protocol1_20_3To1_20_5.java:78,88`.
By 1.20.5 the **armor-trim** material/pattern registries (the smithing-template feature from base 1.20) are themselves data-driven, so Via keeps a default-registry fallback in `storage/ArmorTrimStorage.java` — the 10 vanilla trim **materials** (`amethyst…redstone`) and 16 **patterns** (`coast…wild`) as `KeyMappings`, updatable from the live registry. Source: `storage/ArmorTrimStorage.java:2658`. The `TRIM1_20_5` structured key resolves trim material/pattern indices against this storage during conversion.
### 2. The Known Packs handshake (Configuration state)
1.20.5 adds **`SELECT_KNOWN_PACKS`** to the Configuration state, in *both* directions. The server clientbound-pushes the packs it intends to drive the registries from; the client serverbound-replies with the packs it already has bundled, letting the server skip re-sending registry entries the client can reconstruct locally.
**Clientbound Configuration (1.20.5)**`ClientboundConfigurationPackets1_20_5.java`. Note the Configuration table grew from 9 to 15 entries (cookies + transfer + reset-chat + pack push/pop all live here now):
| ID | Name | | ID | Name |
|---|---|---|---|---|
| 0x00 | `COOKIE_REQUEST` | | 0x08 | `RESOURCE_PACK_POP` |
| 0x01 | `CUSTOM_PAYLOAD` | | 0x09 | `RESOURCE_PACK_PUSH` |
| 0x02 | `DISCONNECT` | | 0x0A | `STORE_COOKIE` |
| 0x03 | `FINISH_CONFIGURATION` | | 0x0B | `TRANSFER` |
| 0x04 | `KEEP_ALIVE` | | 0x0C | `UPDATE_ENABLED_FEATURES` |
| 0x05 | `PING` | | 0x0D | `UPDATE_TAGS` |
| 0x06 | `RESET_CHAT` | | 0x0E | **`SELECT_KNOWN_PACKS`** |
| 0x07 | `REGISTRY_DATA` | | | |
Source: `v1_20_3to1_20_5/packet/ClientboundConfigurationPackets1_20_5.java:2438`.
**Serverbound Configuration (1.20.5)**`ServerboundConfigurationPackets1_20_5.java`:
| ID | Name | | ID | Name |
|---|---|---|---|---|
| 0x00 | `CLIENT_INFORMATION` | | 0x04 | `KEEP_ALIVE` |
| 0x01 | `COOKIE_RESPONSE` | | 0x05 | `PONG` |
| 0x02 | `CUSTOM_PAYLOAD` | | 0x06 | `RESOURCE_PACK` |
| 0x03 | `FINISH_CONFIGURATION` | | 0x07 | **`SELECT_KNOWN_PACKS`** |
Source: `v1_20_3to1_20_5/packet/ServerboundConfigurationPackets1_20_5.java:2835`.
**How Via bridges it:** a 765 server never sends `SELECT_KNOWN_PACKS`, but a 766 client *expects* it before `REGISTRY_DATA`. So Via intercepts the server's clientbound `REGISTRY_DATA` and **synthesises** a `SELECT_KNOWN_PACKS` with an empty list ("no known packs, everything is sent here") immediately before it — forcing the client to take all registry entries from the wire rather than from local packs:
```java
// EntityPacketRewriter1_20_5.java:98101
protocol.registerClientbound(ClientboundConfigurationPackets1_20_3.REGISTRY_DATA, wrapper -> {
final PacketWrapper knownPacksPacket = wrapper.create(ClientboundConfigurationPackets1_20_5.SELECT_KNOWN_PACKS);
knownPacksPacket.write(Types.VAR_INT, 0); // No known packs, everything is sent here
knownPacksPacket.send(Protocol1_20_3To1_20_5.class);
```
Conversely the client's serverbound `SELECT_KNOWN_PACKS` reply is **cancelled** (the old server wouldn't understand it). Source: `Protocol1_20_3To1_20_5.java:258`. The same `REGISTRY_DATA` handler also splits the single monolithic registry blob into per-registry packets and injects extra registries (wolf variants, banner patterns) that 1.20.5 expects but 1.20.3 didn't send — `EntityPacketRewriter1_20_5.java:98230`.
> See [`../06-configuration.md`](../06-configuration.md) §4 for the topical Known-Packs treatment and how Velocity uses the Known-Packs boundary to bridge registries on backend switches.
### Other 766 changes (from source)
- **Cookies + transfer** — `STORE_COOKIE` / `COOKIE_REQUEST` / `COOKIE_RESPONSE` and `TRANSFER` packets (server can ask a client to connect elsewhere, carrying cookie state up to 5 KiB) ([wiki 1.20.5](https://minecraft.wiki/w/Java_Edition_1.20.5), fetched 2026-06-19). Via simply cancels the serverbound cookie responses downward — `Protocol1_20_3To1_20_5.java:256259`.
- **`CHAT_COMMAND` split** — signed vs unsigned: `CHAT_COMMAND_SIGNED` (mapped to old `CHAT_COMMAND`) is separate from the new unsigned `CHAT_COMMAND` ([wiki 1.20.5]; source `:139,175`). Chat-signature handling moved behind a `secureChatEnforced` flag that now arrives in `SERVER_DATA` and is replayed via `AcknowledgedMessagesStorage` (`:106198, 263276`).
- **Login `HELLO`** gained a trailing `Authenticate` boolean (`:99104`); `LOGIN_FINISHED` gained a trailing strict-error-handling boolean (`:202207`).
- **Strict error handling** — invalid packet data now disconnects the client by default; Via exposes `viaversion.strict-error-handling1_20_5` to opt out for modded servers (`:7476`). Matches the wiki's "invalid packet data now causes client disconnection (opt-out for modded)".
- **`DEBUG_SAMPLE_SUBSCRIPTION`** new serverbound packet (cancelled downward) — `:260`.
**1.20.6** shares protocol 766 with 1.20.5 (a stability hotfix); no separate ViaVersion package. `data/pc/1.20.6/version.json``{"version":766,"minecraftVersion":"1.20.6"}`.
ViaVersion git log for `v1_20_3to1_20_5` (selected):
| Commit | Subject |
|---|---|
| `a09fa97f4` | Send new entries for fully missing registries |
| `b3d560bd3` | Track tags for adventure-mode predicates again in 1.20.3→1.20.5 |
| `a66dbf77a` | Fix profile with multiple properties of the same key in 1.20.5→1.20.3 (#4909) |
| `31f257fee` | Register empty handlers for transient data components in 1.20.5→1.20.3 |
| `c267a754e` | Always send extra attributes in 1.20.5 (#4742) |
| `7657a59b0` | Add step_height and version-dependent interaction-range 1.20.5 attributes (#4741) |
| `b54a87f46` | Re-shuffle mapping files to reduce size, include sound identifiers |
| `e63c806d6` | Fix custom potion effects translation in 1.20.3→1.20.5 (#4858) |
---
## Proxy / forwarding & translation impact
### The Configuration state is non-optional for proxies (764+)
This is the load-bearing takeaway of the whole 1.20 line. **Any proxy that wants to support 1.20.2+ clients MUST implement the Configuration state.** It is not a passthrough nicety — the connection genuinely sits in a distinct state between Login and Play, with its own packet ID space, its own `FINISH_CONFIGURATION` boundary, and a server-initiated **re-configuration** loop (`START_CONFIGURATION` Play→Config and back). A proxy must:
1. Recognise the **Login Acknowledged** packet as the end of Login and switch its own state tracking to Configuration (not Play).
2. Track which Configuration-state packet IDs apply (the tables above) — they are *not* the Play IDs.
3. Bridge backend server switches through Configuration: on a backend swap, Velocity/BungeeCord push the player back into Configuration, swap the registries/tags, then re-finish into Play. (Velocity's bridge logic is covered in [`../06-configuration.md`](../06-configuration.md) §5; lifecycle in [`../02-connection-lifecycle.md`](../02-connection-lifecycle.md) §67.)
ViaVersion, which is a *translation* layer rather than a state-aware proxy, has to go further still: when the backend speaks 763 (no Configuration), it **fabricates** the entire Configuration exchange toward the client — queueing Play packets from the old server, synthesising `REGISTRY_DATA` + tags + `FINISH_CONFIGURATION`, replaying the client's settings, and bounding the queue. The four-value `BridgePhase` state machine (`ConfigurationState.java:167169`) is the canonical reference implementation of "fake a Configuration state for a server that doesn't have one."
### Structured-data must be downgraded to NBT (766↔765)
For item stacks, a proxy/translation layer bridging a 766 server to a ≤765 client must convert the **typed structured-component map back to the legacy `tag` NBT compound** (and the reverse when going up). This is lossy/complex enough that ViaVersion devotes an entire class to it (`StructuredDataConverter.java`) plus per-version data tables for attributes, enchantments, potions, banner patterns, map decorations, armor-trim materials/patterns, instruments, and max-stack-size (`v1_20_3to1_20_5/data/*`). A proxy that only forwards raw bytes will hand a 765 client component-format items it cannot parse. The armor-trim registries in particular must be tracked from `REGISTRY_DATA` (or fall back to the vanilla defaults in `ArmorTrimStorage`) so trim indices resolve correctly.
### Known Packs must be answered, even if trivially (766)
A 766 client will wait for `SELECT_KNOWN_PACKS` before finalising its registries. A proxy bridging from a server that doesn't send it must **synthesise an empty Known-Packs selection** (as ViaVersion does at `EntityPacketRewriter1_20_5.java:98101`) so the client falls back to taking all registry data off the wire. Failing to do so leaves the client stuck in Configuration.
### Summary table — what changed per bump
| Protocol | Release(s) | Headline wire change | New connection mechanic | Proxy must… |
|---|---|---|---|---|
| 763 | 1.20 / 1.20.1 | Content remap only (reuses 1.19.4 enums) | — | Refresh block/item/entity ID maps; drop combat-packet leading entity-ID |
| 764 | 1.20.2 | **Configuration state inserted Login→Play**; registry/tags/features/pack move there | **Configuration state**; Login Acknowledged ends Login; Finish Configuration enters Play; re-configuration loop | **Implement Configuration state**; recognise Login Acknowledged; bridge backend switches through Config |
| 765 | 1.20.3 / 1.20.4 | Components serialised as NBT not JSON; multi-pack push/pop; score reset/number-format | — | Convert JSON↔NBT components; map single resource pack ↔ push/pop with UUIDs |
| 766 | 1.20.5 / 1.20.6 | **Structured item components replace `tag` NBT**; cookies/transfer; signed-command split; strict errors | **Known Packs handshake** in Configuration; cookies persist across transfer | **Downgrade structured components ↔ NBT**; **answer Known Packs** (empty selection ok); handle cookies/transfer |
---
## VERIFY flags
- `<!-- VERIFY -->` Exact release dates for **1.20.1** (used 2023-06-12) and **1.20.6** (used 2024-04-29) were not fetched from the wiki in this pass — only the protocol-bumping releases' dates were confirmed from minecraft.wiki articles. Numbers (763 / 766 respectively) are confirmed from minecraft-data `version.json`.
- The 1.20.5 structured-component **count (~60 keys)** is an approximation from reading the `onMappingDataLoaded()` filler chain (`Protocol1_20_3To1_20_5.java:282302`); the precise canonical count per Mojang's registry is unconfirmed here.
## Sources
- minecraft.wiki release articles (all fetched 2026-06-19): [1.20](https://minecraft.wiki/w/Java_Edition_1.20), [1.20.2](https://minecraft.wiki/w/Java_Edition_1.20.2), [1.20.3](https://minecraft.wiki/w/Java_Edition_1.20.3), [1.20.4](https://minecraft.wiki/w/Java_Edition_1.20.4), [1.20.5](https://minecraft.wiki/w/Java_Edition_1.20.5).
- minecraft.wiki protocol pages (accessed 2026-06-19): [Registry Data](https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Registry_Data), [Registries](https://minecraft.wiki/w/Java_Edition_protocol/Registries).
- ViaVersion source `/tmp/mcproto-refs/ViaVersion/` — packages `v1_19_4to1_20`, `v1_20to1_20_2`, `v1_20_2to1_20_3`, `v1_20_3to1_20_5`, plus `protocols/base/` login enums and `api/.../minecraft/data/StructuredDataKey.java`. Commits via `git -C /tmp/mcproto-refs/ViaVersion log --oneline -- <package>`.
- minecraft-data `/tmp/mcproto-refs/minecraft-data/data/pc/``version.json` (protocol numbers) and `protocol.json` (state sets / packet counts) for 1.19.4, 1.20, 1.20.11.20.6.
- Cross-references: [`../02-connection-lifecycle.md`](../02-connection-lifecycle.md), [`../06-configuration.md`](../06-configuration.md), [`INDEX.md`](INDEX.md), [`../07-version-differences.md`](../07-version-differences.md).
+347
View File
@@ -0,0 +1,347 @@
# Java Edition 1.21.x — "Tricky Trials" and the Long Patch Series
**Protocol numbers:** 767 (1.21/1.21.1) · 768 (1.21.2/1.21.3) · 769 (1.21.4) · 770 (1.21.5) · 771 (1.21.6) · 772 (1.21.7/1.21.8) · 773 (1.21.9/1.21.10) · 774 (1.21.11)
**Release span:** June 13, 2024 (1.21) — December 9, 2025 (1.21.11) — 8 distinct protocol versions across 11 game releases.
Sources:
- MCWIKI: `https://minecraft.wiki/w/Java_Edition_1.21[.X]` (fetched 2026-06-19)
- VV: `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_20_5to1_21/``v1_21_9to1_21_11/` (Protocol*.java + packet enums)
- VV-LOG: `git -C /tmp/mcproto-refs/ViaVersion log --oneline -- common/.../protocols/<pkg>` (fetched 2026-06-19)
- MD: `/tmp/mcproto-refs/minecraft-data/data/pc/1.21[.x]/version.json`
---
## Headline
1.21 "Tricky Trials" (June 2024) was the major combat/adventure update: trial chambers, breeze mob, mace weapon, wind charge, crafter block, copper and tuff block variants, four new status effects. Patch releases after it were unusually numerous — 10 patches over 18 months spanning the "Wild West" (1.21.2: bundles), "Winter Drop" (1.21.4: pale garden/creaking), animal variants (1.21.5), "Chase the Skies" (1.21.6: happy ghasts, dialog system), "Copper Age" (1.21.9: copper golem/equipment), and "Mounts of Mayhem" (1.21.11: spears, nautilus, dimension attribute overhaul). Each sub-update bumped the protocol, producing 8 distinct wire versions inside a single major release line.
---
## Protocol bump summary
| Protocol | MC versions | Released | Key protocol change |
|---|---|---|---|
| 767 | 1.21, 1.21.1 | 2024-06-13, 2024-08-08 | First 1.21 wire (attribute UUID→ID remap, `PROJECTILE_POWER` scalar) |
| 768 | 1.21.2, 1.21.3 | 2024-10-22, 2024-10-23 | Major: `ENTITY_POSITION_SYNC` + `MOVE_MINECART_ALONG_TRACK` new; serverbound `CLIENT_TICK_END` + `BUNDLE_ITEM_SELECTED`; full item component rebuild; new entity types (Salmon sizes, etc.) |
| 769 | 1.21.4 | 2024-12-03 | `PICK_ITEM` split into `PICK_ITEM_FROM_BLOCK`/`PICK_ITEM_FROM_ENTITY`; `LEVEL_PARTICLES` gets `always_show` field; `CUSTOM_MODEL_DATA` format extended (`1_21_4`); `PROFILE_ACTIONS_ENUM` gains "show hat" bit |
| 770 | 1.21.5 | 2025-03-25 | `ADD_EXPERIENCE_ORB` removed (merged into `ADD_ENTITY`); `PLAYER_ROTATION` packet added; `PLAYER_CHAT` gets message-index prefix; serverbounds: `SET_TEST_BLOCK` + `TEST_INSTANCE_BLOCK_ACTION` (test framework); `PLAYER_LOADED` new; large item component churn (`UNBREAKABLE`, `ENCHANTMENTS`, `ATTRIBUTE_MODIFIERS`, `EQUIPPABLE`, `INSTRUMENT`, `JUKEBOX_PLAYABLE`, `TRIM`, many variants bump to `1_21_5`) |
| 771 | 1.21.6 | 2025-06-17 | Dialog system: `CLEAR_DIALOG` + `SHOW_DIALOG` new (play + config); `CUSTOM_CLICK_ACTION` new serverbound (play + config); `TRACKED_WAYPOINT` new; `CHANGE_DIFFICULTY` changes type (unsigned byte → varint); `CHANGE_GAME_MODE` new serverbound; sneak/shift actions removed from `PLAYER_COMMAND` (now absorbed into `PLAYER_INPUT`); `ATTRIBUTE_MODIFIERS``1_21_6`, `EQUIPPABLE``1_21_6` |
| 772 | 1.21.7, 1.21.8 | 2025-06-30, 2025-07-17 | Incremental: entity/item data remapping only (block/item ID changes); no new packet types; 1.21.8 was graphics bug-fix only |
| 773 | 1.21.9, 1.21.10 | 2025-09-30, 2025-10-07 | `ADD_ENTITY` velocity field type changed (short×3 → `LOW_PRECISION_VECTOR`); `SET_ENTITY_MOTION` same; `PLAYER_ROTATION` gets "relative" boolean fields; four new `DEBUG_*` packets (`DEBUG_BLOCK_VALUE`, `DEBUG_CHUNK_VALUE`, `DEBUG_ENTITY_VALUE`, `DEBUG_EVENT`); `GAME_EVENT_TEST_HIGHLIGHT_POS` new; `EXPLODE` restructured (radius+count fields moved to `PROJECTILE_POWER`); config: `CODE_OF_CONDUCT` added; copper golem entity + copper equipment block/item IDs |
| 774 | 1.21.11 | 2025-12-09 | `HORSE_SCREEN_OPEN``MOUNT_SCREEN_OPEN` rename; new entity types (`zombie_nautilus`, new mounts); `zombie_nautilus_variant` + `timeline` registries injected at FINISH_CONFIGURATION; massive `dimension_type` field migration (many fields → `attributes` NBT compound); `worldgen/biome` similarly gains `attributes`; new item components: `ATTACK_RANGE`, `USE_EFFECTS`, `MINIMUM_ATTACK_CHARGE`, `DAMAGE_TYPE`, `PIERCING_WEAPON`, `KINETIC_WEAPON`, `SWING_ANIMATION`, `ZOMBIE_NAUTILUS_VARIANT` |
---
## 767 — 1.21 / 1.21.1 (June 13 / August 8, 2024)
### Gameplay headline
Tricky Trials: trial chambers, breeze mob, mace+heavy core, wind charge, crafter block, copper/tuff variant blocks, four status effects (Wind Charged, Weaving, Oozing, Infested). First release requiring 64-bit OS and Java 21+. [MCWIKI 1.21](https://minecraft.wiki/w/Java_Edition_1.21)
**1.21.1** hotfixed two security exploits (update suppression block-entity swap; server crash via malicious command-completion target selector causing stack exhaustion) plus two gameplay bugs. Same protocol 767. Servers running 1.21.1 are compatible with 1.21 clients. [MCWIKI 1.21.1](https://minecraft.wiki/w/Java_Edition_1.21.1)
### Protocol changes (766→767)
VV package: `v1_20_5to1_21/` ([`Protocol1_20_5To1_21.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_20_5to1_21/Protocol1_20_5To1_21.java))
**Play state — clientbound (0x000x7B):** Packet set identical in count/IDs to 1.20.5 (`ClientboundPackets1_21` enum ends at `SERVER_LINKS` 0x7B = 124 entries). Notable: `PROJECTILE_POWER` (0x79) already existed as of 1.20.5 but its payload changed — 1.21 converts the separate x/y/z doubles (three fields) into a single `acceleration_power` scalar (magnitude of the velocity vector). VV does this inline: `Protocol1_20_5To1_21.java:registerPackets()` reads the three doubles, computes `sqrt(x²+y²+z²)`, writes one double. [VV `Protocol1_20_5To1_21.java:registerPackets()`]
**Attribute modifiers:** UUID-keyed modifiers in `UPDATE_ATTRIBUTES` replaced by string IDs. VV maps UUID→string via `AttributeModifierMappings1_21`. Bidirectional mapping: server sends string IDs, old clients need UUIDs, VV translates. [VV `Protocol1_20_5To1_21.java:registerPackets()`, `AttributeModifierMappings1_21`]
**Chat type holder:** `DISGUISED_CHAT` and `PLAYER_CHAT` chat-type field changed from plain varint to `Holder<ChatType>` (inline or registry reference). VV wraps the varint into `ChatType.TYPE` holder. [VV `Protocol1_20_5To1_21.java:registerPackets()`]
**Item components:** The 1.20.5 component set carries forward. VV's `VersionedTypes.V1_21.structuredData` filler enumerates which components exist at this version — all keys use `1_20_5`-suffixed variants except `FOOD1_21`, `JUKEBOX_PLAYABLE1_21`, `ATTRIBUTE_MODIFIERS1_21`. [VV `Protocol1_20_5To1_21.java:onMappingDataLoaded()`]
**New entity tags:** `blocks_wind_charge_explosions` (block), several entity tags (`can_turn_in_boats`, `deflects_projectiles`, `immune_to_infested`, `immune_to_oozing`, `no_anger_from_wind_charge`). Enchantment tags added (`curse`, `double_trade_price`, `in_enchanting_table`, etc.). [VV `Protocol1_20_5To1_21.java:onMappingDataLoaded()`]
---
## 768 — 1.21.2 / 1.21.3 (October 22 / October 23, 2024)
### Gameplay headline
Bundles (dyeable, store up to 64 mixed items), new banner patterns (field masoned, bordure indented). Experimental content: Minecart Improvements, Redstone Experiments, Winter Drop (pale garden biome, creaking mob) gated behind feature flags. Significant ender pearl chunk-loading change. [MCWIKI 1.21.2](https://minecraft.wiki/w/Java_Edition_1.21.2)
**1.21.3** hotfixed salmon size regression (small salmon incorrectly downsized on world load) and Realms resource-pack failure. Same protocol 768. [MCWIKI 1.21.3](https://minecraft.wiki/w/Java_Edition_1.21.3)
MD confirms: `data/pc/1.21.3/version.json` = `768`.
### Protocol changes (767→768)
VV package: `v1_21to1_21_2/` ([`Protocol1_21To1_21_2.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21to1_21_2/Protocol1_21To1_21_2.java))
This is the most substantial bump in the 1.21 line.
#### New clientbound play packets
- **`ENTITY_POSITION_SYNC` (0x20):** New packet for authoritative entity position sync (replaces the indirect position-via-teleport path for most entities). All subsequent packet IDs shift up by 1. [VV `ClientboundPackets1_21_2.java:0x20`]
- **`MOVE_MINECART_ALONG_TRACK` (0x31):** New dedicated minecart movement packet for the experimental Minecart Improvements feature. [VV `ClientboundPackets1_21_2.java:0x31`]
- **`SET_CURSOR_ITEM` (0x5A):** Sends the item on the cursor (held item in inventory GUI). Was previously implicit. [VV `ClientboundPackets1_21_2.java:0x5A`]
- **`SET_HELD_SLOT` (0x63):** Replaces `SET_CARRIED_ITEM` rename. [VV `ClientboundPackets1_21_2.java:0x63`]
- **`SET_PLAYER_INVENTORY` (0x66):** Sends a specific slot of the player inventory. [VV `ClientboundPackets1_21_2.java:0x66`]
Total play clientbound count grows from 124 (0x7B) to 131 (0x82).
#### New serverbound play packets
- **`BUNDLE_ITEM_SELECTED` (0x02):** Client reports which item inside a bundle was selected. VV cancels this (no equivalent in 1.21). [VV `Protocol1_21To1_21_2.java:cancelServerbound()`]
- **`CLIENT_TICK_END` (0x0B):** Client sends at end of each tick. VV cancels. [VV `Protocol1_21To1_21_2.java:cancelServerbound()`]
#### Packet field changes
- **`LOGIN_FINISHED` (login state):** `strict_error_handling` boolean field removed. VV reads and discards it. [VV `Protocol1_21To1_21_2.java:registerPackets()`]
- **`SET_TIME`:** `doDaylightCycle` was encoded as negative day-time; now explicit `doDaylightCycle: boolean` field added. VV converts the sign-encoding to explicit field. [VV `Protocol1_21To1_21_2.java:registerPackets()`]
- **`PLAYER_INFO_UPDATE`:** `PROFILE_ACTIONS_ENUM` updated to `1_21_2` variant. Display-name entries now include component rewriting. [VV `Protocol1_21To1_21_2.java:replaceClientbound()`]
- **`UPDATE_ATTRIBUTES`:** Attribute modifier format: UUID field removed, modifiers now have string ID only (already done in 767 serverside, now serverbound clean-up). VV tracks `generic.max_health` to compute max health for 1.21 clients. [VV `Protocol1_21To1_21_2.java:appendClientbound()`]
#### Item component changes (notable)
- `DAMAGE_RESISTANT``DAMAGE_RESISTANT1_21_2` (format change)
- `FOOD``FOOD1_21_2` (split into food + consumable components)
- `CONSUMABLE1_21_2`, `USE_COOLDOWN`, `EQUIPPABLE1_21_2`, `ITEM_MODEL`, `GLIDER`, `TOOLTIP_STYLE`, `DEATH_PROTECTION` added
- `REPAIRABLE`, `ENCHANTABLE` added
- `LOCK``LOCK1_21_2`, `POTION_CONTENTS``POTION_CONTENTS1_21_2`, `INSTRUMENT``INSTRUMENT1_21_2`, `TRIM``TRIM1_21_2`
[VV `Protocol1_21To1_21_2.java:onMappingDataLoaded()`]
#### Damage types added
`ender_pearl` and `mace_smash` damage types added to registry. [VV `Protocol1_21To1_21_2.java:registryDataRewriter()`]
#### Chunk-load workaround
1.21.2 introduced a rendering bug: if a loaded chunk received new data without being unloaded first, it wouldn't render correctly. VV injects a `ChunkLoadTracker` that forces chunk unload before re-send, only for 1.21.2 clients (not 1.21.3 which got the 1.21.2 packet set, as 1.21.4 fixed the bug). [VV `Protocol1_21To1_21_2.java:init()`]
#### `CLIENT_INFORMATION`
New `particle_status` varint field added at end. VV reads and discards it when translating to 1.21. [VV `Protocol1_21To1_21_2.java:clientInformation()`]
---
## 769 — 1.21.4 (December 3, 2024)
### Gameplay headline
Pale garden biome + pale oak wood set; creaking heart block; creaking mob (freezes when observed); eyeblossom; resin blocks. Spawn damage immunity removed. Water Breathing/Conduit Power now restore oxygen. [MCWIKI 1.21.4](https://minecraft.wiki/w/Java_Edition_1.21.4)
### Protocol changes (768→769)
VV package: `v1_21_2to1_21_4/` ([`Protocol1_21_2To1_21_4.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21_2to1_21_4/Protocol1_21_2To1_21_4.java))
Relatively lightweight protocol bump — no new packet types, primarily field changes and item-component format updates.
- **`PICK_ITEM` split:** Serverbound `PICK_ITEM` (0x22) split into `PICK_ITEM_FROM_BLOCK` (0x22) and `PICK_ITEM_FROM_ENTITY` (0x23). VV provides a `PickItemProvider` to handle the split. [VV `Protocol1_21_2To1_21_4.java:register()`]
- **`LEVEL_PARTICLES`:** `always_show` boolean field added after `override_limiter`. VV writes `false` for it when translating down. [VV `Protocol1_21_2To1_21_4.java:registerPackets()` `replaceClientbound(LEVEL_PARTICLES)`]
- **`PLAYER_INFO_UPDATE`:** `PROFILE_ACTIONS_ENUM``1_21_4` variant (adds "show hat" action). VV writes `true` by default. [VV `Protocol1_21_2To1_21_4.java:registerPackets()`]
- **Item components:** `CUSTOM_MODEL_DATA``1_21_4` (format extended), `TRIM``1_21_4`.
- **`PLAYER_LOADED` serverbound:** Added (0x2A in 1.21.5 but arrives at 768→769 boundary). <!-- VERIFY exact version introduced -->
- VV does NOT add a `ChunkLoadTracker` for 1.21.4 clients — Mojang fixed the rendering bug in this release. [VV `Protocol1_21To1_21_2.java:init()` comment]
---
## 770 — 1.21.5 (March 25, 2025)
### Gameplay headline
Farm animal variants (cold/warm/temperate pigs, cows, chickens with biome-based spawning), new decorative plants (leaf litter, wildflowers, bushes, firefly bushes), wolf sound variants (7). Game test framework (`/test` command, test blocks). Text components switch from JSON to SNBT. [MCWIKI 1.21.5](https://minecraft.wiki/w/Java_Edition_1.21.5)
### Protocol changes (769→770)
VV package: `v1_21_4to1_21_5/` ([`Protocol1_21_4To1_21_5.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21_4to1_21_5/Protocol1_21_4To1_21_5.java))
This is one of the larger protocol bumps: packet added/removed and major item-component churn.
#### Packet additions and removals
**Clientbound play:**
- **`ADD_EXPERIENCE_ORB` removed.** Experience orbs now spawn via the regular `ADD_ENTITY` packet (entity type `experience_orb`). All subsequent IDs shift. [VV `ClientboundPackets1_21_5.java` — no `ADD_EXPERIENCE_ORB` entry; 1.21 enum had it at 0x02]
- **`PLAYER_ROTATION` (0x42) added:** Server sends player rotation authoritative update. Separate from `PLAYER_POSITION`. [VV `ClientboundPackets1_21_5.java:0x42`]
- **`TEST_INSTANCE_BLOCK_STATUS` (0x77):** New packet for the game test framework. [VV `ClientboundPackets1_21_5.java:0x77`]
**Serverbound play (vs 1.21.4):**
- `PICK_ITEM_FROM_BLOCK` (0x22), `PICK_ITEM_FROM_ENTITY` (0x23) — carried from 769
- **`PLAYER_LOADED` (0x2A):** New — client signals it has loaded the world. [VV `ServerboundPackets1_21_5.java:0x2A`]
- **`SET_TEST_BLOCK` (0x39):** Test framework. VV cancels. [VV `Protocol1_21_4To1_21_5.java:cancelServerbound()`]
- **`TEST_INSTANCE_BLOCK_ACTION` (0x3D):** Test framework. VV cancels. [VV `Protocol1_21_4To1_21_5.java:cancelServerbound()`]
#### Packet field changes
- **`PLAYER_CHAT`:** Message-index varint added as first field. VV synthesizes this using `MessageIndexStorage` (tracks per-connection index). [VV `Protocol1_21_4To1_21_5.java:replaceClientbound(PLAYER_CHAT)`, `MessageIndexStorage`]
- **`CHAT_COMMAND_SIGNED` and `CHAT` (serverbound):** `checksum: byte` field added at end. VV reads and drops it. [VV `Protocol1_21_4To1_21_5.java:registerServerbound(CHAT_COMMAND_SIGNED)`, `registerServerbound(CHAT)`]
- **`wolf_variant` registry:** `assets` field restructured — `wild_texture`, `tame_texture`, `angry_texture` moved under `assets` compound; `biomes` field removed. VV remaps on registry send. [VV `Protocol1_21_4To1_21_5.java:registryDataRewriter` handler for `wolf_variant`]
#### Item component changes (major churn)
Many component keys changed to `1_21_5`-suffixed variants:
- `UNBREAKABLE``1_21_5`, `ENCHANTMENTS``1_21_5`, `STORED_ENCHANTMENTS``1_21_5`
- `DYED_COLOR``1_21_5`, `TRIM``1_21_5`, `ATTRIBUTE_MODIFIERS``1_21_5`
- `EQUIPPABLE``1_21_5`, `TOOL``1_21_5`, `INSTRUMENT``1_21_5`, `JUKEBOX_PLAYABLE``1_21_5`
- **New components added:** `BLOCKS_ATTACKS1_21_5` (shield-like), `PROVIDES_BANNER_PATTERNS1_21_5`, `TOOLTIP_DISPLAY`, `WEAPON`, `POTION_DURATION_SCALE`, `PROVIDES_TRIM_MATERIAL1_21_5`, `BREAK_SOUND`
- **Mob-variant components as items:** `VILLAGER_VARIANT`, `WOLF_VARIANT`, `WOLF_COLLAR`, `FOX_VARIANT`, `SALMON_SIZE`, `PARROT_VARIANT`, `TROPICAL_FISH_*`, `MOOSHROOM_VARIANT`, `RABBIT_VARIANT`, `PIG_VARIANT`, `FROG_VARIANT`, `HORSE_VARIANT`, `PAINTING_VARIANT`, `LLAMA_VARIANT`, `AXOLOTL_VARIANT`, `CAT_VARIANT`, `CAT_COLLAR`, `SHEEP_COLOR`, `SHULKER_COLOR`, `COW_VARIANT`, `CHICKEN_VARIANT1_21_5`, `WOLF_SOUND_VARIANT` — all bucket/spawn-egg entity variant data is now carried as item components.
[VV `Protocol1_21_4To1_21_5.java:onMappingDataLoaded()`]
#### Smithing trim recipe slot display change
Trim pattern slot display format changed — VV substitutes a placeholder (holder index 0) when translating smithing-trim recipe packets down. [VV `Protocol1_21_4To1_21_5.java` `recipeRewriter` anonymous class]
---
## 771 — 1.21.6 (June 17, 2025)
### Gameplay headline
"Chase the Skies" — rideable happy ghasts (up to 4 riders, colored harnesses); locator bar (shows other players' direction/distance in HUD); mob-to-mob leads; craftable saddles; waypoints. Dialog system (server-controlled modal UI). [MCWIKI 1.21.6](https://minecraft.wiki/w/Java_Edition_1.21.6)
### Protocol changes (770→771)
VV package: `v1_21_5to1_21_6/` ([`Protocol1_21_5To1_21_6.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21_5to1_21_6/Protocol1_21_5To1_21_6.java))
#### New packets
**Clientbound play:**
- **`TRACKED_WAYPOINT` (0x83):** Waypoint tracking for the locator bar feature. [VV `ClientboundPackets1_21_6.java:0x83`]
- **`CLEAR_DIALOG` (0x84):** Dismiss the current server dialog. [VV `ClientboundPackets1_21_6.java:0x84`]
- **`SHOW_DIALOG` (0x85):** Display a dialog to the player. [VV `ClientboundPackets1_21_6.java:0x85`]
**Configuration state — clientbound:**
- **`CLEAR_DIALOG` (0x11)** and **`SHOW_DIALOG` (0x12)** also appear in Configuration state — dialogs can be shown during config phase. [VV `ClientboundConfigurationPackets1_21_6.java:0x11,0x12`]
**Serverbound play:**
- **`CHANGE_GAME_MODE` (0x04):** New packet for switching game mode. VV translates this to `CHAT_COMMAND` (`/gamemode <name>`) for 1.21.5 servers. [VV `Protocol1_21_5To1_21_6.java:registerServerbound(CHANGE_GAME_MODE)`]
- **`CUSTOM_CLICK_ACTION` (0x41):** New serverbound packet sent when a player interacts with a dialog element. VV cancels (no equivalent). [VV `Protocol1_21_5To1_21_6.java:cancelServerbound(CUSTOM_CLICK_ACTION)`]
#### Packet field changes
- **`CHANGE_DIFFICULTY`:** Difficulty value changes type from `unsigned byte` to `varint`. VV converts both directions. [VV `Protocol1_21_5To1_21_6.java:registerClientbound(CHANGE_DIFFICULTY)`, `registerServerbound(CHANGE_DIFFICULTY)`]
- **`PLAYER_COMMAND`:** `press_shift_key` (action 0) and `release_shift_key` (action 1) removed — sneak is now a `PLAYER_INPUT` flag. All remaining actions shift down by 2. VV synthesizes `press_shift_key`/`release_shift_key` from `PLAYER_INPUT`'s sneak flag, using `SneakStorage` to track state changes. [VV `EntityPacketRewriter1_21_6.java:registerServerbound(PLAYER_COMMAND)`, `registerServerbound(PLAYER_INPUT)`]
#### Configuration: dialog registry injection
At `FINISH_CONFIGURATION`, VV synthesizes a `minecraft:dialog` registry entry (`server_links` dialog type) and sends it as `REGISTRY_DATA` — vanilla servers don't send this so older clients wouldn't see server links dialogs. [VV `EntityPacketRewriter1_21_6.java:appendClientbound(FINISH_CONFIGURATION)`]
#### Dimension type: cloud_height defaults
VV patches dimension-type registry entries to inject a default `cloud_height: 192` for overworld-like dimensions, since 1.21.6 clients render clouds based on this field. [VV `Protocol1_21_5To1_21_6.java:registryDataRewriter` handler for `dimension_type`]
#### Item components
- `ATTRIBUTE_MODIFIERS``1_21_6`, `EQUIPPABLE``1_21_6`
[VV `Protocol1_21_5To1_21_6.java:onMappingDataLoaded()`]
---
## 772 — 1.21.7 / 1.21.8 (June 30 / July 17, 2025)
### Gameplay headline
**1.21.7:** New music disc "Lava Chicken" (Hyper Potions, from chicken jockey kill); new painting "Dennis" (3×3, A Minecraft Movie tie-in). 16 bug fixes (AMD/Qualcomm rendering, texture atlas leaks). [MCWIKI 1.21.7](https://minecraft.wiki/w/Java_Edition_1.21.7)
**1.21.8:** Graphics bug-fix only — Intel Gen11 shading errors, inventory item-color glitches on Intel integrated graphics. Same protocol 772. [MCWIKI 1.21.8](https://minecraft.wiki/w/Java_Edition_1.21.8)
### Protocol changes (771→772)
VV package: `v1_21_6to1_21_7/` ([`Protocol1_21_6To1_21_7.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21_6to1_21_7/Protocol1_21_6To1_21_7.java))
Purely incremental — **no new packet types**. Same clientbound/serverbound enum as 771. VV's `Protocol1_21_6To1_21_7` class has an empty `registerPackets()` body; translation is limited to block/item/entity ID remapping and dialog-packet item rewriting. [VV `Protocol1_21_6To1_21_7.java:registerPackets()`]
The `git log` shows `6b74116ff Handle item changes in dialog packets` as the main content commit — items embedded in `SHOW_DIALOG` / `CLEAR_DIALOG` needed remapping for the new block/item IDs. [VV-LOG `v1_21_6to1_21_7`]
---
## 773 — 1.21.9 / 1.21.10 (September 30 / October 7, 2025)
### Gameplay headline
"The Copper Age" — copper golem mob, copper tools/armor (stone-tier, high enchantability), copper chests (with oxidation), shelves (all wood types), copper lanterns/chains/bars. Chat drafts persist. New game rules (portal control, mob spawn). [MCWIKI 1.21.9](https://minecraft.wiki/w/Java_Edition_1.21.9)
**1.21.10:** Five bug fixes from 1.21.9 (wind charge collision, TP chunk loading, piston/cobweb entity clip). Same protocol 773. [MCWIKI 1.21.10](https://minecraft.wiki/w/Java_Edition_1.21.10)
### Protocol changes (772→773)
VV package: `v1_21_7to1_21_9/` ([`Protocol1_21_7To1_21_9.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21_7to1_21_9/Protocol1_21_7To1_21_9.java))
This is a notably large bump: movement encoding overhaul, explosion restructure, debug packet additions.
#### Entity velocity encoding change
- **`ADD_ENTITY`:** Entity initial velocity previously encoded as three signed shorts (`velX`, `velY`, `velZ` — each = velocity × 8000, clamped). Now encoded as `LOW_PRECISION_VECTOR` (a compact 3-component float vector type). VV translates: reads the old shorts, reconstructs the vector. [VV `EntityPacketRewriter1_21_9.java:replaceClientbound(ADD_ENTITY)`]
- **`SET_ENTITY_MOTION`:** Same change — shorts → `LOW_PRECISION_VECTOR`. [VV `EntityPacketRewriter1_21_9.java:registerClientbound(SET_ENTITY_MOTION)`]
- **`PLAYER_ROTATION`:** Gets two `relative: boolean` flags (one per axis). VV writes `false` for both when translating down. [VV `EntityPacketRewriter1_21_9.java:registerClientbound(PLAYER_ROTATION)`]
#### Explosion restructure
`EXPLODE` packet restructured: the explosion radius and affected-block count (previously sent inline) are now sourced from `PROJECTILE_POWER`'s tracked state. VV emits the old-format explosion packet using values from `LastExplosionPowerStorage`. A new `block_particles` list field appended (VV writes count=0). [VV `Protocol1_21_7To1_21_9.java:replaceClientbound(EXPLODE)`]
#### New debug packets (clientbound play)
Four new debug visualisation packets added (likely tied to the game test framework):
- **`DEBUG_BLOCK_VALUE` (0x1A)**
- **`DEBUG_CHUNK_VALUE` (0x1B)**
- **`DEBUG_ENTITY_VALUE` (0x1C)**
- **`DEBUG_EVENT` (0x1D)**
And **`GAME_EVENT_TEST_HIGHLIGHT_POS` (0x27):** Highlight test positions in the world. [VV `ClientboundPackets1_21_9.java`]
All existing IDs shift up accordingly — packet count goes from 134 (0x85) in 772 to 139 (0x8A) in 773.
#### Configuration state changes
- **`CODE_OF_CONDUCT` (0x13)** added to serverbound Configuration packets (client accepts/rejects). VV cancels `ACCEPT_CODE_OF_CONDUCT` when translating down. [VV `ServerboundConfigurationPackets1_21_9.java:0x09`; `Protocol1_21_7To1_21_9.java:cancelServerbound(ACCEPT_CODE_OF_CONDUCT)`]
#### Debug sample subscription
`DEBUG_SAMPLE_SUBSCRIPTION` serverbound changed: from a single `type: varint` to a list `count: varint` + per-entry `registry_id: varint`. VV maps the new `DEDICATED_SERVER_TICK_TIME` subscription (id=0) back to the old `TICK_TIME` type (0). [VV `Protocol1_21_7To1_21_9.java:registerServerbound(DEBUG_SAMPLE_SUBSCRIPTION)`]
#### `SET_DEFAULT_SPAWN_POSITION`
<!-- VERIFY: field format change in 773 vs 772 — VV entity rewriter references this packet but full diff not extracted -->
---
## 774 — 1.21.11 (December 9, 2025)
### Gameplay headline
"Mounts of Mayhem" — nautilus (tameable aquatic mount), zombie nautilus (hostile), camel husk, parched (desert skeleton), zombie horseman. Spears (7 material tiers, jab + charge attacks, lunge enchantment). Nautilus armor. Netherite horse armor. Overhaul of dimension type and biome data (environment attributes system). [MCWIKI 1.21.11](https://minecraft.wiki/w/Java_Edition_1.21.11)
### Protocol changes (773→774)
VV package: `v1_21_9to1_21_11/` ([`Protocol1_21_9To1_21_11.java`](/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21_9to1_21_11/Protocol1_21_9To1_21_11.java))
This is the most structurally significant bump of the 1.21 line from a proxy standpoint.
#### Packet rename
- **`HORSE_SCREEN_OPEN``MOUNT_SCREEN_OPEN`** (same ID 0x28; name change only reflects expanded mount support). VV registers a direct remap. [VV `EntityPacketRewriter1_21_11.java:registerClientbound(HORSE_SCREEN_OPEN → MOUNT_SCREEN_OPEN)`]
#### New entity type
- **`zombie_nautilus`** entity type added. New entity-data type `zombieNautilusVariantType` registered. [VV `EntityPacketRewriter1_21_11.java:registerRewrites()`, `EntityTypes1_21_11`]
#### Registry injection at FINISH_CONFIGURATION
VV injects two entirely new registries before the config-phase completes, so older servers' clients see them:
- **`zombie_nautilus_variant`** registry — VV synthesizes a single entry (`minecraft:pale` with `temperate` asset ID). [VV `Protocol1_21_9To1_21_11.java:registerPackets()`, `appendClientbound(FINISH_CONFIGURATION)`]
- **`timeline`** registry — an experimental feature allowing time-based behavior modifications. VV synthesizes entries from `MAPPINGS.timelineRegistry()`. [VV `Protocol1_21_9To1_21_11.java:registerPackets()`]
#### `dimension_type` registry overhaul (massive)
The `dimension_type` data format was significantly restructured — many fields extracted into a new `attributes` compound NBT sub-object. VV performs full field-level migration for each dimension type entry:
- `fixed_time``has_fixed_time` boolean
- `has_raids``attributes/"gameplay/can_start_raid"`
- `piglin_safe``attributes/"gameplay/piglins_zombify"` (inverted)
- `respawn_anchor_works``attributes/"gameplay/respawn_anchor_works"`
- `ultrawarm``attributes/"gameplay/fast_lava"` + `"gameplay/water_evaporates"`
- `cloud_height``attributes/"visual/cloud_height"` + synthesizes `"visual/cloud_color": "#ccffffff"`
- New fields added: `skybox`, `cardinal_light`, `timelines` list, `attributes/"visual/sky_light_color"`, `attributes/"visual/fog_start_distance"`, `attributes/"visual/fog_end_distance"`, `attributes/"audio/background_music"`, ambient cave sounds.
[VV `Protocol1_21_9To1_21_11.java:registryDataRewriter` `dimension_type` handler]
#### `worldgen/biome` registry overhaul
Biome registry entries similarly gain an `attributes` sub-object with all sound and visual properties migrated:
- `effects.sky_color``attributes/"visual/sky_color"` (suppressed for nether biomes)
- `effects.water_fog_color``attributes/"visual/water_fog_color"`
- `effects.fog_color``attributes/"visual/fog_color"`
- `effects.music``attributes/"audio/background_music"`
- `effects.mood_sound`/`additions_sound`/`ambient_sound``attributes/"audio/ambient_sounds"`
- `effects.particle``attributes/"visual/ambient_particles"`
[VV `Protocol1_21_9To1_21_11.java:registryDataRewriter` `worldgen/biome` handler]
#### New item components
- `ATTACK_RANGE`, `USE_EFFECTS`, `MINIMUM_ATTACK_CHARGE`, `DAMAGE_TYPE1_21_11`, `PIERCING_WEAPON`, `KINETIC_WEAPON`, `SWING_ANIMATION`, `ZOMBIE_NAUTILUS_VARIANT1_21_11`
[VV `Protocol1_21_9To1_21_11.java:onMappingDataLoaded()`]
#### Game time storage
VV adds `GameTimeStorage` to track absolute game time (needed for timeline-based behavior). [VV `Protocol1_21_9To1_21_11.java:init()`]
#### Serverbound: spear cancel
`PLAYER_ACTION` action ID 7 ("stab") added for spear jab — VV cancels these when downgrading. [VV `EntityPacketRewriter1_21_11.java:registerServerbound(PLAYER_ACTION)`]
---
## Proxy and ViaVersion translation impact
| Bump | Translation cost | Key challenge |
|---|---|---|
| 766→767 | Low | Attribute UUID↔ID bidirectional map; `PROJECTILE_POWER` scalar; chat type holder |
| 767→768 | High | 7 new clientbound packets (all IDs shift); 2 new serverbound; full item-component set rebuild; max-health tracking; chunk-load workaround for 1.21.2 clients |
| 768→769 | Medium | `PICK_ITEM` split; `LEVEL_PARTICLES` field insert; profile action enum extension |
| 769→770 | High | `ADD_EXPERIENCE_ORB` removal (XP orbs now `ADD_ENTITY`); message-index synthesis for chat; chat checksum stripping; test-framework packets cancelled; 20+ new item components including all mob-variant data |
| 770→771 | Medium | Dialog packet passthrough (play+config); sneak flag synthesis from `PLAYER_INPUT`; `PLAYER_COMMAND` action re-numbering; `CHANGE_DIFFICULTY` type conversion; dialog registry injection |
| 771→772 | Low | ID remapping only; dialog packet item rewriting |
| 772→773 | High | Entity velocity type change (`LOW_PRECISION_VECTOR` ↔ shorts) for ALL entities; explosion packet restructure; 5 new debug/test packets shifting all IDs; explosion power storage tracking |
| 773→774 | Very high | Full `dimension_type` field migration (many fields → `attributes` compound); full `worldgen/biome` field migration; two new registry types injected during config; new entity type + variant; spear action filtering |
**Proxy notes:**
- **Login state:** `LOGIN_FINISHED` lost `strict_error_handling` (768). If a proxy injects it, strip on the way down.
- **Configuration state:** Dialog packets (`CLEAR_DIALOG`, `SHOW_DIALOG`) appear in both config and play states from 771 onward. Proxies must forward these in both states.
- **`CODE_OF_CONDUCT` (config, 773):** New serverbound; cancel when forwarding to 772 servers.
- **`BUNDLE_ITEM_SELECTED` / `CLIENT_TICK_END` (768):** Cancel toward 767 servers.
- **Chunk-load (768):** 1.21.2 clients need forced chunk unload before re-send; 1.21.3/1.21.4 don't.
- **Item components:** Every bump through 770 added or renamed component keys. A proxy doing item translation must track which component key set the connected client/server uses.
- **`dimension_type` (774):** By far the most complex registry translation in the 1.21 line — the field-level migration is large enough that ViaVersion devotes ~150 lines of registry rewriting code to it alone.
+455
View File
@@ -0,0 +1,455 @@
# Minecraft Java Edition 1.7 — Protocol Deep-Dive
> **Range floor.** This line defines the oldest protocol the modern toolchain
> targets. Every proxy, translator, and compatibility layer that claims "1.7+"
> speaks the protocol described here.
---
## Header
| Sub-release range | Protocol version | Release dates | Cross-compatible |
|---|---|---|---|
| 1.7.2 | 4 | 2013-10-25 | — |
| 1.7.3 | 4 | 2013-10-26 | yes, with 1.7.2 |
| 1.7.4 | 4 | 2013-12-09 <!-- VERIFY exact date --> | yes, with 1.7.23 |
| 1.7.5 | 4 | 2014-02-26 <!-- VERIFY exact date --> | yes, with 1.7.24 |
| 1.7.6 | **5** | 2014-04-09 | **incompatible** with 1.7.25 |
| 1.7.7 | **5** | 2014-04-10 <!-- VERIFY exact date --> | yes, with 1.7.6 |
| 1.7.8 | **5** | 2014-06-16 <!-- VERIFY exact date --> | yes, with 1.7.67 |
| 1.7.9 | **5** | 2014-06-16 <!-- VERIFY exact date --> | yes, with 1.7.68 |
| 1.7.10 | **5** | 2014-06-26 | yes, with 1.7.69 |
Sources:
- minecraft.wiki `/w/Java_Edition_1.7.2` (fetched 2026-06-19) — release date, protocol 4, Netty rewrite
- minecraft.wiki `/w/Java_Edition_1.7.6` (fetched 2026-06-19) — release date 2014-04-09, protocol 5 confirmed
- minecraft.wiki `/w/Java_Edition_1.7.10` (fetched 2026-06-19) — release date 2014-06-26, protocol 5, compatibility note
- `/tmp/mcproto-refs/minecraft-data/data/pc/1.7/version.json``{"version":5,"minecraftVersion":"1.7.10","majorVersion":"1.7"}`
The protocol-version page on minecraft.wiki (fetched 2026-06-19) listed all of
1.7.21.7.10 as protocol 4; the per-version pages for 1.7.6 and 1.7.10
individually confirm protocol 5, and the minecraft-data canonical record for
`1.7.10` is version 5. The per-version pages are the higher-fidelity source;
<!-- VERIFY --> the exact boundary (whether 1.7.21.7.5 = 4 and 1.7.61.7.10 = 5
is consistent with the protocol-version list being incomplete/erroneous for the
1.7 range).
---
## 1. Headline changes — the Netty rewrite
1.7.2 ("The Update that Changed the World", released 2013-10-25) is not just a
gameplay release — it is the **architectural reset** for the entire modern
Minecraft protocol. Everything that followed is a delta on top of what 1.7.2
established.
### 1.1 Netty and length-prefixed VarInt framing
The network layer was **completely rewritten** to use [Netty](https://netty.io/),
replacing the old hand-rolled byte-stream code. The visible wire change:
```
[length: VarInt] [packet-id: VarInt] [fields...]
```
Before 1.7, each packet began with a fixed-width packet-ID byte; the receiver
had to know the exact byte length of every packet to know where the next one
started. After 1.7, **every packet is prefixed by its own byte length as a
VarInt**, making the stream self-delimiting regardless of packet content.
The minecraft.wiki 1.7.2 release article confirms "there is now a packet length
header" (fetched 2026-06-19). The `string` type likewise moved to VarInt-length
prefix (see `protocol.json` `types.string` entry,
`/tmp/mcproto-refs/minecraft-data/data/pc/1.7/protocol.json`).
### 1.2 State machine: Handshake / Status / Login / Play
Before 1.7, the connection had two informal phases (pre-login and in-game).
1.7.2 formalised this as **four explicit states**, each with its own packet
namespace and numeric IDs:
| State | Initiated by |
|---|---|
| **Handshaking** | Always the first state; client sends one packet (0x00 Set Protocol) then transitions |
| **Status** (next\_state = 1) | Server List Ping path |
| **Login** (next\_state = 2) | Authentication + encryption setup path |
| **Play** | After Login Success; all gameplay traffic |
Packet IDs are **per-state**. `0x00` in Status means something completely
different from `0x00` in Login. The pre-1.7 protocol had a flat namespace where
a single byte identified every packet across all phases — this is the deepest
structural break from the legacy protocol.
### 1.3 JSON chat
Chat messages on the wire became JSON-encoded `Text` component strings
(source: minecraft.wiki 1.7.2 article — "/tellraw enables JSON formatted
messages"; fetched 2026-06-19). The `packet_chat` payload (both S→C and C→S) is
a VarInt-length-prefixed `string` carrying JSON. This is a breaking change from
the old raw-string chat packet.
### 1.4 New Server List Ping (SLP)
The Server List Ping was redesigned. The new flow uses the **Status state**:
1. Client sends Handshake (0x00) with `next_state = 1`.
2. Client sends Status Request (0x00, no fields).
3. Server responds with Status Response (0x00) carrying a JSON string:
`{"version":{"name":"...","protocol":N},"players":{...},"description":{...},"favicon":"data:image/png;base64,..."}`
4. Client sends Ping (0x01, i64 timestamp); server echoes it back.
The old "legacy SLP" (0xFE magic byte) is preserved as
`packet_legacy_server_list_ping` in the Handshake state for pre-1.7 client
compatibility (see `protocol.json` handshaking.toServer mappings). Server icons
in the server list (`favicon` field as a base64 PNG) are new in 1.7.2
(minecraft.wiki 1.7.2; fetched 2026-06-19).
### 1.5 Packet renumbering
All packet IDs were renumbered. The pre-1.7 flat namespace had IDs like 0x01
(Login Request), 0x02 (Handshake), 0xFF (Kick Disconnect) scattered across 256
possible values. In 1.7, the Play state alone starts at 0x00 and counts up
sequentially within the state; 65 unique packet types were defined (41 S→C + 24
C→S in the 1.7.10 data — see §3).
---
## 2. Protocol 4 → Protocol 5 delta (1.7.6)
The bump from protocol 4 (1.7.21.7.5) to protocol 5 (1.7.61.7.10) coincided
with a **skin system overhaul** in 1.7.6 (released 2014-04-09;
minecraft.wiki `/w/Java_Edition_1.7.6`, fetched 2026-06-19).
Key changes at the wire level:
- **Signed skin URLs**: Skins and capes moved from a single centralised skin
server to per-player signed URLs distributed through session servers. The
`packet_named_entity_spawn` (Play 0x0C S→C) already carried a `data` array of
property `{name, value, signature}` triples in the minecraft-data 1.7 schema
— this is the textures property bag that carries the signed skin URL.
<!-- VERIFY --> whether this field was added in 1.7.2 or specifically in 1.7.6
(it is present in the 1.7.10 protocol.json which covers protocol 5).
- **Name-change infrastructure**: Server-side preparation for player name
changing (actual service launched 2015-02-04). No packet-schema change, but
UUID-keyed identity was reinforced.
- **Per-server resource pack option**: `packet_resource_pack_send` <!--
VERIFY --> exact packet ID / whether this is a new Play packet in protocol 5 or
re-uses an existing custom\_payload channel.
- The 1.7.6 client was incompatible with 1.7.21.7.5 servers (confirmed by
minecraft.wiki 1.7.6; fetched 2026-06-19), making the protocol-5 boundary
hard.
The subsequent 1.7.71.7.10 releases did not change the protocol number; they
were bug-fix and stability releases (1.7.6 had a crash that was fixed the next
day in 1.7.7).
---
## 3. Packet inventory (protocol 5 / 1.7.10)
Source: `/tmp/mcproto-refs/minecraft-data/data/pc/1.7/protocol.json`
### 3.1 Handshaking state
| ID | Direction | Name | Key fields |
|---|---|---|---|
| 0x00 | C→S | Set Protocol | `protocolVersion` (VarInt), `serverHost` (string), `serverPort` (u16), `nextState` (VarInt: 1=Status, 2=Login) |
| 0xFE | C→S | Legacy Server List Ping | `payload` (u8, always 0x01) — pre-1.7 compat |
No S→C packets in this state.
### 3.2 Status state
| ID | Direction | Name | Key fields |
|---|---|---|---|
| 0x00 | C→S | Ping Start | (no fields) |
| 0x01 | C→S | Ping | `time` (i64) |
| 0x00 | S→C | Server Info | `response` (string — JSON payload) |
| 0x01 | S→C | Ping | `time` (i64 echo) |
### 3.3 Login state
| ID | Direction | Name | Key fields |
|---|---|---|---|
| 0x00 | C→S | Login Start | `username` (string) |
| 0x01 | C→S | Encryption Begin | `sharedSecret` (i16-length buffer), `verifyToken` (i16-length buffer) |
| 0x00 | S→C | Disconnect | `reason` (string — JSON chat) |
| 0x01 | S→C | Encryption Request | `serverId` (string), `publicKey` (i16-length buffer), `verifyToken` (i16-length buffer) |
| 0x02 | S→C | Login Success | `uuid` (string), `username` (string) |
Notes:
- No Login Compression packet (0x03) — that is a 1.8 addition.
- `publicKey` and `verifyToken` use **i16** (signed 16-bit) as the length
prefix, not VarInt; this is a known 1.7 quirk that 1.8 also retains
<!-- VERIFY --> exact format change in 1.8 if any.
- `uuid` in Login Success is transmitted as a **string** (hyphenated UUID text),
not as two i64 fields — that encoding came later.
### 3.4 Play state — Server→Client (0x000x40)
| ID | Name | Key fields / notes |
|---|---|---|
| 0x00 | Keep Alive | `keepAliveId` (i32) |
| 0x01 | Login (Join Game) | `entityId` (i32), `gameMode` (u8), `dimension` (i8), `difficulty` (u8), `maxPlayers` (u8), `levelType` (string) |
| 0x02 | Chat Message | `message` (string — JSON) |
| 0x03 | Update Time | `age` (i64), `time` (i64) |
| 0x04 | Entity Equipment | `entityId` (i32), `slot` (i16), `item` (Slot) |
| 0x05 | Spawn Position | `location` (position\_iii: 3×i32) |
| 0x06 | Update Health | `health` (f32), `food` (i16), `foodSaturation` (f32) |
| 0x07 | Respawn | `dimension` (i32), `difficulty` (u8), `gamemode` (u8), `levelType` (string) |
| 0x08 | Player Position And Look | `x/y/z` (f64), `yaw/pitch` (f32), `onGround` (bool) |
| 0x09 | Held Item Change | `slot` (i8) |
| 0x0A | Use Bed | `entityId` (i32), `location` (position\_ibi: i32/u8/i32) |
| 0x0B | Animation | `entityId` (VarInt), `animation` (u8) |
| 0x0C | Spawn Named Entity | `entityId` (VarInt), `playerUUID` (string), `playerName` (string), `data` (array of {name,value,signature} property triples), `x/y/z` (i32), `yaw/pitch` (i8), `currentItem` (i16), `metadata` |
| 0x0D | Collect Item | `collectedEntityId` (i32), `collectorEntityId` (i32) |
| 0x0E | Spawn Object | `entityId` (VarInt), `type` (i8), `x/y/z` (i32), `pitch/yaw` (i8), `objectData` (i32 + conditional i16×3 velocity) |
| 0x0F | Spawn Mob | `entityId` (VarInt), `type` (u8), `x/y/z` (i32), `yaw/pitch/headPitch` (i8), `velocity` (vec3i16), `metadata` |
| 0x10 | Spawn Painting | `entityId` (VarInt), `title` (string), `location` (position\_iii), `direction` (i32) |
| 0x11 | Spawn Experience Orb | `entityId` (VarInt), `x/y/z` (i32), `count` (i16) |
| 0x12 | Entity Velocity | `entityId` (i32), `velocity` (vec3i16) |
| 0x13 | Destroy Entities | `entityIds` (i8-count array of i32) |
| 0x14 | Entity | `entityId` (i32) — no-op movement |
| 0x15 | Entity Relative Move | `entityId` (i32), `dX/dY/dZ` (i8) |
| 0x16 | Entity Look | `entityId` (i32), `yaw/pitch` (i8) |
| 0x17 | Entity Look And Relative Move | `entityId` (i32), `dX/dY/dZ` (i8), `yaw/pitch` (i8) |
| 0x18 | Entity Teleport | `entityId` (i32), `x/y/z` (i32), `yaw/pitch` (i8) |
| 0x19 | Entity Head Look | `entityId` (i32), `headYaw` (i8) |
| 0x1A | Entity Status | `entityId` (i32), `entityStatus` (i8) |
| 0x1B | Attach Entity | `entityId` (i32), `vehicleId` (i32), `leash` (bool) |
| 0x1C | Entity Metadata | `entityId` (i32), `metadata` (entityMetadata) |
| 0x1D | Entity Effect | `entityId` (i32), `effectId/amplifier` (i8), `duration` (i16) |
| 0x1E | Remove Entity Effect | `entityId` (i32), `effectId` (i8) |
| 0x1F | Set Experience | `experienceBar` (f32), `level/totalExperience` (i16) |
| 0x20 | Entity Properties | `entityId` (i32), `properties` (i32-count array of {key:string, value:f64, modifiers:[{uuid,amount:f64,operation:i8}]}) |
| 0x21 | Chunk Data | `x/z` (i32), `groundUp` (bool), `bitMap/addBitMap` (u16), `compressedChunkData` (i32-length buffer) |
| 0x22 | Multi Block Change | `chunkX/chunkZ` (i32), `recordCount` (i16), `dataLength` (i32), `records` (array of {metadata:4bit, blockId:12bit, y:u8, z:4bit, x:4bit}) |
| 0x23 | Block Change | `location` (position\_ibi), `type` (VarInt), `metadata` (u8) |
| 0x24 | Block Action | `location` (position\_isi: i32/i16/i32), `byte1/byte2` (u8), `blockId` (VarInt) |
| 0x25 | Block Break Animation | `entityId` (VarInt), `location` (position\_iii), `destroyStage` (i8) |
| 0x26 | Map Chunk Bulk | `chunkColumnCount` (i16), `dataLength` (i32), `skyLightSent` (bool), `compressedChunkData`, `meta` array of {x/z:i32, bitMap/addBitMap:u16} |
| 0x27 | Explosion | `x/y/z/radius` (f32), `affectedBlockOffsets` (i32-count array of i8×3), `playerMotionX/Y/Z` (f32) |
| 0x28 | Effect (World Event) | `effectId` (i32), `location` (position\_ibi), `data` (i32), `global` (bool) |
| 0x29 | Named Sound Effect | `soundName` (string), `x/y/z` (i32), `volume` (f32), `pitch` (u8) |
| 0x2A | World Particles | `particleName` (string), `x/y/z/offsetX/Y/Z/particleData` (f32), `particles` (i32) |
| 0x2B | Game State Change | `reason` (u8), `gameMode` (f32) |
| 0x2C | Spawn Global Entity (Weather) | `entityId` (VarInt), `type` (i8), `x/y/z` (i32) |
| 0x2D | Open Window | `windowId` (u8), `inventoryType` (u8), `windowTitle` (string), `slotCount` (u8), `useProvidedTitle` (bool), `entityId` (i32, only for type 11) |
| 0x2E | Close Window | `windowId` (u8) |
| 0x2F | Set Slot | `windowId` (i8), `slot` (i16), `item` (Slot) |
| 0x30 | Window Items | `windowId` (u8), `items` (i16-count Slot array) |
| 0x31 | Window Property | `windowId` (u8), `property/value` (i16) |
| 0x32 | Confirm Transaction | `windowId` (u8), `action` (i16), `accepted` (bool) |
| 0x33 | Update Sign | `location` (position\_isi), `text14` (string×4) |
| 0x34 | Maps | `itemDamage` (VarInt), `data` (i16-length buffer) |
| 0x35 | Update Block Entity | `location` (position\_isi), `action` (u8), `nbtData` (compressedNbt) |
| 0x36 | Open Sign Editor | `location` (position\_iii) |
| 0x37 | Statistics | `entries` (VarInt-count array of {name:string, value:VarInt}) |
| 0x38 | Player List Item | `playerName` (string), `online` (bool), `ping` (i16) |
| 0x39 | Player Abilities | `flags` (i8), `flyingSpeed/walkingSpeed` (f32) |
| 0x3A | Tab-Complete | `matches` (VarInt-count string array) |
| 0x3B | Scoreboard Objective | `name` (string), `displayText` (string), `action` (i8) |
| 0x3C | Update Score | `itemName` (string), `action` (i8), `scoreName` (string, absent if action=1), `value` (i32, absent if action=1) |
| 0x3D | Display Scoreboard | `position` (i8), `name` (string) |
| 0x3E | Teams | `team` (string), `mode` (i8), conditional fields for name/prefix/suffix/friendlyFire/players depending on mode |
| 0x3F | Plugin Message | `channel` (string), `data` (i16-length buffer) |
| 0x40 | Disconnect | `reason` (string — JSON chat) |
**Total S→C Play: 65 packet types (0x000x40).**
### 3.5 Play state — Client→Server (0x000x17)
| ID | Name | Key fields / notes |
|---|---|---|
| 0x00 | Keep Alive | `keepAliveId` (i32) |
| 0x01 | Chat Message | `message` (string) |
| 0x02 | Use Entity | `target` (i32), `mouse` (i8: 0=interact, 1=attack, 2=interact\_at), `x/y/z` (f32, only if mouse=2) |
| 0x03 | Player (ground status only) | `onGround` (bool) |
| 0x04 | Player Position | `x/stance/y/z` (f64), `onGround` (bool) — note `stance` field (eye height) |
| 0x05 | Player Look | `yaw/pitch` (f32), `onGround` (bool) |
| 0x06 | Player Position And Look | `x/stance/y/z` (f64), `yaw/pitch` (f32), `onGround` (bool) |
| 0x07 | Player Digging | `status` (i8), `location` (position\_ibi: i32/u8/i32), `face` (i8) |
| 0x08 | Player Block Placement | `location` (position\_ibi), `direction` (i8), `heldItem` (Slot), `cursorX/Y/Z` (i8) |
| 0x09 | Held Item Change | `slotId` (i16) |
| 0x0A | Animation | `entityId` (i32), `animation` (i8) |
| 0x0B | Entity Action | `entityId` (i32), `actionId` (i8), `jumpBoost` (i32) |
| 0x0C | Steer Vehicle | `sideways/forward` (f32), `jump/unmount` (bool) |
| 0x0D | Close Window | `windowId` (u8) |
| 0x0E | Click Window | `windowId` (i8), `slot` (i16), `mouseButton` (i8), `action` (i16), `mode` (i8), `item` (Slot) |
| 0x0F | Confirm Transaction | `windowId` (i8), `action` (i16), `accepted` (bool) |
| 0x10 | Creative Inventory Action | `slot` (i16), `item` (Slot) |
| 0x11 | Enchant Item | `windowId/enchantment` (i8) |
| 0x12 | Update Sign | `location` (position\_isi), `text14` (string×4) |
| 0x13 | Player Abilities | `flags` (i8), `flyingSpeed/walkingSpeed` (f32) |
| 0x14 | Tab-Complete | `text` (string) |
| 0x15 | Client Settings | `locale` (string), `viewDistance` (i8), `chatFlags` (i8), `chatColors` (bool), `difficulty` (u8), `showCape` (bool) |
| 0x16 | Client Status | `payload` (i8: 0=perform\_respawn, 1=request\_stats, 2=open\_inventory) |
| 0x17 | Plugin Message | `channel` (string), `data` (i16-length buffer) |
**Total C→S Play: 24 packet types (0x000x17).**
---
## 4. Notable data-type details
### Slot (item stack)
```
blockId: i16 // -1 = empty (null slot)
if blockId != -1:
itemCount: i8
itemDamage: i16
nbtData: compressedNbt // zlib-deflate compressed NBT, or 0x0000 if absent
```
Source: `protocol.json` `types.slot`.
### Entity Metadata (1.7 wire format)
Each entry is a byte whose high 3 bits are the type and low 5 bits are the key
(`entityMetadataItem` bitfield). Types: 0=i8, 1=i16, 2=i32, 3=f32, 4=string,
5=Slot, 6=int-triple (x/y/z i32s), 7=float-triple (pitch/yaw/roll). The loop
terminates on byte value `0x7F` (127). Source: `protocol.json`
`types.entityMetadataLoop` / `entityMetadataItem`.
### String
VarInt-prefixed UTF-8 (`pstring` with `countType: varint`). No null terminator.
Source: `protocol.json` `types.string`.
### Position variants (1.7-era)
No packed 64-bit Position type (that is a 1.8 addition). 1.7 uses explicit
multi-field combinations:
- `position_iii` — x:i32, y:i32, z:i32
- `position_isi` — x:i32, y:i16, z:i32
- `position_ibi` — x:i32, y:u8, z:i32
Source: `protocol.json` `types.position_iii` / `position_isi` / `position_ibi`.
### Player Position — `stance` field
The client-to-server Player Position packets include a `stance` field (f64)
representing the player's eye height offset above their feet (typically `y + 1.62`).
The server uses this for hitbox calculations. The `stance` field was **removed in
1.8** when the server began computing it server-side.
<!-- VERIFY --> exact removal version.
### Plugin Message channel data length
In 1.7, `packet_custom_payload` (both directions) uses an **i16** length prefix
for the `data` buffer, not VarInt. Source: `protocol.json`
`types` for `packet_custom_payload.data` in both play.toClient and play.toServer.
---
## 5. Wire framing summary
```
Packet (1.7+):
[length: VarInt] ← total byte count of the rest of this packet
[id: VarInt] ← state-scoped packet ID (always 1 byte in 1.7 range: ≤ 0x7F)
[fields: ...]
String:
[byteLength: VarInt]
[utf8 bytes: ...]
No compression in 1.7. Login Compression (0x03) was added in 1.8.
No encryption wrapper at the framing level — AES/CFB8 is applied per-byte
over the already-framed stream after Login Success.
```
---
## 6. Why 1.7 is the "oldest viable baseline"
Modern proxies (Velocity, BungeeCord, mc-router) target 1.7.10 as their floor
because:
1. **State machine is present** — Handshake/Status/Login/Play states let a proxy
correctly intercept the login sequence to inject forwarding data (BungeeCord
IP-forwarding via serverAddress overload, Velocity login-plugin-message). The
pre-1.7 protocol has no Login state; there is no clean injection point.
2. **VarInt framing is self-delimiting** — a proxy can pipeline and inspect
packets without a static byte-length lookup table. The pre-1.7 format
required per-packet hardcoded lengths.
3. **JSON chat and SLP** — modern server-list-ping tools and anti-bot layers
depend on the JSON SLP format. The old 0xFE SLP returns a `§`-delimited
string that is not JSON.
4. **UUIDs in Login Success** — the Login Success packet carries a UUID string,
enabling player identity to be keyed on UUID rather than name. BungeeCord
forwarding injects UUID + skin properties into the Handshake `serverAddress`
or (for Velocity) via a Login Plugin Request; both require the Login state.
5. **Plugin Message channel** — the `custom_payload` packet (C→S 0x17, S→C
0x3F) provides a channel-namespaced side-band that Forge uses for FML
handshake (`FML|HS`) and that proxies use for capability negotiation.
The pre-1.7 legacy protocol (Minecraft ≤1.6.4, protocol ≤78) required a
completely different code path. Proxies that support it (e.g. BungeeCord legacy
mode) maintain a separate parser. The consensus since ~2015 is that 1.7.10 is
the floor: old enough to be "legacy" but new enough to share the modern framing
and state machine.
---
## 7. Proxy / translation impact
### ViaVersion floor
[ViaVersion](https://github.com/ViaVersion/ViaVersion) handles protocol
translation for **1.8 and above only**. Its lowest supported input protocol is
1.8 (protocol 47). There is no `protocols/v4to5/` or `protocols/v5to47/` package
in ViaVersion; the 1.7 → 1.8 gap is explicitly out of scope for the ViaVersion
project.
### Bridging below 1.8: ViaLegacy and ViaRewind
To support 1.7.x clients on modern servers, the translation stack requires:
- **ViaLegacy** — handles protocol ≤ 1.7.10 (protocol ≤ 5) → modern. It
re-implements the full 1.7 state machine, framing parser, and all packet
transforms including the `stance`-removal, position packing, and the 1.7
entity-metadata format.
- **ViaRewind** — handles 1.7 and 1.8 clients against 1.9+ servers. Sits
on top of ViaVersion.
Neither ViaLegacy nor ViaRewind is cloned in this repo's ref set; they are
referenced by name only. See their respective GitHub repos for implementation
detail.
### What a 1.7-aware proxy must handle
| Concern | 1.7 detail | Modern delta |
|---|---|---|
| Framing | VarInt length + VarInt ID | Same in 1.8+ |
| Compression | None | Login Compression added in 1.8 (0x03 Login packet) |
| Encryption | RSA-1024 key exchange, AES/CFB8 shared secret, verifyToken — same mechanism as 1.8+ | i16 length prefix on `publicKey`/`verifyToken` buffers (vs VarInt in later versions <!-- VERIFY -->) |
| Entity IDs in Play | Mix of i32 and VarInt (e.g. `entity_destroy` uses i32 array, `animation` uses VarInt) | Standardised to VarInt in 1.8 |
| Position encoding | Three separate i32/i16/u8 fields | Packed 64-bit Position type added in 1.8 |
| Slot NBT | `compressedNbt` (zlib-compressed) | Uncompressed NBT after 1.8 |
| Player stance | Client sends `stance` (f64) in position packets | Removed in 1.8 |
| Login Success UUID | String (hyphenated text) | Two i64 fields in 1.16+ <!-- VERIFY exact version --> |
| Player List Item | Simple {name, online, ping} per-packet | Replaced with action-tagged packet in 1.8 |
| Chunk format | zlib-compressed section bitmask + addBitMap; separate MapChunkBulk for multi-chunk | Restructured in 1.9 |
### BungeeCord / Velocity forwarding with 1.7 clients
BungeeCord legacy forwarding (IP injection into Handshake `serverAddress`) works
unchanged with 1.7 clients because the `packet_set_protocol` (Handshake 0x00)
field is a plain `string` that BungeeCord appends to with `\0` delimiters.
Velocity modern forwarding uses a **Login Plugin Request / Response** exchange in
the Login state (added in 1.13). 1.7 clients do not support Login Plugin
messages; a Velocity-in-modern-mode proxy cannot forward 1.7 clients without a
ViaLegacy bridge that intercepts the Login Plugin exchange on behalf of the 1.7
client. <!-- VERIFY --> exact Velocity behaviour when a 1.7 client hits a
modern-forwarding backend.
---
## 8. Sources summary
| Source | Used for |
|---|---|
| minecraft.wiki `/w/Java_Edition_1.7.2` (fetched 2026-06-19) | Release date, Netty rewrite, JSON chat, SLP, packet length header |
| minecraft.wiki `/w/Java_Edition_1.7.6` (fetched 2026-06-19) | Protocol 5 confirmation, release date 2014-04-09, skin overhaul, incompatibility note |
| minecraft.wiki `/w/Java_Edition_1.7.10` (fetched 2026-06-19) | Protocol 5 confirmation, release date 2014-06-26, 1.7.69 compatibility note, Log4j note |
| `/tmp/mcproto-refs/minecraft-data/data/pc/1.7/version.json` | Canonical: `{"version":5,"minecraftVersion":"1.7.10","majorVersion":"1.7"}` |
| `/tmp/mcproto-refs/minecraft-data/data/pc/1.7/protocol.json` | Full packet inventory by state: all IDs, field names, data types |
+405
View File
@@ -0,0 +1,405 @@
# 1.8.x — The Bountiful Update
**Protocol:** 47
**Versions:** 1.8 (2014-09-02) through 1.8.9 (2015-12-09) — all share the same protocol number
**Minecraft wiki release article:** <https://minecraft.wiki/w/Java_Edition_1.8> (fetched 2026-06-19)
**minecraft-data sources:** `/tmp/mcproto-refs/minecraft-data/data/pc/1.8/protocol.json`, `version.json`; diff against `/tmp/mcproto-refs/minecraft-data/data/pc/1.7/protocol.json`
**ViaVersion source:** `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_8to1_9/packet/``ClientboundPackets1_8.java`, `ServerboundPackets1_8.java`
**Protocol version confirmed:** `version.json` (`/tmp/mcproto-refs/minecraft-data/data/pc/1.8/version.json`) = `{"version":47,"minecraftVersion":"1.8.8","majorVersion":"1.8"}`; 1.7.10 = `{"version":5}`.
---
## Headline changes
1.8 ("The Bountiful Update") shipped 2014-09-02. On the wire it made three structural additions that defined the protocol for years:
**Packet compression.** A new `Set Compression` packet was inserted into the login flow (login-state, clientbound 0x03). From that point forward, each packet carries a second VarInt (`Data Length`) between the existing `Packet Length` VarInt and the payload, enabling optional zlib deflate per-packet. This is the biggest framing change since the Netty rewrite in 1.7. See [§ Compression detail](#compression-in-detail) below.
**Palette-based block and chunk encoding.** The `Map Chunk` (`0x21`) and `Multi Block Change` (`0x22`) packets switched from the 1.7 format (separate 8-bit block-ID + 4-bit metadata nibble-plane) to a single unified block-state integer. The `Multi Block Change` record went from a packed `{12-bit blockId, 4-bit metadata}` bitfield (separate words) to a single VarInt block-state ID per record. The `block_change` packet's payload similarly changed from `{type:varint, metadata:u8}` (two fields) to a single `{type:varint}` block-state ID.
**New packed-integer Position type.** Block coordinates moved from three separate `i32`/`i16`/`i32` fields in a `position_iii`/`position_isi`/`position_ibi` struct to a single `i64` packed as `{x:26-bit signed, y:12-bit signed, z:26-bit signed}`. This is the `position` bitfield type that appears throughout the 1.8 protocol.
**Why protocol 47 stayed so long.** 1.8 was the last major version for ~17 months (1.9 shipped 2016-02-29). Server operators preferred it for PvP because 1.9 introduced a weapon cooldown mechanic that most competitive networks rejected. Combined with Mojang's slow adoption of forced upgrades, protocol 47 remained the dominant live-traffic version until at least 2017 — the era that spawned the entire ViaVersion ecosystem.
---
## Compression in detail
> Cross-reference: [../05-login-encryption.md](../05-login-encryption.md) covers the full login flow; this section focuses on the 1.8 addition.
### The Set Compression packet
Sent by the server **during the login state**, after `Login Success` is in the pipeline but **before** it is actually sent:
| State | Direction | ID | Name | Field | Type |
|---|---|---|---|---|---|
| login | clientbound | `0x03` | Set Compression | threshold | VarInt |
Source: `minecraft-data/data/pc/1.8/protocol.json` login.toClient mappings `"0x03": "compress"` and `packet_compress` definition `[{"name":"threshold","type":"varint"}]`.
The 1.7 login state had only three clientbound packets (`0x00` disconnect, `0x01` encryption_begin, `0x02` success). 1.8 added `0x03` compress.
A negative `threshold` value disables compression. A value ≥ 0 means: packets whose uncompressed payload is ≥ `threshold` bytes **must** be zlib-compressed; smaller packets **may** be sent uncompressed (Data Length = 0).
### Framing change: the Data Length VarInt
Before compression is enabled the packet format is identical to 1.7:
```
[Packet Length : VarInt] [Packet ID : VarInt] [Payload...]
```
After `Set Compression` is received, **all** subsequent packets (in both directions) add a second VarInt:
```
[Packet Length : VarInt] [Data Length : VarInt] [Packet ID + Payload : zlib or raw]
```
- `Packet Length` = byte-count of everything that follows, i.e. `len(Data Length varint) + len(compressed-or-raw-data)`.
- `Data Length = 0` → the following bytes are **not** compressed (uncompressed payload was below threshold).
- `Data Length > 0` → the following bytes are zlib-deflated; `Data Length` is the **uncompressed** byte count (used to size the decompression buffer).
This second VarInt is invisible in the 1.7 framing; adding it is the entire framing change. The rest of the packet (ID + payload) is structurally unchanged — compression is a wrapper layer only.
### Login flow with compression (1.8)
```mermaid
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Login Start (0x00) [username]
S->>C: Encryption Request (0x01) [serverId, pubKey, verifyToken]
C->>S: Encryption Response (0x02) [encSharedSecret, encVerifyToken]
Note over C,S: AES/CFB8 encryption begins (both directions)
S->>C: Set Compression (0x03) [threshold] — NEW in 1.8
Note over C,S: Framing switches to compressed format (both directions)
S->>C: Login Success (0x02) [uuid, username]
Note over C,S: Transition to PLAY state
```
Note: `Set Compression` is sent **encrypted** (after the AES stream is established) but **before** the state transitions to PLAY. The compression threshold is server-controlled; vanilla default is 256 bytes.
---
## Protocol changes vs 1.7 (from minecraft-data diff)
The 1.7 and 1.8 protocol.json files share the same top-level packet names and IDs for nearly all play-state packets (0x000x40 clientbound, 0x000x17 serverbound). The protocol-breaking changes are in field types and the new packets. Key diffs:
### Handshake state
Identical. Both: `0x00` set_protocol (`protocolVersion:varint, serverHost:string, serverPort:u16, nextState:varint`).
### Login state (clientbound)
| ID | 1.7 packet | 1.8 change |
|---|---|---|
| 0x00 | disconnect | unchanged |
| 0x01 | encryption_begin | **field type change**: `publicKey` and `verifyToken` lengths changed from `i16`-prefixed buffers (1.7) to `varint`-prefixed buffers (1.8) |
| 0x02 | success | unchanged (`uuid:string, username:string`) |
| 0x03 | — | **NEW**: `compress` (`threshold:varint`) |
Source: compare `login.toClient` in both `protocol.json` files. In 1.7, `packet_encryption_begin` uses `{"countType":"i16"}` for both fields; in 1.8 they use `{"countType":"varint"}`.
### Login state (serverbound)
Same change: 1.8 `encryption_begin` uses `varint`-prefixed `sharedSecret` and `verifyToken` buffers instead of `i16`-prefixed.
### Play state — new/changed clientbound packets
| ID | Name | 1.7 → 1.8 change |
|---|---|---|
| 0x01 (login) | Login | **Added** `reducedDebugInfo:bool` field in 1.8 |
| 0x02 (chat) | Chat | **Added** `position:i8` field in 1.8 (chat box=0, system=1, game info/action bar=2) |
| 0x04 (entity_equipment) | Entity Equipment | Entity ID: `i32``varint` |
| 0x05 (spawn_position) | Spawn Position | `location`: `position_iii` (3×i32) → packed `position` (single i64 bitfield) |
| 0x06 (update_health) | Update Health | `food`: `i16``varint` |
| 0x0a (bed) | Use Bed | Entity ID `i32``varint`; `location`: `position_ibi` → packed `position` |
| 0x0c (named_entity_spawn) | Named Entity Spawn | **Major**: removed inline `playerName:string` and `data[]` properties array (1.7 sent these inline); added `playerUUID:UUID` binary type; removed `currentItem` field; 1.7 had string UUID + full profile inline |
| 0x0d (collect) | Collect | Both entity IDs `i32``varint` |
| 0x0e (spawn_entity) | Spawn Entity | `objectData` refactored: was inline `{intField:i32, velocityX/Y/Z:i16-conditional}`; same semantics, slightly different schema |
| 0x12 (entity_velocity) | Entity Velocity | Entity ID `i32``varint` |
| 0x13 (entity_destroy) | Entity Destroy | Count prefix `i8`+entity `i32` array → `varint` count + `varint` array |
| 0x140x19 (entity move/look/teleport) | Entity movement | Entity ID `i32``varint`; `onGround:bool` added to rel_entity_move, entity_look, entity_move_look, entity_teleport |
| 0x1a (entity_status) | Entity Status | Entity ID unchanged (`i32`) |
| 0x1c (entity_metadata) | Entity Metadata | Entity ID `i32``varint` |
| 0x1d (entity_effect) | Entity Effect | `duration`: `i16``varint`; added `hideParticles:bool` |
| 0x1e (remove_entity_effect) | Remove Entity Effect | Entity ID `i32``varint` |
| 0x1f (experience) | Set Experience | `level` and `totalExperience`: `i16``varint` |
| 0x20 (update_attributes) | Update Attributes | Modifier count prefix: `i16``varint` |
| 0x21 (map_chunk) | Map Chunk | **Major**: removed `addBitMap:u16` and `compressedChunkData` (zlib-in-packet); replaced with raw `chunkData:ByteArray(varint-prefixed)`; blocks now use palette block-state IDs |
| 0x22 (multi_block_change) | Multi Block Change | **Major**: record format changed from separate `{metadata+blockId bitfield, y:u8, x+z bitfield}` to `{horizontalPos:u8, y:u8, blockId:varint}`; count now `varint`, removed `dataLength:i32` |
| 0x23 (block_change) | Block Change | `location`: `position_ibi` → packed `position`; removed `metadata:u8` field — block-state ID in `type:varint` encodes both |
| 0x24 (block_action) | Block Action | `location`: `position_isi` → packed `position` |
| 0x25 (block_break_animation) | Block Break Animation | `location`: `position_iii` → packed `position` |
| 0x26 (map_chunk_bulk) | Map Chunk Bulk | **Major**: removed per-chunk `addBitMap:u16`; removed `compressedChunkData` bulk buffer; switched to `skyLightSent:bool` + varint-prefixed meta array + raw data buffer |
| 0x28 (world_event) | World Event | `location`: `position_ibi` → packed `position` |
| 0x2a (world_particles) | World Particles | Switched from `particleName:string` to `particleId:i32`; added `longDistance:bool`; added extra `data[]` varint array for item/block particles |
| 0x2c (spawn_entity_weather) | Spawn Weather Entity | Entity ID unchanged (`varint`) |
| 0x2d (open_window) | Open Window | `inventoryType`: `u8` numeric → `string` identifier; removed `useProvidedTitle:bool` |
| 0x33 (update_sign) | Update Sign | `location`: `position_isi` → packed `position` |
| 0x34 (map) | Map Item Data | **Major reformat**: 1.7 had just `{itemDamage:varint, data:buffer(i16)}`; 1.8 expanded to `{itemDamage:varint, scale:i8, icons:[...], columns:i8, rows:i8, x:i8, y:i8, data:ByteArray}` |
| 0x35 (tile_entity_data) | Tile Entity Data | `location`: `position_isi` → packed `position`; `nbtData`: `compressedNbt` → uncompressed `optionalNbt` |
| 0x36 (open_sign_entity) | Open Sign Editor | `location`: `position_iii` → packed `position` |
| 0x38 (player_info) | Player Info | **Major**: 1.7 was a flat `{playerName:string, online:bool, ping:i16}`; 1.8 became an action-based structure with `{action:varint, data:[{uuid:UUID, ...}]}` supporting add/remove/update-gamemode/update-latency/update-displayname actions |
| 0x41 (difficulty) | Change Difficulty | **NEW** in 1.8 (absent from 1.7) |
| 0x42 (combat_event) | Player Combat | **NEW** in 1.8 |
| 0x43 (camera) | Set Camera | **NEW** in 1.8 |
| 0x44 (world_border) | World Border | **NEW** in 1.8 |
| 0x45 (title) | Title | **NEW** in 1.8 |
| 0x46 (set_compression) | Set Compression | **NEW** in 1.8 (play-state duplicate; also exists in login state as 0x03) |
| 0x47 (playerlist_header) | Tab List Header/Footer | **NEW** in 1.8 |
| 0x48 (resource_pack_send) | Resource Pack | **NEW** in 1.8 |
| 0x49 (update_entity_nbt) | Update Entity NBT | **NEW** in 1.8 |
Source: packet maps in `minecraft-data/data/pc/1.8/protocol.json` play.toClient vs `data/pc/1.7/protocol.json` play.toClient. 1.7 clientbound play topped at `0x40` (kick_disconnect); 1.8 extends to `0x49`.
### Play state — changed serverbound packets
| ID | Name | 1.7 → 1.8 change |
|---|---|---|
| 0x02 (use_entity) | Interact | `target:i32``varint`; `mouse:i8``varint`; added optional `{x,y,z}:f32` fields when `mouse=2` (interact-at) |
| 0x04 (position) | Player Position | **Removed** `stance:f64` field (1.7 had x,stance,y,z; 1.8 has x,y,z) |
| 0x06 (position_look) | Player Pos+Look | Same: removed `stance:f64` |
| 0x07 (block_dig) | Player Action | `status:i8``varint`; `location`: `position_ibi` → packed `position` |
| 0x08 (block_place) | Use Item On | `location`: `position_ibi` → packed `position` |
| 0x0a (arm_animation) | Swing Arm | 1.7 had `{entityId:i32, animation:i8}`; 1.8 is empty (no fields) |
| 0x0b (entity_action) | Player Command | `entityId:i32``varint`; `actionId:i8``varint`; `jumpBoost:i32``varint` |
| 0x0c (steer_vehicle) | Player Input | `jump:bool+unmount:bool` → single `jump:u8` flags byte |
| 0x12 (update_sign) | Sign Update | `location`: `position_isi` → packed `position` |
| 0x14 (tab_complete) | Command Suggestion | Added optional `block:option<position>` field |
| 0x15 (settings) | Client Information | Removed `difficulty:u8` and `showCape:bool`; added `skinParts:u8` bitmask |
| 0x18 (spectate) | Teleport To Entity | **NEW** in 1.8 |
| 0x19 (resource_pack_receive) | Resource Pack Status | **NEW** in 1.8 |
Source: `minecraft-data/data/pc/1.8/protocol.json` play.toServer vs `data/pc/1.7/protocol.json` play.toServer. 1.7 serverbound play topped at `0x17` (custom_payload); 1.8 extends to `0x19`.
---
## Entity metadata format
Both 1.7 and 1.8 use the same sentinel-terminated metadata loop structure:
- Each entry is a header byte with `{type:3 bits, key:5 bits}`.
- End of stream signalled by byte `0x7F` (127).
- Entry values by type: 0=i8, 1=i16, 2=i32, 3=f32, 4=string, 5=slot, 6={x:i32,y:i32,z:i32}, 7={pitch:f32,yaw:f32,roll:f32}.
The type table is identical in both versions (verified from `entityMetadataItem` in both `protocol.json` files). However, the set of actual metadata indices and their meanings changed for several entity types in 1.8 to accommodate new entity properties (guardians, rabbits, armor stands). <!-- VERIFY: specific index changes for new 1.8 entities -->
The container types `entityMetadata` and `entityMetadataItem` in 1.8's `protocol.json` are structurally identical to 1.7 — confirming that the metadata **encoding format** did not change between protocol 5 and 47; the changes were in which indices are used, not the framing.
---
## Chunk and block encoding
### 1.7 chunk format (for comparison)
In 1.7, `Map Chunk` (`0x21`) contained:
- `bitMap:u16` — which of the 16 vertical sections are present
- `addBitMap:u16` — additional-data sections for blocks requiring >8 bits of block ID (extended blocks)
- `compressedChunkData:buffer(i32-count)` — zlib-deflated payload containing raw nibble arrays: 8-bit block IDs + 4-bit metadata nibbles + 4-bit block light + optional 4-bit sky light + optional 4-bit add-data
The 1.7 format encoded block type as `(blockID << 4) | metadata` using two separate nibble planes.
### 1.8 chunk format
In 1.8, `Map Chunk` (`0x21`) contains:
- `bitMap:u16` — which sections are present
- `chunkData:ByteArray` (varint-prefixed) — raw uncompressed per-section data
The per-section data layout in 1.8 encodes blocks as **block-state IDs** — a single integer combining what was previously `blockId` and `metadata`. Each section now stores:
- 4096 block-state values as 16-bit entries (2 bytes per block, stored little-endian in a flat array of `4096 × 2` bytes)
- 2048 bytes block light nibble array
- 2048 bytes sky light nibble array (overworld only)
The `addBitMap` field and the separate metadata nibble plane are gone entirely. Block state IDs are the canonical 1.8+ block representation: e.g., oak log facing north is a single integer distinct from oak log facing east, rather than `id=17, meta=4` vs `id=17, meta=0`.
The `Multi Block Change` record format reflects this too: in 1.7, each record packed `{metadata:4bits, blockId:12bits}` in one i16 plus a separate `y:u8` and `{x:4bits,z:4bits}` nibble. In 1.8, each record is `{horizontalPos:u8, y:u8, blockId:varint}` where `blockId` is the full block-state ID. <!-- VERIFY: exact 1.8 per-section binary layout (block-state 16-bit vs varint) from wiki source -->
---
## Packet inventory by state — 1.8 (protocol 47)
Source: `minecraft-data/data/pc/1.8/protocol.json` packet mapper sections. ViaVersion `ClientboundPackets1_8.java` and `ServerboundPackets1_8.java` confirm the same IDs.
### Handshake (C→S)
| ID | Name |
|---|---|
| 0x00 | Set Protocol (Handshake) |
| 0xFE | Legacy Server List Ping |
### Status
| ID | Direction | Name |
|---|---|---|
| 0x00 | S→C | Server Info (JSON response) |
| 0x01 | S→C | Ping (i64 payload echo) |
| 0x00 | C→S | Request (empty) |
| 0x01 | C→S | Ping (i64 timestamp) |
### Login
| ID | Direction | Name |
|---|---|---|
| 0x00 | S→C | Disconnect |
| 0x01 | S→C | Encryption Request |
| 0x02 | S→C | Login Success |
| 0x03 | S→C | **Set Compression** ← new in 1.8 |
| 0x00 | C→S | Login Start |
| 0x01 | C→S | Encryption Response |
### Play — Clientbound (S→C)
| ID | Name |
|---|---|
| 0x00 | Keep Alive |
| 0x01 | Login (Join Game) |
| 0x02 | Chat Message |
| 0x03 | Time Update |
| 0x04 | Entity Equipment |
| 0x05 | Spawn Position |
| 0x06 | Update Health |
| 0x07 | Respawn |
| 0x08 | Player Position and Look |
| 0x09 | Held Item Change |
| 0x0A | Use Bed |
| 0x0B | Animation |
| 0x0C | Spawn Named Entity |
| 0x0D | Collect Item |
| 0x0E | Spawn Object |
| 0x0F | Spawn Mob |
| 0x10 | Spawn Painting |
| 0x11 | Spawn Experience Orb |
| 0x12 | Entity Velocity |
| 0x13 | Destroy Entities |
| 0x14 | Entity (no-op move) |
| 0x15 | Entity Relative Move |
| 0x16 | Entity Look |
| 0x17 | Entity Look and Relative Move |
| 0x18 | Entity Teleport |
| 0x19 | Entity Head Look |
| 0x1A | Entity Status |
| 0x1B | Attach Entity |
| 0x1C | Entity Metadata |
| 0x1D | Entity Effect |
| 0x1E | Remove Entity Effect |
| 0x1F | Set Experience |
| 0x20 | Entity Properties |
| 0x21 | Chunk Data |
| 0x22 | Multi Block Change |
| 0x23 | Block Change |
| 0x24 | Block Action |
| 0x25 | Block Break Animation |
| 0x26 | Map Chunk Bulk |
| 0x27 | Explosion |
| 0x28 | Effect |
| 0x29 | Sound Effect |
| 0x2A | Particle |
| 0x2B | Change Game State |
| 0x2C | Spawn Global Entity |
| 0x2D | Open Window |
| 0x2E | Close Window |
| 0x2F | Set Slot |
| 0x30 | Window Items |
| 0x31 | Window Property |
| 0x32 | Confirm Transaction |
| 0x33 | Update Sign |
| 0x34 | Maps |
| 0x35 | Update Block Entity |
| 0x36 | Open Sign Editor |
| 0x37 | Statistics |
| 0x38 | Player List Item |
| 0x39 | Player Abilities |
| 0x3A | Tab-Complete |
| 0x3B | Scoreboard Objective |
| 0x3C | Update Score |
| 0x3D | Display Scoreboard |
| 0x3E | Teams |
| 0x3F | Plugin Message |
| 0x40 | Disconnect |
| 0x41 | Server Difficulty ← new in 1.8 |
| 0x42 | Combat Event ← new in 1.8 |
| 0x43 | Camera ← new in 1.8 |
| 0x44 | World Border ← new in 1.8 |
| 0x45 | Title ← new in 1.8 |
| 0x46 | Set Compression (play-state) ← new in 1.8 |
| 0x47 | Player List Header and Footer ← new in 1.8 |
| 0x48 | Resource Pack Send ← new in 1.8 |
| 0x49 | Update Entity NBT ← new in 1.8 |
### Play — Serverbound (C→S)
| ID | Name |
|---|---|
| 0x00 | Keep Alive |
| 0x01 | Chat Message |
| 0x02 | Use Entity |
| 0x03 | Player (ground status only) |
| 0x04 | Player Position |
| 0x05 | Player Look |
| 0x06 | Player Position and Look |
| 0x07 | Player Digging |
| 0x08 | Player Block Placement |
| 0x09 | Held Item Change |
| 0x0A | Animation (arm swing — empty payload in 1.8) |
| 0x0B | Entity Action |
| 0x0C | Steer Vehicle |
| 0x0D | Close Window |
| 0x0E | Click Window |
| 0x0F | Confirm Transaction |
| 0x10 | Creative Inventory Action |
| 0x11 | Enchant Item |
| 0x12 | Update Sign |
| 0x13 | Player Abilities |
| 0x14 | Tab-Complete |
| 0x15 | Client Settings |
| 0x16 | Client Status |
| 0x17 | Plugin Message |
| 0x18 | Spectate ← new in 1.8 |
| 0x19 | Resource Pack Status ← new in 1.8 |
---
## Proxy and translation impact
**ViaVersion floor.** Protocol 47 is ViaVersion's oldest natively supported client. There is no `v1_7to1_8` package in ViaVersion — 1.7 support requires ViaLegacy/ViaRewind. The `v1_8to1_9` package (`/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_8to1_9/`) is the FROM-1.8 direction and serves as the canonical definition of the 1.8 packet set.
**Compression negotiation at the proxy.** A proxy operating in front of a 1.8+ server must intercept `Set Compression` (login 0x03) from the backend and decide whether to relay, modify, or suppress it. The compression threshold may differ between the proxy's own connection to the client and the proxy-to-server leg. BungeeCord and Velocity both intercept this packet and re-issue their own `Set Compression` to the client with their own threshold, then maintain separate compression state for each half of the connection.
**Position type translation.** Any proxy accepting 1.7 clients and forwarding to a 1.8+ server must translate between the old triple-i32/i16 position types and the new packed `i64` `position` type for every block-coordinate field (spawn_position, bed, block_change, block_action, block_break_animation, update_sign, tile_entity_data, open_sign_entity, world_event, and on the serverbound side: block_dig, block_place, update_sign). <!-- VERIFY: completeness of this list against actual ViaLegacy implementation -->
**block_change metadata removal.** A 1.7-to-1.8 translator must combine the 1.7 `{type:varint, metadata:u8}` pair into a single 1.8 block-state VarInt on `block_change`, and split in the reverse direction.
**Stance removal.** The 1.7 serverbound `position` packet had a `stance:f64` field (eye-height offset for collision) that was removed in 1.8. A proxy bridging 1.7→1.8 must strip this field; a 1.8→1.7 bridge must synthesise it (typically `y + 1.62`). <!-- VERIFY: exact stance calculation -->
**Player Info restructure.** The flat single-player `player_info` from 1.7 (`playerName, online, ping`) was replaced in 1.8 with the action-based multi-player structure. Proxies doing tab-list passthrough must understand the 1.8 structure.
**New packets requiring proxy awareness.** `World Border` (0x44), `Title` (0x45), `Combat Event` (0x42), and `Set Compression` (0x46 in play-state) are all 1.8-new and unknown to 1.7 clients. A proxy connecting a 1.7 client to a 1.8 server must either translate or drop these; ViaLegacy/ViaRewind handle this for the compatibility layer.
---
## Sub-version notes (1.8.1 1.8.9)
All 1.8.x patches share **protocol 47**. Patch releases addressed server-side exploits (combat, enchantment mechanics, item duplication), server crash vectors, and connectivity bugs but made **no wire-format changes**. A 1.8 client connects to a 1.8.9 server without negotiation issues, and vice versa.
<!-- VERIFY: confirm no sub-version made any packet format change (all share protocol 47 per minecraft-data version.json which keys on majorVersion "1.8") -->
---
## Sources summary
| Claim | Source |
|---|---|
| Protocol 47; version 1.8.8 as representative | `/tmp/mcproto-refs/minecraft-data/data/pc/1.8/version.json` |
| Release date 2014-09-02 | <https://minecraft.wiki/w/Java_Edition_1.8> (fetched 2026-06-19) |
| Login 0x03 `compress` packet field def | `/tmp/mcproto-refs/minecraft-data/data/pc/1.8/protocol.json` login.toClient |
| 1.7 login had no 0x03 | `/tmp/mcproto-refs/minecraft-data/data/pc/1.7/protocol.json` login.toClient (0x000x02 only) |
| Encryption buffer countType i16→varint | Both `protocol.json` files, `packet_encryption_begin` |
| Play packet IDs 0x000x49 (1.8) | `/tmp/mcproto-refs/minecraft-data/data/pc/1.8/protocol.json` play.toClient mapper |
| Play packet IDs 0x000x40 (1.7) | `/tmp/mcproto-refs/minecraft-data/data/pc/1.7/protocol.json` play.toClient mapper |
| 1.8 clientbound packet enum with IDs | `/tmp/mcproto-refs/ViaVersion/…/v1_8to1_9/packet/ClientboundPackets1_8.java` |
| 1.8 serverbound packet enum with IDs | `/tmp/mcproto-refs/ViaVersion/…/v1_8to1_9/packet/ServerboundPackets1_8.java` |
| Compression framing (Data Length VarInt) | <https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol> (fetched 2026-06-19) |
| chunk_data addBitMap present in 1.7, absent in 1.8 | diff of `packet_map_chunk` in both `protocol.json` |
| multi_block_change record format change | diff of `packet_multi_block_change` in both `protocol.json` |
| `stance` field in 1.7 position, absent in 1.8 | `packet_position` in both `protocol.json` |
| metadata loop structure identical 1.7 and 1.8 | `entityMetadata`/`entityMetadataItem` in both `protocol.json` |
+409
View File
@@ -0,0 +1,409 @@
# Java Edition 1.9.x — Protocol Deep-Dive
| Release | Protocol # | Release date | minecraft-data dir |
|---|---|---|---|
| 1.9 | **107** | 2016-02-29 | `data/pc/1.9` |
| 1.9.1 | **108** | 2016-03-30 | `data/pc/1.9.1-pre2` |
| 1.9.2 | **109** | 2016-03-30 | `data/pc/1.9.2` |
| 1.9.3 | **110** | 2016-05-10 | *(no separate mc-data dir; same packets as 1.9.4)* |
| 1.9.4 | **110** | 2016-05-10 | `data/pc/1.9.4` |
Sources: minecraft.wiki release articles ([1.9](https://minecraft.wiki/w/Java_Edition_1.9) fetched 2026-06-19, [1.9.1](https://minecraft.wiki/w/Java_Edition_1.9.1), [1.9.2](https://minecraft.wiki/w/Java_Edition_1.9.2), [1.9.3](https://minecraft.wiki/w/Java_Edition_1.9.3), [1.9.4](https://minecraft.wiki/w/Java_Edition_1.9.4)); ViaVersion source at `/tmp/mcproto-refs/ViaVersion/`; minecraft-data at `/tmp/mcproto-refs/minecraft-data/data/pc/`.
---
## Headline — the Combat Update
1.9 is the **Combat Update**, shipping 2016-02-29 after a development stretch that touched nearly every Play-state subsystem. The protocol impact was the largest single-version Play-packet reorganisation since the Netty rewrite in 1.7:
- 77 Play clientbound packets in 1.9 vs 74 in 1.8 (net +3; the real story is that almost all IDs shifted).
- 30 Play serverbound packets in 1.9 vs 26 in 1.8 (net +4).
- The entire CB packet list was renumbered wholesale — no packet kept its 1.8 ID except by coincidence.
- Four major protocol subsystems arrived at once: **dual-hand** (off-hand slot), **Boss Bar** promoted to dedicated packets, **teleport-confirm** handshake, and **vehicle movement** packets.
The patch line (108 / 109 / 110) is comparatively quiet: a Login dimension-type fix (108), a connectivity hotfix (109), and a chunk-format extension bundling block-entity NBT into the chunk packet (110 / 1.9.31.9.4).
---
## Protocol changes vs 1.8 — Play state
All IDs below are **Play** state only. Handshake / Status / Login state packets were unchanged structurally between 1.8 and 1.9 (the Login state `SET_COMPRESSION` 0x03 packet was moved exclusively into the Play path — see below).
Sources for this section:
- ViaVersion packet enums: `common/src/main/java/com/viaversion/viaversion/protocols/v1_8to1_9/packet/ClientboundPackets1_8.java` and `ClientboundPackets1_9.java`, `ServerboundPackets1_8.java`, `ServerboundPackets1_9.java`.
- minecraft-data `data/pc/1.8/protocol.json` and `data/pc/1.9/protocol.json` (cross-check).
### Clientbound Play — 1.8 vs 1.9
The table below lists every 1.8 packet by name, its 1.8 ID, and what happened to it in 1.9.
| 1.8 name | 1.8 ID | 1.9 name | 1.9 ID | Notes |
|---|---|---|---|---|
| `keep_alive` | 0x00 | `keep_alive` | 0x1F | renumbered |
| `login` | 0x01 | `login` | 0x23 | renumbered |
| `chat` | 0x02 | `chat` | 0x0F | renumbered |
| `update_time` | 0x03 | `update_time` | 0x44 | renumbered |
| `entity_equipment` | 0x04 | `entity_equipment` | 0x3C | renumbered; slot field changed: 1.8 used Short (0=hand, 14=armor); 1.9 uses VarInt with expanded enum (0=main hand, 1=off hand, 25=armor) |
| `spawn_position` | 0x05 | `spawn_position` | 0x43 | renumbered |
| `update_health` | 0x06 | `update_health` | 0x3E | renumbered |
| `respawn` | 0x07 | `respawn` | 0x33 | renumbered |
| `position` | 0x08 | `position` | 0x2E | renumbered; **+VarInt teleport ID** appended — the new teleport-confirm field (see below) |
| `held_item_slot` | 0x09 | `held_item_slot` | 0x37 | renumbered |
| `bed` | 0x0A | `bed` | 0x2F | renumbered |
| `animation` | 0x0B | `animation` | 0x06 | renumbered |
| `named_entity_spawn` | 0x0C | `named_entity_spawn` | 0x05 | renumbered; +UUID field inserted after entity ID |
| `collect` | 0x0D | `collect` | 0x49 | renumbered |
| `spawn_entity` | 0x0E | `spawn_entity` | 0x00 | renumbered; +UUID after entity ID; position coords changed from fixed-point Int×(1/32) to Double |
| `spawn_entity_living` | 0x0F | `spawn_entity_living` | 0x03 | renumbered; +UUID; coords to Double |
| `spawn_entity_painting` | 0x10 | `spawn_entity_painting` | 0x04 | renumbered; +UUID |
| `spawn_entity_experience_orb` | 0x11 | `spawn_entity_experience_orb` | 0x01 | renumbered |
| `entity_velocity` | 0x12 | `entity_velocity` | 0x3B | renumbered |
| `entity_destroy` | 0x13 | `entity_destroy` | 0x30 | renumbered |
| `entity` (no-move) | 0x14 | `entity` | 0x28 | renumbered |
| `rel_entity_move` | 0x15 | `rel_entity_move` | 0x25 | renumbered; delta values changed from Byte to Short (larger range) |
| `entity_look` | 0x16 | `entity_look` | 0x27 | renumbered |
| `entity_move_look` | 0x17 | `entity_move_look` | 0x26 | renumbered; delta values Byte→Short |
| `entity_teleport` | 0x18 | `entity_teleport` | 0x4A | renumbered; position changed from Int×(1/32) to Double; +on-ground Boolean |
| `entity_head_rotation` | 0x19 | `entity_head_rotation` | 0x34 | renumbered |
| `entity_status` | 0x1A | `entity_status` | 0x1B | renumbered |
| `attach_entity` | 0x1B | `attach_entity` | 0x3A | renumbered; leash Boolean field removed (1.9 uses `set_passengers` for riding) <!-- VERIFY: leash field drop --> |
| `entity_metadata` | 0x1C | `entity_metadata` | 0x39 | renumbered; metadata format revised (new EntityDataTypes1_9) |
| `entity_effect` | 0x1D | `entity_effect` | 0x4C | renumbered |
| `remove_entity_effect` | 0x1E | `remove_entity_effect` | 0x31 | renumbered |
| `experience` | 0x1F | `experience` | 0x3D | renumbered |
| `update_attributes` | 0x20 | `entity_update_attributes` | 0x4B | renumbered |
| `map_chunk` | 0x21 | `map_chunk` | 0x20 | renumbered; chunk wire format changed to `ChunkType1_9_1` (direct length-prefixed sections, no bulk; light separate) |
| `multi_block_change` | 0x22 | `multi_block_change` | 0x10 | renumbered |
| `block_change` | 0x23 | `block_change` | 0x0B | renumbered |
| `block_action` | 0x24 | `block_action` | 0x0A | renumbered |
| `block_break_animation` | 0x25 | `block_break_animation` | 0x08 | renumbered |
| `map_chunk_bulk` | 0x26 | *(removed)* | — | eliminated; servers send individual `map_chunk` per column instead |
| `explosion` | 0x27 | `explosion` | 0x1C | renumbered |
| `world_event` | 0x28 | `world_event` | 0x21 | renumbered; effect ID values remapped |
| `named_sound_effect` | 0x29 | `named_sound_effect` | 0x19 | renumbered; +sound category VarInt inserted after name; position encoding changed to Int×8 |
| `world_particles` | 0x2A | `world_particles` | 0x22 | renumbered |
| `game_state_change` | 0x2B | `game_state_change` | 0x1E | renumbered |
| `spawn_entity_weather` | 0x2C | `spawn_entity_weather` | 0x02 | renumbered (lightning bolt) |
| `open_window` | 0x2D | `open_window` | 0x13 | renumbered |
| `close_window` | 0x2E | `close_window` | 0x12 | renumbered |
| `set_slot` | 0x2F | `set_slot` | 0x16 | renumbered |
| `window_items` | 0x30 | `window_items` | 0x14 | renumbered |
| `craft_progress_bar` | 0x31 | `craft_progress_bar` | 0x15 | renumbered |
| `transaction` | 0x32 | `transaction` | 0x11 | renumbered |
| `update_sign` | 0x33 | `update_sign` | 0x46 | renumbered (removed in 1.9.4 — replaced by block entity data; see §110) |
| `map` | 0x34 | `map` | 0x24 | renumbered |
| `tile_entity_data` | 0x35 | `tile_entity_data` | 0x09 | renumbered |
| `open_sign_entity` | 0x36 | `open_sign_entity` | 0x2A | renumbered |
| `statistics` | 0x37 | `statistics` | 0x07 | renumbered |
| `player_info` | 0x38 | `player_info` | 0x2D | renumbered; display name field type changed String→optional JSON component |
| `abilities` | 0x39 | `abilities` | 0x2B | renumbered |
| `tab_complete` | 0x3A | `tab_complete` | 0x0E | renumbered |
| `scoreboard_objective` | 0x3B | `scoreboard_objective` | 0x3F | renumbered |
| `scoreboard_score` | 0x3C | `scoreboard_score` | 0x42 | renumbered |
| `scoreboard_display_objective` | 0x3D | `scoreboard_display_objective` | 0x38 | renumbered |
| `scoreboard_team` | 0x3E | `teams` | 0x41 | renumbered; +collision rule field added to mode 0/2 |
| `custom_payload` | 0x3F | `custom_payload` | 0x18 | renumbered |
| `kick_disconnect` | 0x40 | `kick_disconnect` | 0x1A | renumbered; reason field changed String→JSON component |
| `difficulty` | 0x41 | `difficulty` | 0x0D | renumbered |
| `combat_event` | 0x42 | `combat_event` | 0x2C | renumbered |
| `camera` | 0x43 | `camera` | 0x36 | renumbered |
| `world_border` | 0x44 | `world_border` | 0x35 | renumbered |
| `title` | 0x45 | `title` | 0x45 | same ID |
| `set_compression` | 0x46 | *(removed from Play)* | — | absorbed: 1.9 handles compression entirely in Login state; 1.8 servers sent this during Play, ViaVersion intercepts and discards it |
| `playerlist_header` | 0x47 | `playerlist_header` | 0x48 | renumbered |
| `resource_pack_send` | 0x48 | `resource_pack_send` | 0x32 | renumbered |
| `update_entity_nbt` | 0x49 | *(removed)* | — | dropped; ViaVersion cancels it |
**New in 1.9 (no 1.8 equivalent):**
| 1.9 name | 1.9 ID | Description |
|---|---|---|
| `boss_bar` | 0x0C | First-class boss bar packet with UUID + action enum (add/remove/update health/update title/update style/update flags). In 1.8 boss bar was emulated via entity data on WitherBoss/EnderDragon entities. ViaVersion provides a `BossBarProvider` to translate old entity-data-based bars to the new packet for downstream 1.9 clients. |
| `set_cooldown` | 0x17 | Item use cooldown — VarInt item ID + VarInt cooldown ticks. New with the attack-speed system. |
| `sound_effect` | 0x47 | Numeric sound ID (VarInt) + category + fixed-point position + volume + pitch. Complements `named_sound_effect` (0x19) which keeps the string-name form. |
| `unload_chunk` | 0x1D | Explicit chunk unload (Int chunkX, Int chunkZ). In 1.8 unload was signalled by a `map_chunk` with bitmask=0; ViaVersion converts the old form to this packet. |
| `vehicle_move` | 0x29 | Server→client vehicle position sync (Double x/y/z + Float yaw/pitch). |
| `set_passengers` | 0x40 | Replaces the old riding field in `attach_entity`; carries a VarInt[] of passenger entity IDs. |
Sources: ViaVersion `ClientboundPackets1_8.java` and `ClientboundPackets1_9.java` (ordinal = packet ID); `WorldPacketRewriter1_9.java:197` (MAP_BULK_CHUNK cancel), `WorldPacketRewriter1_9.java:140` (unload-chunk emit), `PlayerPacketRewriter1_9.java:332` (SET_COMPRESSION cancel), `EntityPacketRewriter1_9.java:258` (UPDATE_ENTITY_NBT cancel), `EntityPacketRewriter1_9.java:83` (SET_PASSENGERS emit).
---
### Serverbound Play — 1.8 vs 1.9
| 1.8 name | 1.8 ID | 1.9 name | 1.9 ID | Notes |
|---|---|---|---|---|
| `keep_alive` | 0x00 | `keep_alive` | 0x0B | renumbered |
| `chat` | 0x01 | `chat` | 0x02 | renumbered |
| `use_entity` | 0x02 | `use_entity` | 0x0A | renumbered; +hand VarInt appended |
| `flying` | 0x03 | `flying` | 0x0F | renumbered |
| `position` | 0x04 | `position` | 0x0C | renumbered |
| `look` | 0x05 | `look` | 0x0E | renumbered |
| `position_look` | 0x06 | `position_look` | 0x0D | renumbered |
| `block_dig` | 0x07 | `block_dig` | 0x13 | renumbered |
| `block_place` | 0x08 | `block_place` / `use_item` | 0x1C / 0x1D | **split into two packets**: `use_item_on` (0x1C, block interaction) and `use_item` (0x1D, general item use in hand). Each now carries a hand VarInt (0=main, 1=off). |
| `held_item_slot` | 0x09 | `held_item_slot` | 0x17 | renumbered |
| `arm_animation` | 0x0A | `arm_animation` | 0x1A | renumbered; +hand VarInt |
| `entity_action` | 0x0B | `entity_action` | 0x14 | renumbered |
| `steer_vehicle` | 0x0C | `steer_vehicle` | 0x15 | renumbered |
| `close_window` | 0x0D | `close_window` | 0x08 | renumbered |
| `window_click` | 0x0E | `window_click` | 0x07 | renumbered |
| `transaction` | 0x0F | `transaction` | 0x05 | renumbered |
| `set_creative_slot` | 0x10 | `set_creative_slot` | 0x18 | renumbered |
| `enchant_item` | 0x11 | `enchant_item` | 0x06 | renumbered |
| `update_sign` | 0x12 | `update_sign` | 0x19 | renumbered |
| `abilities` | 0x13 | `abilities` | 0x12 | renumbered |
| `tab_complete` | 0x14 | `tab_complete` | 0x01 | renumbered; **+Boolean** "is command block" field removed in 1.9 (ViaVersion reads and discards it from 1.9 clients before forwarding to 1.8 servers) |
| `settings` | 0x15 | `settings` | 0x04 | renumbered; +hand VarInt appended (main-hand preference) |
| `client_command` | 0x16 | `client_command` | 0x03 | renumbered |
| `custom_payload` | 0x17 | `custom_payload` | 0x09 | renumbered |
| `spectate` | 0x18 | `spectate` | 0x1B | renumbered |
| `resource_pack_receive` | 0x19 | `resource_pack_receive` | 0x16 | renumbered |
**New in 1.9 (no 1.8 equivalent):**
| 1.9 name | 1.9 ID | Description |
|---|---|---|
| `teleport_confirm` | 0x00 | Client acknowledges a `PLAYER_POSITION` (0x2E) by echoing its VarInt teleport ID. 1.8 servers do not send or expect this; ViaVersion cancels it when translating down. Source: `PlayerPacketRewriter1_9.java:382` (`protocol.cancelServerbound(ServerboundPackets1_9.ACCEPT_TELEPORTATION)`). |
| `vehicle_move` | 0x10 | Client→server vehicle position update (Double x/y/z + Float yaw/pitch). Cancelled by ViaVersion when translating down to 1.8. Source: `PlayerPacketRewriter1_9.java:383`. |
| `steer_boat` | 0x11 | Two Booleans: left paddle turning, right paddle turning. New for boats redesign. Cancelled by ViaVersion. Source: `PlayerPacketRewriter1_9.java:384` (`cancelServerbound(PADDLE_BOAT)`). |
| `use_item` | 0x1D | Use held item without targeting a block (e.g. eat food, throw ender pearl). Carries hand VarInt. ViaVersion translates to a synthetic `block_place` at position 1,1,1 face 255. Source: `WorldPacketRewriter1_9.java:314`. |
Sources: ViaVersion `ServerboundPackets1_8.java`, `ServerboundPackets1_9.java`; `PlayerPacketRewriter1_9.java:375,382384`; `WorldPacketRewriter1_9.java:314,370`.
---
## Key data-format changes
### Teleport-confirm round-trip
`PLAYER_POSITION` (CB 0x2E) gains a trailing `VarInt teleportId` field. The client must reply with `ACCEPT_TELEPORTATION` (SB 0x00) carrying the same ID before the server will accept further movement packets. This prevents the classic 1.8 rubber-band exploit where a lagging client could ignore server position corrections.
ViaVersion approach for 1.8 servers: inject a fake teleport ID of 0 into every `PLAYER_POSITION` sent downward; cancel all `ACCEPT_TELEPORTATION` packets from 1.9 clients (they mean nothing to a 1.8 server). Source: `PlayerPacketRewriter1_9.java:102116` (add `create(Types.VAR_INT, 0)` to PLAYER_POSITION), line 382 (cancel ACCEPT_TELEPORTATION).
### Dual-hand (off-hand slot)
Several packets gained a `hand` VarInt discriminator (0=main, 1=off):
- SB `use_entity` — which hand interacted.
- SB `arm_animation` — which arm swings.
- SB `use_item_on` / `use_item` — which hand placed / used item.
- SB `settings` (CLIENT_INFORMATION) — new VarInt for main-hand preference (0=left, 1=right).
CB `entity_equipment` slot encoding expanded: 1.8 Short (0=hand, 14=armor) → 1.9 VarInt (0=main hand, 1=off hand, 25=armor). ViaVersion translates the old Short to the new VarInt, remapping armor slots by +1 and handling the self-entity edge case. Source: `EntityPacketRewriter1_9.java:157205`.
### Boss Bar
CB `boss_bar` (0x0C) is a new multiplexed packet with action=VarInt:
- 0 = add (UUID + title JSON + health float + color VarInt + division VarInt + flags Byte)
- 1 = remove (UUID)
- 2 = update health
- 3 = update title
- 4 = update style
- 5 = update flags
In 1.8, boss-bar appearance was achieved by spawning WitherBoss / EnderDragon entities whose health controlled the bar. ViaVersion's `EntityTracker1_9.java` (lines 243273) reverse-engineers entity metadata updates for those entity types and synthesises the new `BOSS_EVENT` packet for 1.9+ clients connecting to 1.8 servers, using the `BossBarProvider` abstraction.
### Sound system split
1.9 introduces two parallel sound mechanisms:
- CB `named_sound_effect` (0x19) — string name (namespaced resource location) + category VarInt + fixed-point position (Int×8) + volume + pitch. Wire format changed from 1.8 (no category, string-only position). ViaVersion injects category and reformats position. Source: `WorldPacketRewriter1_9.java:96130`.
- CB `sound_effect` (0x47) — numeric sound ID (VarInt) + same remaining fields as named. New packet type absent from 1.8 entirely.
### Chunk format — ChunkType1_9_1
The 1.8 `map_chunk_bulk` (0x26) packet is gone; all chunk data is sent as individual `map_chunk` (0x20) packets. The chunk data section encoding changed to `ChunkType1_9_1`:
- Sections are length-prefixed in a single VAR_INT-prefixed byte array.
- Block light and sky light are appended per section directly after palette+data.
- Biome array (256 bytes) follows all sections for full-chunk packets.
- No embedded block-entity NBT (that arrives in a separate `tile_entity_data` 0x09 packet or via `FakeTileEntities` during translation). <!-- VERIFY: exact 1.9 wire layout vs wiki -->
Source: `ViaVersion/api/src/main/java/com/viaversion/viaversion/api/type/types/chunk/ChunkType1_9_1.java`.
### Entity coordinate precision
Entity spawn positions changed from 1.8 fixed-point integer (value/32 = world coord) to IEEE 754 Double. ViaVersion applies the `/32` transform when translating `ADD_ENTITY` / `ADD_MOB` down to 1.8. Source: `SpawnPacketRewriter1_9.java:6062` (`map(Types.INT, toNewDouble)` where `toNewDouble = inputValue / 32D`).
### Entity metadata format
`ENTITY_DATA_LIST1_8``ENTITY_DATA_LIST1_9`: the index/type packing changed, and the type tag set was revised (new types for optional UUID, VarInt enum, etc.). ViaVersion performs full metadata translation in `EntityPacketRewriter1_9.java`. The `EntityDataTypes1_9` type set is referenced in `SpawnPacketRewriter1_9.java:26`.
### Removed: `update_entity_nbt` (CB 0x49)
The 1.8 packet that sent arbitrary NBT for an entity (used by command blocks to sync state) was removed. ViaVersion cancels it: `EntityPacketRewriter1_9.java:258`. Functionality moved to the block-entity / custom-payload path.
### Removed: Play-state `set_compression`
In 1.8, the server could send `set_compression` (0x46) during the **Play** state. In 1.9 compression is negotiated exclusively in Login state (as it had always been intended). ViaVersion intercepts the 1.8 Play-state `set_compression`, sets the compression threshold on the connection, and cancels the packet so the 1.9 client never sees it. Source: `PlayerPacketRewriter1_9.java:332340`.
---
## Per-patch sub-sections
### Protocol 107 — 1.9 (2016-02-29)
The full 1.8→1.9 translation lives in:
```
common/src/main/java/com/viaversion/viaversion/protocols/v1_8to1_9/
Protocol1_8To1_9.java — entry point; registers rewriters + providers
rewriter/PlayerPacketRewriter1_9.java
rewriter/EntityPacketRewriter1_9.java
rewriter/ItemPacketRewriter1_9.java
rewriter/SpawnPacketRewriter1_9.java
rewriter/WorldPacketRewriter1_9.java
packet/ClientboundPackets1_9.java
packet/ServerboundPackets1_9.java
```
Selected ViaVersion commits for this package (from `git -C /tmp/mcproto-refs/ViaVersion log --oneline -- common/…/v1_8to1_9`):
| Commit | Subject |
|---|---|
| `0b5fa37f9` | Put title directly into component in 1.8→1.9 bossbar emulation |
| `73286fd94` | Send op permission level 4 in 1.8→1.9 |
| `08b921729` | Fix entity defaults across all protocols |
| `75c666266` | Sword blocking: consumables for 1.21.4+, back to shields for 1.20.51.21.3 |
| `e0a7b70e3` | Fix 1.8/1.9 thrown potion entity data handling |
| `e436bbe37` | Refactor dimension switch handling across all protocols |
| `501f65e21` | Packet and entity type renames (Mojang-mapped names) |
Key protocol facts established at 107:
- 77 CB / 30 SB Play packets.
- Teleport-confirm round-trip mandatory.
- `boss_bar` 0x0C, `set_cooldown` 0x17, `sound_effect` 0x47, `unload_chunk` 0x1D, `vehicle_move` 0x29, `set_passengers` 0x40 all new.
- `map_chunk_bulk`, `update_entity_nbt`, Play-state `set_compression` all removed.
### Protocol 108 — 1.9.1 (2016-03-30)
ViaVersion package: `protocols/v1_9to1_9_1/Protocol1_9To1_9_1.java`
This package is a single file with two packet handlers:
**1. `LOGIN` — dimension field type change.**
```java
// Protocol1_9To1_9_1.java:4048
map(Types.INT); // 0 - Player ID
map(Types.UNSIGNED_BYTE); // 1 - Player Gamemode
// 1.9.1 PRE 2 Changed this
map(Types.BYTE, Types.INT); // 2 - Player Dimension ← Byte in 1.9, Int in 1.9.1
```
The `LOGIN` packet's dimension field widened from `Byte` to `Int`. ViaVersion automatically converts the Int sent by a 1.9.1 server back to a Byte for any 1.9 client still connecting. Source: `Protocol1_9To1_9_1.java:4048`.
**2. `SOUND` — Elytra sound ID insertion.**
```java
// Protocol1_9To1_9_1.java:5163
if (sound >= 415) // Add 1 since there is no Elytra sound on a 1.9 server
wrapper.set(Types.VAR_INT, 0, sound + 1);
```
1.9.1 inserted the Elytra wing-flap sound into the sound registry at ID 415, shifting all higher numeric IDs up by one. Any `sound_effect` packet from a 1.9.1+ server with ID ≥ 415 must have its ID decremented by one when sent to a 1.9 client, and incremented when the reverse applies. Source: `Protocol1_9To1_9_1.java:5859`.
All other Play packets (IDs, fields, format) are identical between 107 and 108 — the packet enum used by `Protocol1_9To1_9_1` is still `ClientboundPackets1_9` / `ServerboundPackets1_9` with no new packet types. Source: `Protocol1_9To1_9_1.java:26`.
mc-wiki: 1.9.1 was superseded by 1.9.2 the same day due to a multiplayer bug; historically short-lived. ([minecraft.wiki/w/Java_Edition_1.9.1](https://minecraft.wiki/w/Java_Edition_1.9.1), fetched 2026-06-19.)
ViaVersion git log for `v1_9to1_9_1` (shallow clone — limited history): `cff9a8715` (copyright), `9f6e7fa4e` (copyright), `75d86851c` (IJ reformat + rewriter renames), `501f65e21` (packet/entity renames), `e965e9713` (package renames).
### Protocol 109 — 1.9.2 (2016-03-30)
1.9.2 was a same-day connectivity hotfix replacing 1.9.1. No new ViaVersion package exists for the 1.9.1→1.9.2 bump — `Protocol1_9To1_9_1.java` covers "1.9.1 and 1.9.2" (comment at line 34: `"// Currently supports 1.9.1 and 1.9.2"`). The packet set at protocol 109 is identical to 108: same 77 CB / 30 SB Play packets, same IDs.
minecraft-data: `data/pc/1.9.2/version.json``{"version":109,"minecraftVersion":"1.9.2","majorVersion":"1.9"}`. Packet list identical to 1.9 (`protocol.json` diff produces no changes). Source: minecraft-data `data/pc/1.9.2/`.
mc-wiki notes a connectivity issue with 1.9.1 servers was fixed; 7 bugs resolved total. No wire-protocol changes. ([minecraft.wiki/w/Java_Edition_1.9.2](https://minecraft.wiki/w/Java_Edition_1.9.2), fetched 2026-06-19.)
### Protocol 110 — 1.9.3 and 1.9.4 (2016-05-10)
ViaVersion package: `protocols/v1_9_1to1_9_3/`
```
Protocol1_9_1To1_9_3.java
packet/ClientboundPackets1_9_3.java
packet/ServerboundPackets1_9_3.java
```
1.9.3 and 1.9.4 share protocol number 110. 1.9.3 shipped and was replaced on the same day because it incorrectly showed a "you're running a snapshot" warning for old world saves. The actual protocol changes are in 1.9.3/1.9.4 together.
**Chunk format: ChunkType1_9_1 → ChunkType1_9_3**
The single largest change: the `map_chunk` (0x20) payload now **appends a `CompoundTag[]` (NBT array) of block entities** after the existing section data. This means the receiver no longer needs to request separate `tile_entity_data` packets for blocks in newly loaded chunks.
```
// ChunkType1_9_3.java:89
List<CompoundTag> nbtData = new ArrayList<>(
Arrays.asList(Types.NAMED_COMPOUND_TAG_ARRAY.read(input)));
return new BaseChunk(chunkX, chunkZ, fullChunk, false,
primaryBitmask, sections, biomeData, nbtData);
// write side: ChunkType1_9_3.java:127
Types.NAMED_COMPOUND_TAG_ARRAY.write(output,
chunk.getBlockEntities().toArray(new CompoundTag[0]));
```
ViaVersion's `Protocol1_9_1To1_9_3.java:89113` translates this: when receiving a `LEVEL_CHUNK` from a 1.9/1.9.2 server (no embedded NBT), it walks the block palette looking for blocks that are tile entities (`FakeTileEntities1_9_1.isTileEntity(id)`) and synthesises placeholder `CompoundTag` entries so the 1.9.3+ client gets the required array. Source: `Protocol1_9_1To1_9_3.java:89113`; chunk type classes `ChunkType1_9_1.java` and `ChunkType1_9_3.java`.
**`update_sign` removed from Play packet list**
In 1.9 the CB `update_sign` packet existed at 0x46. In 1.9.3/1.9.4 it is absent from the packet list (76 CB packets vs 77 in 1.9). Sign data is instead conveyed via `tile_entity_data` (0x09) with action=9. ViaVersion `Protocol1_9_1To1_9_3.java:6187` translates an `UPDATE_SIGN` from a 1.9 server into a `BLOCK_ENTITY_DATA` packet with the sign lines serialised as NBT `Text1``Text4` strings. Source: `Protocol1_9_1To1_9_3.java:6187`.
**Sound pitch scaling change**
The `SOUND` packet pitch field encoding changed:
```java
// Protocol1_9_1To1_9_3.java:4751
public static final ValueTransformer<Short, Short> ADJUST_PITCH =
new ValueTransformer<>(Types.UNSIGNED_BYTE, Types.UNSIGNED_BYTE) {
public Short transform(PacketWrapper wrapper, Short inputValue) {
return (short) Math.round(inputValue / 63.5F * 63.0F);
}
};
```
Applied to every `SOUND` packet at `Protocol1_9_1To1_9_3.java:143154`. The denominator changed from 63.5 to 63.0 (a subtle range normalisation).
**`LOGIN` and `RESPAWN` — dimension tracking**
Both packets are handled to keep the proxy's `ClientWorld` dimension-tracking in sync, since dimension determines sky-light presence in chunk packets. No field changes. Source: `Protocol1_9_1To1_9_3.java:115140`.
**Serverbound: no changes**
`ServerboundPackets1_9_3` is byte-for-byte identical to `ServerboundPackets1_9` (same 30 packets, same IDs). Source: `v1_9_1to1_9_3/packet/ServerboundPackets1_9_3.java`.
ViaVersion git log for `v1_9_1to1_9_3` (shallow): `cff9a8715` (copyright), `fd5dadbe0` (clean imports), `e436bbe37` (dimension switch refactor), `c5756fe45` (BlockPosition rename), `75d86851c` (reformat), `501f65e21` (packet/entity renames), `e965e9713` (package renames).
mc-wiki: 1.9.3 introduced Netty blockedservers check and memory-use fixes for pathfinding/biome caching; 1.9.4 fixed the snapshot warning and 5 additional bugs. ([minecraft.wiki/w/Java_Edition_1.9.3](https://minecraft.wiki/w/Java_Edition_1.9.3), [1.9.4](https://minecraft.wiki/w/Java_Edition_1.9.4), fetched 2026-06-19.)
---
## Proxy and translation impact
### What a 1.9-aware proxy must handle at protocol 107 (largest workload)
1. **Full play-state packet ID remapping** — every packet from a 1.8 server must be renumbered before forwarding to a 1.9 client, and vice versa.
2. **Teleport-confirm injection** — proxy must synthesise a teleport ID (e.g. 0) and append it to every `PLAYER_POSITION` forwarded to 1.9 clients; must swallow all `ACCEPT_TELEPORTATION` from those clients before they reach a 1.8 server.
3. **Boss Bar emulation** — for 1.9 clients on 1.8 servers: track Wither/EnderDragon entity metadata and synthesise `BOSS_EVENT` packets. Requires an entity tracker.
4. **Chunk format translation** — 1.8 sends `MAP_BULK_CHUNK`; proxy must split into individual `MAP_CHUNK` packets encoded as `ChunkType1_9_1`. 1.8 unload-via-bitmask=0 must become `FORGET_LEVEL_CHUNK`.
5. **Dual-hand shim**`USE_ITEM` (SB 0x1D) has no 1.8 equivalent; proxy synthesises a `BLOCK_PLACE` at pos 1,1,1 face 255; off-hand `USE_ITEM_ON` packets are cancelled. `SWING` loses its hand argument.
6. **Sound category insertion** — 1.8's `NAMED_SOUND_EFFECT` lacks the sound category field; proxy must inject a category VarInt.
7. **Entity coordinate conversion** — Double → Int×32 for spawn packets going toward 1.8 servers; Int/32 → Double for spawn packets coming from 1.8 servers.
8. **Entity-equipment slot remapping** — old Short (0=hand, 14=armor) ↔ new VarInt (0=main, 1=off, 25=armor).
9. **Component type upgrades** — disconnect reason, title text, player_info display name, tab-list header/footer: String → JSON component.
10. **`SET_COMPRESSION` absorption** — Play-state compression packet from 1.8 servers must not reach 1.9 clients.
### Additional at protocol 108/109
- Rewrite `LOGIN` dimension field: Int (1.9.1 server) ↔ Byte (1.9 client).
- Remap `SOUND` IDs ≥ 415 (+1 when going server→1.9 client against a 1.9.1+ server).
### Additional at protocol 110 (1.9.3/1.9.4)
- **Chunk format upgrade**: strip embedded `CompoundTag[]` from incoming 1.9.3 chunk packets when forwarding to 1.9/1.9.2 clients; or synthesise fake tile-entity NBT when forwarding 1.9-server chunks to 1.9.3+ clients.
- **`UPDATE_SIGN``BLOCK_ENTITY_DATA`**: translate sign update packet in either direction depending on server/client version.
- **Sound pitch rescaling**: apply `round(p / 63.5 × 63.0)` when bridging 1.9.1/1.9.2 ↔ 1.9.3/1.9.4.
ViaVersion handles all of the above via the three packages `v1_8to1_9`, `v1_9to1_9_1`, `v1_9_1to1_9_3` described throughout this document.
+345
View File
@@ -0,0 +1,345 @@
# Java Edition 26.x — Protocol 775 (26.1) / 776 (26.2)
| Version | Protocol | Data Version | Release Date |
|---|---|---|---|
| 26.1 | 775 | 4786 | 2026-03-24 |
| 26.1.1 | 775 | 4788 | <!-- VERIFY exact date --> |
| 26.1.2 | 775 | 4790 | <!-- VERIFY exact date --> |
| 26.2 | 776 | 4903 | 2026-06-16 |
Sources: minecraft.wiki/w/Java_Edition_26.1 (fetched 2026-06-19), minecraft.wiki/w/Java_Edition_26.2 (fetched 2026-06-19), minecraft-data `protocolVersions.json` (`/tmp/mcproto-refs/minecraft-data/data/pc/common/protocolVersions.json`), ViaVersion `ProtocolVersion.java:96` (`/tmp/mcproto-refs/ViaVersion`).
---
## The versioning-scheme rename: `1.X.Y``YY.N.hotfix`
### What changed
Starting with 26.1 (released 2026-03-24), Mojang retired the `1.X.Y` naming convention it had used since Java Edition's early release era and replaced it with a **calendar-based `year.drop.hotfix`** format.
> "This is the first Java Edition release version to use the new 'year.drop.hotfix' version format announced in December 2025."
> — minecraft.wiki/w/Java_Edition_26.1 (fetched 2026-06-19)
**What the fields mean:**
| Field | Meaning | Example |
|---|---|---|
| `YY` | Two-digit year | `26` = 2026 |
| `N` | Drop number within that year (first, second, …) | `1` = first 2026 drop, `2` = second |
| `.hotfix` | Optional patch suffix; shares the same protocol number as the base drop | `26.1.1`, `26.1.2` |
The old `1.X.Y` scheme was a pseudo-semver inherited from pre-1.0 Minecraft versioning and carried no calendar meaning. The new format makes the release cadence legible at a glance and aligns Java Edition with Mojang's publicly communicated "drops" model (multiple major content updates per year instead of one big annual release).
### What did NOT change
- **Protocol wire format**: the protocol version number continues the existing integer sequence (`773 = 1.21.9`, `774 = 1.21.11`, `775 = 26.1`, `776 = 26.2`). The rename is purely in the human-readable game version string; the handshake's VarInt protocol field is unaffected.
- **Snapshot scheme**: pre-release builds still use the `0x40000000` high-bit convention (`0x40000000 + N`). 26.1's snapshot range ran `0x4000011F` (snapshot-1) through `0x4000012F` (rc-3); 26.2's ran `0x40000134` (snapshot-2) through `0x40000142` (rc-2). Source: minecraft-data `protocolVersions.json`.
- **Snapshot naming convention**: the `YYwNNa` week-snapshot style is still used — `26w14a` appears in minecraft-data between 26.1.2 and the first 26.2 snapshot (dataVersion 5000, protocol `0x40000131`), consistent with how April-type or interstitial snapshots have always been named.
---
## 26.1 — Protocol 775 (vs 1.21.11 / protocol 774)
**Release name:** "Tiny Takeover"
**Development timeline:** 11 snapshots, 3 pre-releases, 3 release candidates.
**Pack formats:** Resource pack 84.0, Data pack 101.1.
**Minimum Java:** Java SE 25 (LTS); first version to require Java 25.
**Obfuscation:** First release without an obfuscated jar variant.
ViaVersion package: `v1_21_11to26_1/` at `/tmp/mcproto-refs/ViaVersion/common/src/main/java/com/viaversion/viaversion/protocols/v1_21_11to26_1/`.
### Headline gameplay changes (mc-wiki, fetched 2026-06-19)
- **Golden Dandelion**: new flower (crafted from 1 dandelion + 8 gold nuggets) that pauses/resumes baby mob aging on right-click.
- **Baby mob overhaul**: updated models, textures, and sounds for 70+ baby mobs — cats, chickens, cows, horses, wolves, pigs, sheep, rabbits, axolotls, squids, foxes, goats, camels, armadillos, polar bears, llamas, zombies, husks, drowned, piglins, villagers, zombie villagers. Dedicated baby armor textures; saddles and armor no longer rendered on baby pigs, camels, wolves.
- **Craftable name tags**: paper + any nugget type (previously uncraftable).
- **Note block trumpet**: new instrument on copper blocks; sound varies with oxidation level.
- **`/swing` command**: animate entity arm movements.
- **`/time` redesign**: now references world clocks (dimension-specific timekeeping registry).
- **Villager trades data-driven**: `villager_trade` and `trade_set` datapack types.
- **Stonecutter**: deepslate and stone craftable directly into variants.
- **Requires Java SE 25**: first such requirement.
### Protocol changes in 775
Sources: ViaVersion `ClientboundPackets26_1.java`, `ServerboundPackets26_1.java`, `Protocol1_21_11To26_1.java`, `ChunkSectionType26_1.java`, `EntityDataTypes26_1.java` (all under `/tmp/mcproto-refs/ViaVersion/...`).
#### New clientbound packets (play state)
Three packets present in 26.1 but absent from 1.21.11:
| ID | Name | Purpose |
|---|---|---|
| `0x27` | `GAME_RULE_VALUES` | Sends current game rule values to the client (new in 26.1; replaces or supplements the `/gamerule` response flow) |
| `0x32` | `LOW_DISK_SPACE_WARNING` | Server notifies client of low disk space condition |
> Note: `CLEAR_DIALOG` (0x8B) and `SHOW_DIALOG` (0x8C) and `TRACKED_WAYPOINT` (0x8A) already existed in 1.21.11 at `0x89`, `0x8A`, `0x88` respectively. They shifted by two positions because `GAME_RULE_VALUES` and `LOW_DISK_SPACE_WARNING` were inserted earlier in the enum. All IDs shifted accordingly from `GAME_RULE_VALUES`'s position onward.
`PLAYER_ROTATION` (`0x49`) similarly existed in 1.21.11 at `0x47`; no new packet, just renumbered by the two insertions above it.
ViaVersion cancels `GAME_RULE_VALUES` when translating 26.1→1.21.11 (i.e. it is dropped for older clients); `LOW_DISK_SPACE_WARNING` is likewise unhandled (implicitly cancelled). Source: `Protocol1_21_11To26_1.java` — only `SET_TIME`, `UPDATE_TAGS`, and `LEVEL_CHUNK_WITH_LIGHT` are explicitly remapped.
#### New serverbound packets (play state)
Four packets present in 26.1 but absent from 1.21.11 / 1.21.6:
| ID | Name | ViaVersion translation |
|---|---|---|
| `0x01` | `ATTACK` | Split from old `INTERACT`; maps to `INTERACT` with action=1 (Attack) |
| `0x39` | `SET_GAME_RULE` | Client requests a game rule change; ViaVersion **cancels** this packet (action==`Request game rule values` case) |
| `0x3E` | `SPECTATE_ENTITY` | Split from `INTERACT`; maps to `INTERACT` with action=1 (Attack) on the target entity |
| `0x44` | `CUSTOM_CLICK_ACTION` | Custom UI click action (existed in 1.21.6 at `0x41`; renumbered by the three insertions above) |
Source: `EntityPacketRewriter26_1.java:registerPackets()` + `ServerboundPackets26_1.java`.
**INTERACT refactor** (significant): In 26.1 the monolithic `INTERACT` packet (which carried action=0/interact, 1/attack, 2/interact-at) is split. `ATTACK` carries the attack action; `SPECTATE_ENTITY` carries spectate. The old `INTERACT` (0x1A) remains for right-click interact. ViaVersion synthesizes the old format from the new: for `ATTACK` and `SPECTATE_ENTITY` it writes entity ID + action=1 (Attack) + sneaking state. Sneaking state is now tracked from `PLAYER_INPUT` flags (bit 5) rather than from `INTERACT`'s `secondaryAction` field; ViaVersion stores this in `PlayerSneaking` per-connection storage. Source: `EntityPacketRewriter26_1.java`.
#### `SET_TIME` restructured (play state, 0x71 → new multi-clock format)
Old format (1.21.11):
```
Long game_time total world age ticks
Long day_time current day time (negative = paused)
Boolean tick_day_time whether time advances
```
New format (26.1):
```
Long game_time total world age ticks
VarInt clock_count number of clock entries (typically 1)
for each clock:
VarInt clock_id registry index into world_clock dimension
VarLong total_ticks current tick count for this clock
Float partial_tick sub-tick interpolation
Float tick_rate 0.0 = paused, 1.0 = normal
```
ViaVersion translation: writes `clock_count=1`, `clock_id=0` (overworld), then maps the old `day_time` to `total_ticks` and `tick_day_time` to `tick_rate` (0F or 1F). Source: `Protocol1_21_11To26_1.java:registerClientbound(SET_TIME)`.
The restructuring is driven by the new **world clock registry** (`world_clock`) that allows per-dimension custom time tracking. ViaVersion injects `world_clock` registry data with entries for `overworld` and `the_end` during `FINISH_CONFIGURATION`. Source: `Protocol1_21_11To26_1.java:appendClientbound(FINISH_CONFIGURATION)`.
#### Chunk section format change (`LEVEL_CHUNK_WITH_LIGHT`)
26.1 adds a **fluid count** short to the chunk section header:
Old section header (1.21.51.21.11):
```
Short non_air_blocks_count
[block palette]
[biome palette]
```
New section header (26.1):
```
Short non_air_blocks_count
Short fluid_count ← NEW
[block palette]
[biome palette]
```
ViaVersion computes `fluid_count` from the block palette during downgrade (scanning each block ID against a set of known fluid block-states). Source: `ChunkSectionType26_1.java:read/write`, `BlockItemPacketRewriter26_1.java:replaceClientbound(LEVEL_CHUNK_WITH_LIGHT)`.
#### Entity data type additions
`EntityDataTypes26_1` adds four new sound-variant entity data types (indices 22, 24, 29, 31), interleaved among the existing variant types:
| Index | Name |
|---|---|
| 22 | `catSoundVariant` |
| 24 | `cowSoundVariant` |
| 29 | `pigSoundVariant` |
| 31 | `chickenSoundVariant` |
These drive the new per-mob sound variant system (distinct cries for different cat breeds, cow breeds, etc.). Existing variant indices above these shift accordingly. Source: `EntityDataTypes26_1.java`.
ViaVersion inserts/removes these via `dataTypeMapper().added(entityDataTypes.catSoundVariant)…register()` in `EntityPacketRewriter26_1.registerRewrites()`.
Also new entity data indices per the entity rewriter:
- Villager: index 19 added (`is_villager_data_finalized`)
- Zombie Villager: index 21 added (`is_villager_data_finalized`)
Source: `EntityPacketRewriter26_1.java:registerRewrites()`.
#### Item component changes
Several structured data keys were renamed/versioned for 26.1. ViaVersion translates:
| Old key (1.21.11) | New key (26.1) | Change |
|---|---|---|
| `JUKEBOX_PLAYABLE1_21_5` | `JUKEBOX_PLAYABLE26_1` | Internal Holder type change (key vs inline data) |
| `INSTRUMENT1_21_5` | `INSTRUMENT26_1` | EitherHolder → Holder |
| `PROVIDES_TRIM_MATERIAL1_21_5` | `PROVIDES_TRIM_MATERIAL26_1` | EitherHolder unwrapping |
| `CHICKEN_VARIANT1_21_5` | `CHICKEN_VARIANT26_1` | Either resolved to int |
| `ZOMBIE_NAUTILUS_VARIANT1_21_11` | `ZOMBIE_NAUTILUS_VARIANT26_1` | Either resolved to int |
| `DAMAGE_TYPE1_21_11` | `DAMAGE_TYPE26_1` | EitherHolder → int |
| `PROVIDES_BANNER_PATTERNS1_21_5` | `PROVIDES_BANNER_PATTERNS26_1` | Key → HolderSet |
| `DAMAGE_RESISTANT1_21_2` | `DAMAGE_RESISTANT26_1` | Tag key → HolderSet |
| `BLOCKS_ATTACKS1_21_5` | `BLOCKS_ATTACKS26_1` | Gains `bypassedBy` field |
New components not present in 1.21.11 (removed on downgrade):
| Component | Description |
|---|---|
| `ADDITIONAL_TRADE_COST` | Transient modifier for villager trade quantities |
| `DYE` | Dye color component for generic color interactions |
| `CAT_SOUND_VARIANT` | Per-item cat sound variant |
| `CHICKEN_SOUND_VARIANT` | Per-item chicken sound variant |
| `COW_SOUND_VARIANT` | Per-item cow sound variant |
| `PIG_SOUND_VARIANT` | Per-item pig sound variant |
Container item `null` semantics: 26.1 uses `null` for empty container slots instead of empty item objects (`count=0`). ViaVersion converts bidirectionally. Source: `BlockItemPacketRewriter26_1.java:upgradeData/downgradeData`.
#### Registry additions (via FINISH_CONFIGURATION injection)
ViaVersion injects these registries for 26.1 clients during configuration:
| Registry key | Contents |
|---|---|
| `world_clock` | `overworld`, `the_end` — dimension clock entries |
| `cat_sound_variant` | Cat breed sound mappings |
| `cow_sound_variant` | Cow variant sound mappings |
| `pig_sound_variant` | Pig variant sound mappings |
| `chicken_sound_variant` | Chicken variant sound mappings |
Also adds `FINISH_CONFIGURATION`-time tag patches: emits an empty `UPDATE_TAGS` if no tags packet was received (to ensure damage type and banner pattern tags are populated). Source: `Protocol1_21_11To26_1.java`.
#### Villager/dimension registry rewrites
The `dimension_type` registry entry gains `has_ender_dragon_fight` (computed from key == `the_end`) and `visual/ambient_light_color`. `wolf_sound_variant` entries are restructured into `adult_sounds`/`baby_sounds` compound tags. `wolf_variant` gains `baby_assets`. `frog_variant`, `chicken_variant`, `cow_variant`, `pig_variant`, `cat_variant` all gain asset-id affixes and baby asset IDs. Source: `Protocol1_21_11To26_1.java:onMappingDataLoaded`.
#### `UPDATE_TAGS` / configuration tags
`UPDATE_TAGS` is replaced via `replaceClientbound` in both play and configuration state to track whether tags were sent (for the FINISH_CONFIGURATION injection logic). A large set of `damage_type` and `banner_pattern` empty tags are added for compatibility. Source: `Protocol1_21_11To26_1.java:handleTags`.
---
## 26.2 — Protocol 776 (vs 26.1 / protocol 775)
**Release name:** "Chaos Cubed"
**Release date:** 2026-06-16
**Development timeline:** 8 snapshots, 6 pre-releases, 2 release candidates.
**Pack formats:** Resource pack 88.0, Data pack 107.1.
**Minimum Java:** Java SE 25 (unchanged).
> **Source note:** The ViaVersion clone (`/tmp/mcproto-refs/ViaVersion`) predates 26.2 — no `v26_1to26_2` package exists in the local ref. Protocol 776 is confirmed from minecraft-data `protocolVersions.json` (entry `{'minecraftVersion': '26.2', 'version': 776, 'dataVersion': 4903}`) and minecraft.wiki/w/Java_Edition_26.2 (fetched 2026-06-19). The packet-level diff below is from wiki sources only. <!-- VERIFY once ViaVersion ships v26_1to26_2 -->
### Headline gameplay changes (mc-wiki, fetched 2026-06-19)
- **Sulfur Caves biome**: new underground biome with sulfur spikes (stalactite/stalagmite mechanics), sulfur springs, and distinctive lighting.
- **New blocks**: sulfur block, polished sulfur, sulfur bricks, sulfur stairs/slabs/walls, chiseled sulfur; cinnabar block and all its variants (same palette); potent sulfur (creates geysers); sulfur spike.
- **Sulfur Cube mob**: passive mob with 12 archetypes (`regular`, `bouncy`, `slow_bouncy`, `fast_flat`, `slow_flat`, `light`, `fast_sliding`, `slow_sliding`, `high_resistance`, `sticky`, `explosive`, `hot`). Physics properties driven by absorbed blocks; can absorb TNT to become explosive.
- **Music Disc "Bounce"**: by fingerspit.
- **`/unpublish` command**: disconnects the integrated (LAN) server.
- **Friends List**: in-game friend request management system. <!-- VERIFY wire-level packet -->
- **Vulkan 1.2 renderer** (experimental): optional renderer with automatic OpenGL fallback.
### Protocol changes in 776
Sources: minecraft.wiki/w/Java_Edition_26.2 (fetched 2026-06-19). No ViaVersion source available for this bump. All protocol claims in this section are wiki-only. <!-- VERIFY -->
#### New entity attributes
Five new entity attributes registered server-side and sent in `UPDATE_ATTRIBUTES`:
| Attribute | Range | Purpose |
|---|---|---|
| `minecraft:air_drag_modifier` | 0.02048.0 | Air resistance multiplier |
| `minecraft:bounciness` | 0.01.0 | Restitution coefficient for bounce mechanics |
| `minecraft:friction_modifier` | 0.02048.0 | Surface friction multiplier |
| `minecraft:below_name_distance` | 0.0512.0 | Minimum distance to show name tag below |
| `minecraft:name_tag_distance` | 0.0512.0 | Maximum render distance for name tag |
#### New game event
`minecraft:bounce` (vibration frequency 2): emitted when an entity with non-zero `bounciness` attribute collides with a surface. Carried in `GAME_EVENT` packets.
#### New item component
`minecraft:sulfur_cube_content`: holds the block stack absorbed by a Sulfur Cube entity, driving archetype behavior.
#### New registry
`minecraft:sulfur_cube_archetype`: 12 entries (`regular`, `bouncy`, `slow_bouncy`, `fast_flat`, `slow_flat`, `light`, `fast_sliding`, `slow_sliding`, `high_resistance`, `sticky`, `explosive`, `hot`). Sent via `REGISTRY_DATA` during configuration.
#### World generation feature renames
`pointed_dripstone` feature type renamed to `speleothem`; `dripstone_cluster` renamed to `speleothem_cluster`. Impacts `LEVEL_CHUNK_WITH_LIGHT` structural data for affected chunk regions. <!-- VERIFY if this touches the wire format or only server-side gen -->
#### New density function
`minecraft:interval_select`: threshold-based density function selector. Server-side only (world generation); not directly in the play protocol. <!-- VERIFY -->
#### Predicate restructuring
Entity predicate `type` field renamed to `minecraft:entity_type`; new `minecraft:entity_tags` sub-predicate. Affects datapack commands and any packets that embed predicates (e.g. `COMMANDS` argument types). <!-- VERIFY exact packet impact -->
#### Chunk section changes
<!-- VERIFY: unknown whether 26.2 changes the chunk section format vs 26.1. The ViaVersion source is absent; assume format is identical to 26.1 (non-air + fluid count shorts + block/biome palettes) unless wiki or ViaVersion indicates otherwise. -->
#### Particle additions
Five new particles: `geyser_base`, `geyser_poof`, `geyser_plume`, `geyser`, `sulfur_cube_goo`. These appear in `LEVEL_PARTICLES` and `SET_ENTITY_DATA` payloads.
---
## 1.21 trajectory continuity
Both 26.1 and 26.2 continue the patterns established in 1.21.x:
- **Structured item components** (`minecraft:*` data components introduced in 1.20.5): 26.1 extends this with `dye`, `additional_trade_cost`, sound variant components; 26.2 adds `sulfur_cube_content`. The `EitherHolder` intermediate layer introduced in 1.21.x is being progressively resolved to plain `Holder` or `int` IDs (see 26.1 item component changes above).
- **Registry-driven content**: new registries (`world_clock`, `sulfur_cube_archetype`, sound variant registries) follow the Configuration-state `REGISTRY_DATA` pattern established in 1.20.2.
- **Chunk section header**: grew from 1.21.5's block-palette-only header to 1.21.x's non-air count, then 26.1 adds fluid count. Still the same short+palette+palette structure.
- **Bundle delimiter and batched chunk delivery**: unchanged from 1.21.x.
- **Snapshot high-bit scheme**: `0x40000000 + N` for all pre-release builds — unbroken from 1.13.
---
## Proxy and ViaVersion translation notes
### 26.1 (775): fully supported by ViaVersion clone
ViaVersion's `v1_21_11to26_1` package translates in both directions. Key proxy concerns:
| Concern | What to do |
|---|---|
| Chunk section fluid count | Recompute from block palette on downgrade; inject zero on upgrade |
| ATTACK / SPECTATE_ENTITY → INTERACT | Map to `action=1` (attack); track sneak state from `PLAYER_INPUT` bit 5 |
| INTERACT → ATTACK + optional INTERACT_AT | Split one packet into two for upgrade (entity id + action + location) |
| SET_TIME multi-clock → old format | Use clock 0 (overworld) only; extract `total_ticks` + `tick_rate` |
| SET_GAME_RULE serverbound cancel | Drop `SET_GAME_RULE` action=2 (request values) entirely |
| GAME_RULE_VALUES cancel | Not sent to 1.21.11 clients |
| LOW_DISK_SPACE_WARNING cancel | Not sent to older clients |
| World clock registry injection | Inject `world_clock` entries in FINISH_CONFIGURATION |
| Sound variant registries | Inject `cat_sound_variant`, `cow_sound_variant`, etc. in FINISH_CONFIGURATION |
| Entity data index shifts | catSoundVariant@22, cowSoundVariant@24, pigSoundVariant@29, chickenSoundVariant@31 inserted |
| Item component renames | See table in §item-component-changes above |
Source: `Protocol1_21_11To26_1.java`, `EntityPacketRewriter26_1.java`, `BlockItemPacketRewriter26_1.java`.
### 26.2 (776): no ViaVersion source in local clone
The ViaVersion ref predates 26.2. Expected translation requirements (from wiki analysis):
- New entity attributes (`air_drag_modifier`, `bounciness`, etc.) in `UPDATE_ATTRIBUTES` must be stripped for 26.1 clients.
- `sulfur_cube_archetype` registry must be injected or stripped.
- Five new particles must be remapped or dropped.
- `sulfur_cube_content` item component must be removed on downgrade.
- New game event `bounce` can be suppressed without client-visible impact.
- Entity predicate restructuring may affect `COMMANDS` packet argument types.
<!-- VERIFY all 776 proxy notes once ViaVersion v26_1to26_2 ships -->
---
## Version map summary
| Version string | Protocol | Snapshot range | Confirmed by |
|---|---|---|---|
| 26.126.1.2 | 775 | `0x4000011F``0x4000012F` (snapshot-1 to rc-3) | ViaVersion `ProtocolVersion.java:96`; minecraft-data `protocolVersions.json`; mc-wiki |
| 26.2 | 776 | `0x40000134``0x40000142` (snapshot-2 to rc-2) | minecraft-data `protocolVersions.json`; mc-wiki |
| 26w14a | `0x40000131` | — (interstitial snapshot, April-type) | minecraft-data `protocolVersions.json` (dataVersion 5000) |
+28
View File
@@ -0,0 +1,28 @@
# Version map — 1.7.10 → 26.2
Navigation hub for the per-version deep-dives. One doc per **release line** (`versions/<line>.md`), each with a sub-section per protocol bump inside that line. Protocol numbers from [07-version-differences](../07-version-differences.md) (sourced from ViaVersion `ProtocolVersion.java` + minecraft.wiki + minecraft-data).
**Per-version sources** (see [PLAN §7](../PLAN.md)): mc-wiki release article + protocol page · ViaVersion `protocols/<pkg>/` + its `git log` (commits) · minecraft-data `data/pc/<ver>/protocol.json`.
| Doc | Release line | Protocol #(s) | ViaVersion package(s) | minecraft-data dirs | Era | Status |
|---|---|---|---|---|---|---|
| [1.7.md](1.7.md) | 1.7.10 | 5 | *(< ViaVersion floor; ViaLegacy)* | `1.7` | 1 | ✅ |
| [1.8.md](1.8.md) | 1.8.x | 47 | `v1_8to1_9` (from-side) | `1.8` | 1 | ✅ |
| [1.9.md](1.9.md) | 1.9.x | 107,108,109,110 | `v1_8to1_9`,`v1_9to1_9_1`,`v1_9_1to1_9_3`,`v1_9_3to1_10` | `1.9`,`1.9.1-pre2`,`1.9.2`,`1.9.4` | 2 | ✅ |
| [1.10.md](1.10.md) | 1.10.x | 210 | `v1_9_3to1_10`,`v1_10to1_11` | `1.10`,`1.10.1`,`1.10.2` | 2 | ✅ |
| [1.11.md](1.11.md) | 1.11.x | 315,316 | `v1_10to1_11`,`v1_11to1_11_1`,`v1_11_1to1_12` | `1.11`,`1.11.2` | 2 | ✅ |
| [1.12.md](1.12.md) | 1.12.x | 335,338,340 | `v1_11_1to1_12`,`v1_12to1_12_1`,`v1_12_1to1_12_2`,`v1_12_2to1_13` | `1.12`,`1.12.1`,`1.12.2` | 2 | ✅ |
| [1.13.md](1.13.md) | 1.13.x | 393,401,404 | `v1_12_2to1_13`,`v1_13to1_13_1`,`v1_13_1to1_13_2`,`v1_13_2to1_14` | `1.13`,`1.13.1`,`1.13.2` | 3 | ✅ |
| [1.14.md](1.14.md) | 1.14.x | 477,480,485,490,498 | `v1_13_2to1_14``v1_14_4to1_15` | `1.14`,`1.14.1`,`1.14.3`,`1.14.4` | 3 | ✅ |
| [1.15.md](1.15.md) | 1.15.x | 573,575,578 | `v1_14_4to1_15``v1_15_2to1_16` | `1.15`,`1.15.1`,`1.15.2` | 3 | ✅ |
| [1.16.md](1.16.md) | 1.16.x | 735,736,751,753,754 | `v1_15_2to1_16``v1_16_4to1_17` | `1.16``1.16.5` | 4 | ✅ |
| [1.17.md](1.17.md) | 1.17.x | 755,756 | `v1_16_4to1_17`,`v1_17to1_17_1`,`v1_17_1to1_18` | `1.17`,`1.17.1` | 4 | ✅ |
| [1.18.md](1.18.md) | 1.18.x | 757,758 | `v1_17_1to1_18`,`v1_18to1_18_2`,`v1_18_2to1_19` | `1.18`,`1.18.1`,`1.18.2` | 4 | ✅ |
| [1.19.md](1.19.md) | 1.19.x | 759,760,761,762 | `v1_18_2to1_19``v1_19_4to1_20` | `1.19`,`1.19.2`,`1.19.3`,`1.19.4` | 5 | ✅ |
| [1.20.md](1.20.md) | 1.20.x | 763,764,765,766 | `v1_19_4to1_20``v1_20_5to1_21` | `1.20``1.20.6` | 6 | ✅ |
| [1.21.md](1.21.md) | 1.21.x | 767774 | `v1_20_5to1_21``v1_21_11to26_1` | `1.21`,`1.21.1`,`1.21.3``1.21.11` | 7 | ✅ |
| [26.md](26.md) | 26.126.2 | 775,776 | `v1_21_11to26_1` | `latest` | 7 | ✅ |
**Batches** (per [PLAN §3](../PLAN.md)): B1=Era1 (1.7,1.8) · B2=Era2 (1.91.12) · B3=Era3 (1.131.15) · B4=Era4 (1.161.18) · B5=Era5 (1.19) · B6=Era6 (1.20) · B7=Era7 (1.21,26).
> Note: ViaVersion's floor is 1.8 (protocol 47); 1.7.10 (protocol 5) sits below it — the `1.7.md` doc relies on minecraft-data + mc-wiki + ViaLegacy/ViaRewind references rather than a core ViaVersion package.