A compact, type-safe Python library that implements a Minecraft protocol codec (Java Edition). The project provides a complete type system with primitives, complex types, and packet classes for encoding and decoding Minecraft network traffic, following the official protocol specification.
- Core: Protocol-compliant encoding/decoding for packet bodies, length framing, and optional zlib compression.
- Goal: Safe, testable building blocks for implementing clients, bots, testing tools, or servers that speak the Minecraft Java Edition protocol.
- Architecture: Clean separation between type definitions, packet structures, and network I/O.
-
Comprehensive type system:
- Primitives:
Boolean,Byte,Int,Long,UnsignedShort,String,UUID,VarInt,VarLong,Enum, and more. - Complex types:
Array,PrefixedArray,PrefixedOptional,Identifier,JsonTextComponent,GameProfile. - Base class
DataTypeensures consistent behavior across all types.
- Primitives:
-
Protocol-compliant packet handling:
- Uncompressed and zlib-compressed packet support with automatic threshold-based switching.
- Strict adherence to Minecraft packet framing (VarInt length prefixes, size limits).
- Packet ID management and state-aware packet routing.
-
Fail-fast validation:
- All values validated at construction time (no silent errors).
- Range checking for integers, length validation for strings/arrays.
- Descriptive error messages with expected vs. actual values.
-
Extensible packet architecture:
- Define new packets by subclassing
Packetand implementing_iter_fields(). - Support for both serverbound (client→server) and clientbound (server→client) packets.
- Organized by protocol state: Handshaking, Status, Login, Configuration, Play.
- Define new packets by subclassing
-
Pure Python, dependency-free:
- No external runtime dependencies required.
- Python 3.9+ (uses dataclasses with
slotsandfrozen).
- Python 3.9+ (dataclasses with
slotsandfrozenare used). - No third-party dependencies are necessary for core functionality.
Clone the repository and import it in your project:
git clone https://github.com/your-username/mcprotocol.git
cd mcprotocolUse the package in your code by adding src to PYTHONPATH or installing the package into a virtualenv (project-specific packaging not provided by default).
Example running from repo root:
python -m src.mainAll primitives inherit from DataType and provide __bytes__() for serialization and from_bytes() for deserialization:
from codec.data_types.primitives.varint import VarInt
from codec.data_types.primitives.string import String
from codec.data_types.primitives.unsigned_short import UnsignedShort
# Create and serialize primitives
version = VarInt(773)
address = String("localhost")
port = UnsignedShort(25565)
print(bytes(version)) # b'\xf5\x06'
print(bytes(address)) # VarInt length prefix + UTF-8 encoded string
print(bytes(port)) # b'\\x64\\x4d' (25565 in big-endian)
# Deserialize
version_decoded = VarInt.from_bytes(b'\xf5\x06')
print(version_decoded.value) # 773Define packets by subclassing Packet and implementing _iter_fields():
from codec.packets.packet import Packet
from codec.data_types.primitives.varint import VarInt
from codec.data_types.primitives.string import String
from codec.data_types.primitives.unsigned_short import UnsignedShort
from codec.data_types.primitives.enum import Enum
class Intention(Packet):
"""Handshake packet (serverbound, packet state: Handshaking)."""
__slots__ = ("protocol_version", "server_address", "server_port", "next_state")
def __init__(self, protocol_version: int, server_address: str, server_port: int, next_state: int):
super().__init__(packet_id=VarInt(0x00))
self.protocol_version = VarInt(protocol_version)
self.server_address = String(server_address)
self.server_port = UnsignedShort(server_port)
self.next_state = Enum(next_state, VarInt) # 1=Status, 2=Login, 3=Transfer
def _iter_fields(self):
yield self.protocol_version
yield self.server_address
yield self.server_port
yield self.next_state
# Serialize without compression
packet = Intention(773, "mc.example.com", 25565, 2)
data = packet.serialize() # Returns bytes ready for transmission
print(f"Packet size: {len(data)} bytes")
# Serialize with compression threshold (e.g., 256 bytes)
data_compressed = packet.serialize(compression_threshold=256)
print(f"Compressed packet size: {len(data_compressed)} bytes")Combine primitives into structured data:
from codec.data_types.complex.prefixed_array import PrefixedArray
from codec.data_types.primitives.byte import Byte
# Create an array of bytes with VarInt length prefix
public_key_bytes = PrefixedArray([Byte(71), Byte(34), Byte(122), Byte(19), Byte(8)])
print(bytes(public_key_bytes)) # VarInt(length) + raw bytesPre-implemented packets organized by protocol state:
Intention(serverbound, 0x00): Initiates connectionLegacyServerListPing(serverbound, 0xFE): Legacy ping support
StatusRequest(serverbound, 0x00): Request server statusPingRequest(serverbound, 0x01): Send ping with payloadStatusResponse(clientbound, 0x00): Server status JSONPongResponse(clientbound, 0x01): Ping response
Hello(serverbound, 0x00): Send username and UUIDEncryption Response(serverbound, 0x01): Shared secret + verify tokenCustom Query Answer(serverbound, 0x02): Plugin responseLogin Acknowledged(serverbound, 0x03): Login complete signalCookie Response(serverbound, 0x04): Cookie payloadLoginDisconnect(clientbound, 0x00): Disconnect during loginHello(clientbound, 0x01): Server encryption/auth requestLogin Finished(clientbound, 0x02): GameProfile payloadSet Compression(clientbound, 0x03): Compression thresholdCustom Query(clientbound, 0x04): Plugin requestCookie Request(clientbound, 0x05): Cookie request
Cookie Request(clientbound, 0x00): Cookie request
- Coming soon...
Packet.serialize(compression_threshold: Optional[int]) accepts either None (no compression) or a non-negative integer threshold. Negative values are treated as compression disabled.
None→ uncompressed frame:[VarInt: body_len][body]threshold >= 0:- if
body_len < threshold: sendData Length = VarInt(0)andPacket Length = VarInt(len(Data Length) + body_len), thenData Length + body - if
body_len >= threshold: compressbodywithzlib.compress,Data Length = VarInt(body_len),Packet Length = VarInt(len(Data Length) + len(compressed)), thenPacket Length + Data Length + compressed_body
- if
Example with compression threshold of 256 bytes:
serialized_compressed = packet.serialize(compression_threshold=256)The library automatically handles packet compression when a threshold is provided:
# No compression (development/testing)
serialized = packet.serialize(compression_threshold=None)
# Enable compression with 256 byte threshold
serialized = packet.serialize(compression_threshold=256)Compression behavior:
compression_threshold = None→ all packets uncompressed:[VarInt: body_len][body]compression_threshold < 0→ treated as disabled (same asNone)compression_threshold >= 0:- Packets < threshold:
[VarInt: packet_len][VarInt: 0][body] - Packets >= threshold:
[VarInt: packet_len][VarInt: original_len][zlib_compressed_body]
- Packets < threshold:
No automated tests are currently tracked in this repository.
All primitives and complex types inherit from DataType:
from codec.data_types.data_type import DataType
class MyType(DataType):
def __bytes__(self) -> bytes:
"""Serialize to bytes."""
pass
@classmethod
def from_bytes(cls, data: bytes) -> "MyType":
"""Deserialize from bytes."""
pass| Type | Size | Range / Notes | Example |
|---|---|---|---|
Boolean |
1 byte | 0x00 (False) or 0x01 (True) |
Boolean(True) |
Byte |
1 byte | -128 to 127 | Byte(-50) |
Int |
4 bytes | -2³¹ to 2³¹-1 | Int(12345) |
Long |
8 bytes | -2⁶³ to 2⁶³-1 | Long(9223372036854775807) |
UnsignedShort |
2 bytes | 0 to 65535 | UnsignedShort(25565) |
String |
Variable | UTF-8 + VarInt length (max 32767 chars) | String("hello") |
UUID |
16 bytes | 128-bit UUID | UUID(uuid.uuid4()) |
VarInt |
1-5 bytes | Variable-length 32-bit int | VarInt(300) |
VarLong |
1-10 bytes | Variable-length 64-bit int | VarLong(99999999999) |
Enum |
Depends on base | Restricted integer set | Enum(2, VarInt) |
| Type | Purpose |
|---|---|
Array[T] |
Fixed-length array of items |
PrefixedArray[T] |
Array with VarInt length prefix |
PrefixedOptional[T] |
Boolean-prefixed optional value |
Identifier |
Namespaced identifier (namespace:path) |
JsonTextComponent |
Minecraft chat component (JSON) |
GameProfile |
UUID + name + properties |
class Packet(ABC):
def __init__(self, packet_id: VarInt) -> None:
"""Initialize with packet ID."""
@abstractmethod
def _iter_fields(self) -> Iterable[bytes]:
"""Yield field bytes in protocol order."""
def serialize(self, compression_threshold: Optional[int] = None) -> bytes:
"""Serialize packet with optional compression."""A clientbound response class can accept raw bytes or typed values:
from codec.packets.status.clientbound.pong_response import PongResponse
# From raw bytes:
packet = PongResponse(b"\x00\x00\x00\x00\x00\x00\x00\x2a")
print(packet.timestamp)
# Or from an int directly:
packet = PongResponse(42)
print(bytes(packet))- Follow existing code conventions: classes in
CamelCase, functions and methods insnake_case, docstrings for public API. - Add unit tests for any new primitive or packet. Tests should demonstrate both valid and invalid inputs.
- Ensure all new primitives and packets validate input and follow the contract described above.
This project is licensed under the GNU General Public License v3 (GPL-3.0). See the LICENSE file for details.