Packet Structure
Hytale uses a custom binary protocol built on top of Netty. All packets are framed with a length prefix and use little-endian byte order for multi-byte integers.
Frame Format
Section titled “Frame Format”Every packet is wrapped in a frame with the following structure:
┌─────────────────────────────────────────────────────────────┐│ Payload Length (4 bytes, LE) │ Packet ID (4 bytes, LE) │├─────────────────────────────────────────────────────────────┤│ Payload (N bytes) ││ (optionally Zstd compressed) │└─────────────────────────────────────────────────────────────┘| Field | Size | Description |
|---|---|---|
| Payload Length | 4 bytes (int32 LE) | Size of payload in bytes |
| Packet ID | 4 bytes (int32 LE) | Unique packet type identifier |
| Payload | Variable | Packet data (may be compressed) |
Limits:
- Maximum payload size: 1,677,721,600 bytes (~1.6 GB)
- Minimum frame size: 8 bytes (header only, empty payload)
Compression
Section titled “Compression”Large packets can be automatically compressed using Zstd (Zstandard). Whether a packet type supports compression is defined in the packet registry.
Compressed packets work as follows:
- Serialize packet payload to buffer
- Compress with Zstd at configurable level (default: Zstd’s default level)
- Write compressed size as the “Payload Length” in frame header
- Receiver decompresses based on packet registry metadata
The compression level can be configured via system property:
-Dhytale.protocol.compressionLevel=3Variable-Length Integers (VarInt)
Section titled “Variable-Length Integers (VarInt)”Strings and arrays are prefixed with their length encoded as a VarInt. This encoding uses 7 bits per byte for data, with the high bit indicating continuation:
| Value Range | Bytes Used |
|---|---|
| 0 - 127 | 1 |
| 128 - 16,383 | 2 |
| 16,384 - 2,097,151 | 3 |
| 2,097,152 - 268,435,455 | 4 |
| 268,435,456+ | 5 |
Encoding example:
Value 300 = 0b100101100Encoded as: [0xAC, 0x02] - Byte 1: 0xAC = 0b10101100 (0b0101100 | 0x80 continuation flag) - Byte 2: 0x02 = 0b00000010 (no continuation) - Result: (0x02 << 7) | 0x2C = 256 + 44 = 300Important: VarInt only encodes non-negative values. Negative values throw a ProtocolException.
Data Types
Section titled “Data Types”Primitive Types
Section titled “Primitive Types”All multi-byte primitives use little-endian byte order.
| Type | Size | Description |
|---|---|---|
byte | 1 | Signed 8-bit integer |
short | 2 | Signed 16-bit integer (LE) |
int | 4 | Signed 32-bit integer (LE) |
long | 8 | Signed 64-bit integer (LE) |
float | 4 | 32-bit IEEE 754 float (LE) |
half | 2 | 16-bit half-precision float (LE) |
Strings
Section titled “Strings”Strings come in two variants:
Variable-length string (VarString):
┌──────────────────┬──────────────────────────┐│ Length (VarInt) │ UTF-8/ASCII bytes │└──────────────────┴──────────────────────────┘Fixed-length string:
┌──────────────────────────────────────────────┐│ Bytes (N fixed) │ Null-padded if shorter │└──────────────────────────────────────────────┘Maximum string lengths are defined per-field. For example:
- Username: max 16 bytes (ASCII)
- Identity token: max 8,192 bytes (UTF-8)
- Protocol hash: 64 bytes fixed (ASCII)
UUIDs are stored as two 64-bit values (big-endian within the UUID itself):
┌──────────────────────────┬──────────────────────────┐│ Most Significant (8B) │ Least Significant (8B) │└──────────────────────────┴──────────────────────────┘Arrays
Section titled “Arrays”Arrays are prefixed with their count as a VarInt:
┌──────────────────┬──────────────────────────────────┐│ Count (VarInt) │ Elements (count × element size) │└──────────────────┴──────────────────────────────────┘Nullable Fields
Section titled “Nullable Fields”Packets use a null bit field at the start to track which optional fields are present:
┌────────────────────┬───────────────────────────────────────┐│ Null Bits (1-N B) │ Fixed fields, then variable fields │└────────────────────┴───────────────────────────────────────┘Each bit corresponds to an optional field:
- Bit = 1: Field is present
- Bit = 0: Field is null/absent
Packet Layout
Section titled “Packet Layout”Packets are structured with fixed-size fields first, followed by variable-size fields:
┌─────────────────────────────────────────────────────────────┐│ Null Bits (N bytes) │├─────────────────────────────────────────────────────────────┤│ Fixed-Size Block ││ (primitives, enums, inline structs) │├─────────────────────────────────────────────────────────────┤│ Offset Table ││ (4-byte LE offsets to variable fields) │├─────────────────────────────────────────────────────────────┤│ Variable Block ││ (strings, arrays, nested objects) │└─────────────────────────────────────────────────────────────┘Variable fields are accessed by offset table entries that point into the variable block relative to its start.
Example: Connect Packet
Section titled “Example: Connect Packet”The Connect packet (ID 0) demonstrates the full structure:
public class Connect implements Packet { public String protocolHash; // fixed 64 bytes ASCII public ClientType clientType; // 1 byte enum public String language; // nullable VarString (max 128) public String identityToken; // nullable VarString (max 8192) public UUID uuid; // 16 bytes public String username; // varString (max 16) public byte[] referralData; // nullable byte array (max 4096) public HostAddress referralSource; // nullable nested object}Wire format:
Offset Size Field────── ──── ─────0 1 Null bits (4 nullable fields)1 64 protocolHash (fixed ASCII)65 1 clientType (enum byte)66 16 uuid82 4 language offset (into variable block)86 4 identityToken offset90 4 username offset94 4 referralData offset98 4 referralSource offset102 var Variable block start ... (actual string/array data)Validation
Section titled “Validation”Before deserializing a packet, the protocol validates its structure:
- Buffer size meets minimum requirements
- String/array lengths don’t exceed maximums
- Offsets point within valid ranges
- Nested objects validate recursively
Invalid packets cause the connection to be closed with no response.
Packet Registry
Section titled “Packet Registry”Each packet type is registered with:
- ID: Unique 32-bit identifier
- Name: Human-readable name for debugging
- Fixed block size: Minimum size of non-variable fields
- Max size: Maximum total packet size
- Compressed: Whether to use Zstd compression
- Validator: Structure validation function
- Deserializer: Function to parse bytes into object
See Packet Categories for the full ID range allocation.