💾 Archived View for thingvellir.net › classic-protocol.gmi captured on 2024-05-12 at 15:13:53. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-04-19)
-=-=-=-=-=-=-
The protocol for Minecraft Classic is a little messy, though far less of a mess than the multiplayer protocols for all of the versions after Classic 0.30.
A CC0-licensed Classic protocol server written in Python.
The community has designed numerous extensions for things like defining custom block types, custom entity models, client messages longer than 64 bytes, and more.
All integers are big-endian.
A signed 11-5 fixed point format is used for entity positions. Dividing these by 32 gives you the true fractional position.
Unsigned bytes are used for player angles. When scaled, 0–255 equals 0-2π.
Strings are space-padded 64-byte long ASCII or IBM CP437 byte strings.
The client sends a handshake packet immediately after connecting. The server immediately responds back with a handshake packet.
The client-to-server handshake packet's `passkey` field may be a plaintext password or a special key generated by the server browser to ensure the player is connecting with a legitimate username.
struct client_handshake { opcode: u8 = 0x00, version: u8 = 0x07, username: [64]u8, passkey: [64]u8, cpe_support: u8, // 0x42 in clients with CPE support, 0 otherwise }
struct server_handshake { opcode: u8 = 0x00, version: u8 = 0x07, title: [64]u8, motd: [64]u8, // Used by some clients for cheat settings operator: u8, // 0x64 if the connecting client // is an operator, 0x00 otherwise }
The server occasionally sends this packet to each connected client to make sure the client-server connection is still open.
struct ping { opcode: u8 = 0x01, }
When recieved, the client unloads the current level and prepares for incoming level data.
struct level_change { opcode: u8 = 0x02, }
The sent level data is a one-dimensional unsigned byte array of length `(x size * y size * z size)`.
The level data is prefixed with a big-endian signed 32-bit integer representing the total byte size of the map, then GZip compressed and sent in pieces.
Each Level Data packet sends a 1024-byte long piece of the compressed data, with pieces smaller than 1024 bytes being padded with 0x00 bytes.
struct level_data { opcode: u8 = 0x03, used_len: i16, // How many of the 1024 bytes are used data: [1024]i8, unused: u8, }
Sent after the final Level Data packet, the server informs the client of the dimensions of the map.
struct level_size { opcode: u8 = 0x04, x_size: i16, y_size: i16, z_size: i16, }
Sent by the client when placing or deleting a block. The client assumes its block updates are valid if it does not recieve a response 06 Server Block Update at the same block coordinate.
struct client_block { opcode: u8 = 0x05, x: i16, y: i16, z: i16, place: u8, // 1 if placing a block, 0 if removing block_id: u8, // Always whatever block the client is holding, // even when deleting blocks }
Sent by the server to all clients when a block is updated.
struct server_block { opcode: u8 = 0x06, x: i16, y: i16, z: i16, block_id: u8, }
Sent from the server to all clients to add a player's visible model to the world.
If the player ID field is set to 0xFF, it indicates the recieving client is allowed to switch from a loading screen to the playable game state.
It is unknown if player ID 0xFF is a special case or if any number above 0x7F will trigger the same behavior.
If sent with player ID 0xFF to a client that is already playing, this acts like 08 Player Move and teleports the player.
struct create_player { opcode: u8 = 0x07, eid: i8, name: [64]u8, // The name shown on the player's model // and the player list, not required to // be the player's login username x: i16, y: i16, z: i16, yaw: u8, pitch: u8, }
Sent by the client every 1/20th of a second.
Sent by the server to move the player models each client sees, or if sent with a player ID of 0xFF, the client is teleported.
struct player_move { opcode: u8 = 0x08, eid: i8, // Unused on the client, used by some clients to // represent the current block ID the player is holding x: i16, y: i16, z: i16, yaw: u8, pitch: u8, }
These packets are not required for client or server operation. They should only be implemented if you are building a client targeting compatibility with servers that send these packets. The client can only ever send 08 Player Move.
A player will be moving and rotating at the same time often enough to render these practically useless for saving bytes.
struct relative_move_rot { opcode: u8 = 0x09, eid: i8, xdelta: i8, //! signed 3-5 fixed point ydelta: i8, zdelta: i8, yaw: u8, //! sets the rotation, is not a delta update pitch: u8, }
struct relative_move { opcode: u8 = 0x0a, eid: i8, xdelta: i8, //! signed 3-5 fixed point ydelta: i8, zdelta: i8, }
struct relative_rot { opcode: u8 = 0x0b, eid: i8, yaw: u8, //! absolute angles, not a delta pitch: u8, }
The server sends this packet to all clients to remove a player model from the world, typically because someone disconnected.
struct remove_player { opcode: u8 = 0x0c, eid: i8, }
Sent by the client to send a chat message or command to the server.
Sent by the server to one or all clients to add a line of text to the client's chat area.
The player ID field is unused when the client sends this packet. Some clients use this field to mark if a message has more parts to recieve.
struct chat_message { opcode: u8 = 0x0d, eid: i8, data: [64]u8, }
Sent by the server to forcefully disconnect a client with a message.
struct kick { opcode: u8 = 0x0e, message: [64]u8, }