Skip to content

Latest commit

 

History

History
320 lines (241 loc) · 9.86 KB

protocol.md

File metadata and controls

320 lines (241 loc) · 9.86 KB

Protocol Guide

MindPy includes a complete, asyncio-native Minecraft Java Edition protocol implementation — no any any other external MC protocol library is required.

This guide covers:

  • How the protocol layer works
  • Supported packet types per version
  • Writing custom packet handlers
  • Adding support for a new version
  • Using the low-level connection directly

Overview

mindpy.protocol/
├── types.py        VarInt, VarLong, PacketBuffer, UUID, Position helpers
├── states.py       ConnectionState enum
├── packets.py      Packet ABC, PacketDirection, PacketRegistry
├── connection.py   MinecraftConnection — TCP, encryption, compression, read loop
├── encryption.py   AES-128-CFB8 via `cryptography`
├── compression.py  zlib compress/decompress helpers
├── auth.py         Microsoft OAuth2 → Xbox → Minecraft auth
├── codec.py        ProtocolRegistry — version-to-registry mapping
├── login.py        LoginOrchestrator — HANDSHAKING → LOGIN → PLAY
└── versions/
    ├── __init__.py auto-loads all version modules
    ├── base.py     shared packets (Handshake, Status, Ping)
    ├── v47.py      1.8  — all Login + core Play packets
    ├── v340.py     1.12.2 additions/overrides
    ├── v754.py     1.16.5 additions/overrides
    ├── v762.py     1.19.4 additions/overrides
    ├── v765.py     1.20.4 additions/overrides
    └── v769.py     1.21+  additions/overrides

Connection state machine

TCP connect
    │
    ▼
HANDSHAKING  ──next_state=1──→  STATUS (ping → pong → disconnect)
    │
    └──next_state=2──→  LOGIN
                            │
                        (1.20.2+) CONFIGURATION
                            │
                            └──────────→  PLAY

The LoginOrchestrator drives this automatically. In PLAY state, the MinecraftConnection._read_loop runs in the background, calling registered packet handlers as packets arrive.


Packet framing (wire format)

Uncompressed:
  [VarInt length] [VarInt packet_id] [... fields ...]

Compressed (after Set Compression):
  [VarInt frame_length]
    [VarInt data_length]   ← uncompressed size; 0 = not compressed
    [... data ...]         ← zlib-compressed if data_length > 0

Encrypted (after EncryptionResponse):
  AES-128-CFB8 wraps the entire wire bytes.
  Key = IV = 16-byte shared_secret negotiated during login.

Supported versions

Protocol Minecraft Key differences from previous
47 1.8 VarInt keepalive; string UUID in LoginSuccess
340 1.12.2 int64 keepalive; ClientSettings (0x04); some ID shifts
754 1.16.5 UUID as 16 raw bytes; TeleportConfirm (0x00); more ID shifts
762 1.19.4 Signed chat; SystemChatMessage (0x60); keepalive 0x20/0x11
765 1.20.4 Configuration state added; keepalive 0x24/0x15
769 1.21+ keepalive 0x26/0x18; JoinGame 0x2B; serves as forward-compat fallback

Versions not in this table automatically fall back to the closest older supported version (e.g. protocol 766 → 765, protocol 770 → 769).


Writing a custom packet handler

Use connection.on_packet() or connection.add_handler():

import asyncio
from mindpy import Bot
from mindpy.protocol.versions.v769 import (
    KeepAliveClientboundPacket,
    KeepAliveServerboundPacket,
    JoinGamePacket,
)

bot = Bot("localhost", protocol_version=769)

@bot.on("bot.connected")
async def on_connected(event) -> None:
    conn = bot._connection

    # Handler decorator
    @conn.on_packet(JoinGamePacket)
    async def handle_join(pkt: JoinGamePacket) -> None:
        print(f"Entity ID: {pkt.entity_id}, Game Mode: {pkt.game_mode}")

    # Programmatic registration
    async def auto_keepalive(pkt: KeepAliveClientboundPacket) -> None:
        await conn.send_packet(KeepAliveServerboundPacket(
            keep_alive_id=pkt.keep_alive_id
        ))

    conn.add_handler(KeepAliveClientboundPacket, auto_keepalive)

async with bot:
    await bot.run()

Note: The Bot class already registers KeepAlive, Position, Health, JoinGame, Chat, and Disconnect handlers automatically. You only need to add custom handlers for packets not covered by default.


Sending packets

from mindpy.protocol.versions.v769 import ChatMessageServerboundPacket

await conn.send_packet(ChatMessageServerboundPacket(message="Hello!"))

Any Packet subclass can be sent. send_packet handles encryption and compression transparently.


Defining a new packet

from __future__ import annotations
from dataclasses import dataclass
from mindpy.protocol.packets import Packet, PacketDirection
from mindpy.protocol.states import ConnectionState
from mindpy.protocol.types import PacketBuffer

@dataclass
class MyCustomPacket(Packet):
    packet_id = 0x42
    state = ConnectionState.PLAY
    direction = PacketDirection.CLIENTBOUND

    some_int: int = 0
    some_str: str = ""

    def encode(self, buf: PacketBuffer, protocol_version: int) -> None:
        buf.write_varint(self.some_int)
        buf.write_string(self.some_str)

    @classmethod
    def decode(cls, buf: PacketBuffer, protocol_version: int) -> "MyCustomPacket":
        return cls(
            some_int=buf.read_varint(),
            some_str=buf.read_string(),
        )

Register it in the relevant version registry:

from mindpy.protocol.codec import ProtocolRegistry

codec = ProtocolRegistry.get(769)
codec.registry.register(MyCustomPacket)

Adding a new protocol version

Create mindpy/protocol/versions/vXXX.py:

"""Minecraft 1.22 protocol (version 800)."""
from __future__ import annotations
from mindpy.protocol.codec import ProtocolRegistry
from mindpy.protocol.packets import PacketRegistry
from mindpy.protocol.versions.v769 import *   # inherit all v769 packets

PROTOCOL_VERSION = 800
_registry = PacketRegistry()

# Re-register unchanged packets
from mindpy.protocol.versions.v769 import (
    LoginStartPacket, LoginSuccessPacket, # etc.
)
for _cls in [...]:
    _registry.register(_cls)

# Override changed packets
from dataclasses import dataclass
from mindpy.protocol.packets import Packet, PacketDirection
from mindpy.protocol.states import ConnectionState
from mindpy.protocol.types import PacketBuffer

@dataclass
class KeepAliveClientboundPacket(Packet):
    packet_id = 0x28   # new ID in v800
    state = ConnectionState.PLAY
    direction = PacketDirection.CLIENTBOUND
    keep_alive_id: int = 0

    def encode(self, buf: PacketBuffer, protocol_version: int) -> None:
        buf.write_long(self.keep_alive_id)

    @classmethod
    def decode(cls, buf: PacketBuffer, protocol_version: int) -> "KeepAliveClientboundPacket":
        return cls(keep_alive_id=buf.read_long())

_registry.register(KeepAliveClientboundPacket)
ProtocolRegistry.register(PROTOCOL_VERSION, _registry)

Then add the import to mindpy/protocol/versions/__init__.py:

from mindpy.protocol.versions import v800   # noqa: F401

PacketBuffer reference

Write Read Type
write_varint(n) read_varint() Variable-length int32
write_varlong(n) read_varlong() Variable-length int64
write_string(s, max_len=32767) read_string(max_len=32767) UTF-8 with VarInt length prefix
write_bool(b) read_bool() 0x00 / 0x01
write_byte(n) read_byte() Signed int8
write_ubyte(n) read_ubyte() Unsigned int8
write_short(n) read_short() Signed int16
write_ushort(n) read_ushort() Unsigned int16
write_int(n) read_int() Signed int32
write_long(n) read_long() Signed int64
write_float(f) read_float() IEEE 754 float32
write_double(d) read_double() IEEE 754 float64
write_bytes(b) read_bytes(n) Raw bytes
write_uuid(u) read_uuid() uuid.UUID ↔ 16 bytes

Authentication

Offline mode (default)

No authentication — the username is sent as-is. The server must have online-mode=false.

Online mode (Microsoft OAuth2)

from mindpy.protocol.auth import MicrosoftAuth, AuthProfile

async with MicrosoftAuth() as auth:
    profile = await auth.device_flow_auth()
    # Prints:
    # Visit https://microsoft.com/link
    # Enter code: XXXX-XXXX
    # (15-minute window)

print(profile.username)      # Minecraft username
print(profile.uuid)          # UUID string (with hyphens)
print(profile.access_token)  # Minecraft access token

bot = Bot("play.example.com", auth_profile=profile, online_mode=True)

The LoginOrchestrator handles the full RSA key exchange when the server sends EncryptionRequest:

  1. Generates a random 16-byte shared_secret
  2. Computes Minecraft's SHA1 server hash: sha1(server_id + shared_secret + public_key_der)
  3. POSTs to https://sessionserver.mojang.com/session/minecraft/join
  4. RSA-encrypts shared_secret and verify_token with server's public key
  5. Sends EncryptionResponse
  6. Enables AES-128-CFB8 on both sides simultaneously

Server ping (status check)

import asyncio
from mindpy.protocol.connection import MinecraftConnection
from mindpy.protocol.login import LoginOrchestrator
from mindpy.protocol.codec import ProtocolRegistry
import mindpy.protocol.versions

async def ping(host: str, port: int = 25565) -> None:
    codec = ProtocolRegistry.get(769)
    conn = MinecraftConnection(host, port, 769, codec.registry)
    await conn.connect()

    orch = LoginOrchestrator(conn, username="")
    info = await orch.ping()

    print(f"Version:  {info.version_name} (protocol {info.protocol_version})")
    print(f"Players:  {info.online_players}/{info.max_players}")
    print(f"MOTD:     {info.description}")
    print(f"Latency:  {info.latency_ms:.1f} ms")

    await conn.close()

asyncio.run(ping("mc.hypixel.net"))