Version-aware C++ packet codecs for the Minecraft: Bedrock protocol, generated from a Python schema.
Bedrock's wire format moves between protocol versions: fields appear, packets get
reordered, and whole packets migrate to BDS's Cereal serialization. This repository
holds one schema covering every version it supports, and a protoc-shaped compiler
(bpc) that turns it into C++ types and serializers — one shape per version, so a
consumer names the version it wants and gets a compile error if it asks for a field
that era never had.
Modelled today: protocol 975 (1.26.20), 1001 (1.26.30), 2168 (1.26.40) and 2192 (1.26.50.26).
The schema lives in protocol/*.py. A packet whose shape changed is declared once
per era; anything smaller carries its own version range:
@packet(id=175, until=1001)
class SubChunkRequestPacket:
dimension_type: DimensionType
center_pos: SubChunkPos
sub_chunk_pos_offsets: list[SubChunkPosOffset] = field(prefix=uint32)
@packet(id=175, since=1001)
class SubChunkRequestPacket:
dimension_type: DimensionType
sub_chunk_pos_offsets: list[SubChunkPosOffset]
center_pos: SubChunkPosThe compiler emits one struct per version behind a selector alias, so both eras are reachable from a single name:
#include <bedrock/protocol.hpp>
namespace bp = bedrock::protocol;
using Packet = bp::SubChunkRequestPacket_<975>;
Packet packet;
packet.dimension_type = static_cast<bp::DimensionType>(0);
std::string buffer;
bp::BinaryWriter writer{buffer};
bp::Serializer<Packet>::serialize(writer, packet);
bp::BinaryReader reader{buffer};
std::expected<Packet, std::error_code> back = bp::Serializer<Packet>::deserialize(reader);Unversioned types keep their plain name; bp::SubChunkRequestPacket without the
suffix is the latest version.
Requires CMake, a C++23 compiler, and uv, which runs the compiler during the build. CI covers GCC (libstdc++), Clang 18 (libc++) and MSVC.
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
ctest --test-dir build --output-on-failureBEDROCK_PROTOCOL_BUILD_TESTS and BEDROCK_PROTOCOL_INSTALL both default to on for a
top-level build and off when the project is added as a subdirectory.
Code generation is wired into the build, but bpc can be run directly:
uv run bpc --language cpp --out build/protocol --import-path . protocol/inventory.pyAs a subproject. Code generation runs during the build, so this needs uv on the consumer's machine too:
add_subdirectory(bedrock-protocol)
target_link_libraries(my_target PRIVATE bedrock::protocol)Or install it once and consume the built artifacts, which needs no Python at all:
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/opt/bedrock-protocol
cmake --build build
cmake --install buildfind_package(bedrock-protocol CONFIG REQUIRED)
target_link_libraries(my_target PRIVATE bedrock::protocol)The install tree:
| path | |
|---|---|
include/bedrock/protocol.hpp |
umbrella — the runtime plus every generated header |
include/bedrock/protocol/*.h |
one generated header per packet family, each paired with a compiled .cpp |
include/bedrock/protocol/*.hpp |
header-only runtime — binary streams, Serializer, NBT, UUID |
include/bedrock/protocol/detail/ |
plumbing the generated code leans on, not meant to be included directly |
lib/libbedrock_protocol.a |
the generated serializer bodies |
lib/cmake/bedrock-protocol/ |
the CMake package find_package resolves |
A consumer that only touches one family can include <bedrock/protocol/inventory.h>
rather than the umbrella.
| path | |
|---|---|
protocol/ |
the schema — one module per packet family |
src/bedrock_protocol/ |
the compiler: compiler/ (parser, descriptors, pool) and compiler/cpp/ (backend) |
include/bedrock/protocol/ |
hand-written runtime — binary streams, the Serializer entry point, NBT |
tests/ |
per-packet round-trip tests against goldens generated by running gophertunnel |
Wire shapes are taken from protocol-docs,
names and C++ types from reverse-engineered BDS headers, and golden bytes from
gophertunnel. CLAUDE.md documents those
sources and the conventions the schema follows.
MIT. See LICENSE.