Corrected real errors: several 1.7.x release dates, resource_pack_send version, config packet ordering, structured-component count (56), PLAYER_LOADED (1.21.4), entity_sound_effect field order. Confirmed+cited the rest; remaining ~19 items re-marked UNCONFIRMED (third-party/ViaLegacy/26.2 internals unreachable from refs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
22 KiB
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, 1.18.1, 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 0–255 (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 (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 57–75 (serverbound CLIENT_INFORMATION handler) synthesises the simulation distance from view distance since the older client doesn't send it. EntityPacketRewriter1_18.java line 57–58 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 0–255, 16 sections | Y −64–319, 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
- 1–3 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). Confirmed: PaletteType.java:27 declares BIOMES(ChunkSection.BIOME_SIZE, 3), so any bpv > 3 falls through to the global/direct palette (PaletteType1_18.java:55-56). The global palette width = MathUtil.ceilLog2(tracker.biomesSent()) — for 1.18's 61 registered biomes this is 6 bits.
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 dimensionmin_y(int) — minimum Y coordinate (negative for the expanded overworld: −64)
ViaVersion reads both to determine section count and minimum Y:
// 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_UPDATEfor an unloaded chunk: cancels the packet (does not forward it) and caches the light data keyed by(chunkX, chunkZ). - On
LIGHT_UPDATEfor an already-loaded chunk: passes through (and optionally caches, controlled bycache-1_17-lightconfig). - On
LEVEL_CHUNK: looks up the cached light, appends it to the mergedLEVEL_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:
// 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 (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:
// 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 #:
// 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:
// 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:
-
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.
-
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
DataPaletteImplper 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).
- 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
-
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'sChunkLightStorageimplements this —ChunkLightStorage.java:28-68,WorldPacketRewriter1_18.java:66-103. -
Block entity format: 1.17.1 sends block entities as raw NBT with
x/y/z/idfields 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. -
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. -
simulationDistanceinjection: When a 1.18 server sendsLOGIN, ViaVersion injects asimulationDistancefield (copied fromviewDistance) for old 1.17.1 clients that don't know about it —EntityPacketRewriter1_18.java:57-58. In the reverse direction (serverbound),CLIENT_INFORMATIONfrom a 1.17.1 client drops theenableServerListingbool 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
effectIdinUPDATE_MOB_EFFECTandREMOVE_MOB_EFFECTbetween i8 and varint. Simple field-type swap, no data loss (effect IDs fit in a byte). - NBT prefix: add/strip
#on theinfiniburnstring tag inLOGINregistry andRESPAWNdimension NBT. - Tags: inject or strip the
fall_damage_resettingblock 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.