Overview
The minecraft/protocol package provides low-level data structures and encoding/decoding functions for the Minecraft Bedrock Edition protocol. These structures are used throughout the packet system.
Data Types
Primitive Types
The protocol uses various integer and floating-point types:
// Unsigned integers
var u8 uint8 // 1 byte
var u16 uint16 // 2 bytes (little-endian)
var u32 uint32 // 4 bytes (little-endian)
var u64 uint64 // 8 bytes (little-endian)
// Signed integers
var i8 int8 // 1 byte
var i16 int16 // 2 bytes (little-endian)
var i32 int32 // 4 bytes (little-endian)
var i64 int64 // 8 bytes (little-endian)
// Floating point
var f32 float32 // 4 bytes (IEEE 754)
var f64 float64 // 8 bytes (IEEE 754)
// Boolean
var b bool // 1 byte (0 = false, 1 = true)
Variable-Length Integers
Varints use variable-length encoding to save space:
import "github.com/sandertv/gophertunnel/minecraft/protocol"
// Write a varint32
writer := protocol.NewWriter(buf, 0)
value := int32(12345)
writer.Varint32(&value)
// Read a varint32
reader := protocol.NewReader(buf, 0, false)
var result int32
reader.Varint32(&result)
Available varint types:
Varint32() - Variable-length signed 32-bit integer
Varuint32() - Variable-length unsigned 32-bit integer
Varint64() - Variable-length signed 64-bit integer
Varuint64() - Variable-length unsigned 64-bit integer
Varints are more efficient for small numbers. A small varint32 uses only 1 byte, while a regular int32 always uses 4 bytes.
Strings
Strings are encoded as a varuint32 length prefix followed by UTF-8 bytes:
// Write string
writer.String(&"Hello, world!")
// Read string
var text string
reader.String(&text)
Byte Slices
Similar to strings, with length prefix:
data := []byte{1, 2, 3, 4, 5}
writer.ByteSlice(&data)
var result []byte
reader.ByteSlice(&result)
Vector Types
Vec2
2D vector using mgl32.Vec2:
import "github.com/go-gl/mathgl/mgl32"
position := mgl32.Vec2{10.5, 20.3}
writer.Vec2(&position)
var pos mgl32.Vec2
reader.Vec2(&pos)
// pos[0] = X coordinate
// pos[1] = Y coordinate
Vec3
3D vector using mgl32.Vec3:
position := mgl32.Vec3{100.0, 64.0, -50.0}
writer.Vec3(&position)
var pos mgl32.Vec3
reader.Vec3(&pos)
// pos[0] = X coordinate
// pos[1] = Y coordinate
// pos[2] = Z coordinate
BlockPos
Block position using varint32 coordinates:
import "github.com/sandertv/gophertunnel/minecraft/protocol"
blockPos := protocol.BlockPos{16, 64, -32}
writer.BlockPos(&blockPos)
var pos protocol.BlockPos
reader.BlockPos(&pos)
// pos[0] = X coordinate
// pos[1] = Y coordinate
// pos[2] = Z coordinate
ChunkPos
Chunk position (2D):
import "github.com/sandertv/gophertunnel/minecraft/protocol"
chunkPos := protocol.ChunkPos{5, -3}
writer.ChunkPos(&chunkPos)
var pos protocol.ChunkPos
reader.ChunkPos(&pos)
// pos[0] = X chunk coordinate
// pos[1] = Z chunk coordinate
UUID
UUIDs use the github.com/google/uuid package:
import "github.com/google/uuid"
// Write UUID
id := uuid.New()
writer.UUID(&id)
// Read UUID
var playerID uuid.UUID
reader.UUID(&playerID)
NBT Data
NBT (Named Binary Tag) is used for complex structured data:
import "github.com/sandertv/gophertunnel/minecraft/nbt"
type PlayerData struct {
Name string `nbt:"name"`
Health float32 `nbt:"health"`
Pos []float32 `nbt:"pos"`
}
// Encode NBT
data := PlayerData{
Name: "Steve",
Health: 20.0,
Pos: []float32{0, 64, 0},
}
writer.NBT(&data, nbt.NetworkLittleEndian)
// Decode NBT
var result PlayerData
reader.NBT(&result, nbt.NetworkLittleEndian)
NBT encoding formats:
nbt.NetworkLittleEndian - Used in most network packets
nbt.LittleEndian - Used in some packets
nbt.BigEndian - Rarely used in Bedrock Edition
Game Data Structures
Item Stacks
import "github.com/sandertv/gophertunnel/minecraft/protocol"
item := protocol.ItemStack{
ItemType: protocol.ItemType{
NetworkID: 1, // Diamond
MetadataValue: 0,
},
Count: 64,
HasStack: true,
}
Attributes
attribute := protocol.Attribute{
Name: "minecraft:health",
Value: 20.0,
Max: 20.0,
Min: 0.0,
Default: 20.0,
}
Game Rules
gameRules := []protocol.GameRule{
{
Name: "showcoordinates",
Value: true,
},
{
Name: "dodaylightcycle",
Value: false,
},
}
Skin Data
import "github.com/sandertv/gophertunnel/minecraft/protocol"
skin := protocol.Skin{
SkinID: "custom_skin",
SkinResourcePatch: skinPatch,
SkinImageWidth: 64,
SkinImageHeight: 64,
SkinData: skinPixelData,
CapeImageWidth: 0,
CapeImageHeight: 0,
CapeData: []byte{},
SkinGeometry: geometryData,
AnimationData: "",
Premium: false,
Persona: false,
}
Protocol Constants
Current Version
import "github.com/sandertv/gophertunnel/minecraft/protocol"
fmt.Println(protocol.CurrentProtocol) // e.g., 712
fmt.Println(protocol.CurrentVersion) // e.g., "1.21.50"
Device Types
const (
DeviceAndroid = 1
DeviceIOS = 2
DeviceOSX = 3
DeviceFireOS = 4
DeviceGearVR = 5
DeviceHololens = 6
DeviceWin10 = 7
DeviceWin32 = 8
DeviceDedicated = 9
DeviceTVOS = 10
DevicePS4 = 11
DeviceNintendo = 12
DeviceXbox = 13
DeviceWindowsPhone = 14
)
Packet IDs
Packet IDs are defined in minecraft/protocol/packet/id.go:
const (
IDLogin = 1
IDPlayStatus = 2
IDServerToClientHandshake = 3
IDClientToServerHandshake = 4
IDDisconnect = 5
IDResourcePacksInfo = 6
IDResourcePackStack = 7
IDText = 9
IDStartGame = 11
IDMovePlayer = 19
// ... many more
)
Reader and Writer
Creating Reader/Writer
import (
"bytes"
"github.com/sandertv/gophertunnel/minecraft/protocol"
)
// Create a buffer
buf := bytes.NewBuffer(nil)
// Create writer
shieldID := int32(0)
writer := protocol.NewWriter(buf, shieldID)
// Write data
writer.Varuint32(&count)
writer.String(&name)
writer.Vec3(&position)
// Create reader from data
reader := protocol.NewReader(buf, shieldID, false)
// Read data back
var count uint32
var name string
var position mgl32.Vec3
reader.Varuint32(&count)
reader.String(&name)
reader.Vec3(&position)
Error Handling
Readers panic on errors (caught by deferred recovery in packet decoding):
func decodePacket(data []byte) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("decode error: %v", r)
}
}()
reader := protocol.NewReader(bytes.NewReader(data), 0, false)
var value uint32
reader.Varuint32(&value) // Panics on invalid data
return nil
}
Endianness
Minecraft Bedrock Edition primarily uses little-endian encoding:
// Little-endian (default)
reader.Uint32(&value) // Little-endian uint32
writer.Uint32(&value)
// Big-endian (explicit)
reader.BigEndianUint32(&value)
writer.BigEndianUint32(&value)
Specialized readers/writers are available:
reader_little_endian.go - Little-endian operations (most common)
reader_big_endian.go - Big-endian operations (rare)
writer_little_endian.go - Little-endian operations
writer_big_endian.go - Big-endian operations
Advanced Structures
Block States
blockState := protocol.BlockState{
Name: "minecraft:stone",
Properties: map[string]any{
"stone_type": "granite",
},
Version: 1,
}
Command Enums
enum := protocol.CommandEnum{
Type: "GameMode",
Options: []string{"survival", "creative", "adventure", "spectator"},
}
Abilities
abilities := protocol.AbilityData{
Type: protocol.AbilityBuild,
Value: protocol.AbilityBaseFalse | protocol.AbilityLayerEnabled,
}
Scoreboard Entries
entry := protocol.ScoreboardEntry{
EntryID: 1,
ObjectiveName: "kills",
Score: 10,
Type: protocol.ScoreboardEntryPlayer,
PlayerUUID: playerID,
}
Protocol Versioning
Handle multiple protocol versions:
import "github.com/sandertv/gophertunnel/minecraft"
type customProtocol struct{}
func (p customProtocol) ID() int32 {
return 712 // Protocol version
}
func (p customProtocol) Packets(serverSide bool) packet.Pool {
// Return packet pool for this protocol version
return packet.NewPool()
}
func (p customProtocol) ConvertToLatest(pk packet.Packet, conn *minecraft.Conn) []packet.Packet {
// Convert old packet format to latest
return []packet.Packet{pk}
}
func (p customProtocol) ConvertFromLatest(pk packet.Packet, conn *minecraft.Conn) []packet.Packet {
// Convert latest packet format to old
return []packet.Packet{pk}
}
Best Practices
Always use the high-level Conn.ReadPacket() and Conn.WritePacket() methods unless you have a specific reason to use low-level protocol operations.
The protocol package uses pointer parameters for all read/write operations. Always pass pointers, not values.
Good Example
var count uint32
reader.Varuint32(&count) // Correct: passing pointer
Bad Example
var count uint32
reader.Varuint32(count) // Wrong: compiler error
Complete Example
Custom packet implementation:
package mypackets
import (
"github.com/sandertv/gophertunnel/minecraft/protocol"
"github.com/sandertv/gophertunnel/minecraft/protocol/packet"
"github.com/go-gl/mathgl/mgl32"
)
type CustomTeleport struct {
PlayerName string
Position mgl32.Vec3
Yaw float32
Pitch float32
}
func (pk *CustomTeleport) ID() uint32 {
return 0x99 // Custom packet ID
}
func (pk *CustomTeleport) Marshal(io protocol.IO) {
io.String(&pk.PlayerName)
io.Vec3(&pk.Position)
io.Float32(&pk.Yaw)
io.Float32(&pk.Pitch)
}
// Usage
func sendTeleport(conn *minecraft.Conn) {
conn.WritePacket(&CustomTeleport{
PlayerName: "Steve",
Position: mgl32.Vec3{100, 64, 200},
Yaw: 90,
Pitch: 0,
})
}