classic-node-protocol
v3.0.0
Published
Minecraft Classic v7 / ClassiCube protocol library for Node.js — Full client & server support, CPE, zero dependencies.
Downloads
1,700
Maintainers
Readme
classic-node-protocol
Minecraft Classic v7 / ClassiCube protocol library for Node.js
Full client & server support · CPE (Classic Protocol Extension) · Auth · Zero dependencies
npm install classic-node-protocolRequires Node.js ≥ 18 · v3.0.0
Table of Contents
- Quick Start
- Architecture Overview
- createServer & ClassiCubeServer
- ClientConnection
- createClient & ClassiCubeClient
- PacketDecoder
- encoder – Low-Level Packet Builders
- codec – Generic Encode/Decode Engine
- protocol – Constants
- level – Map Generation & Transmission
- auth – Authentication & Heartbeat
- cpe – Classic Protocol Extensions
- jugadorUUID – UUID Utilities
- TypeScript Support
- Protocol Reference
1. Quick Start
Minimal server
const { createServer, level, BLOCKS } = require('classic-node-protocol');
const srv = createServer({ port: 25565, pingInterval: 2000 });
srv.on('connection', async (client) => {
// Greet and send a flat map
client.sendIdentification('My Server', 'Welcome!');
const blocks = level.buildFlatMap(64, 64, 64);
await client.sendLevel(blocks, 64, 64, 64);
client.spawnAt(-1, 'You', 32, 33, 32); // spawn self at center
client.on('packet', (p) => {
if (p.name === 'message') {
srv.broadcastMessage(`<${client.username}> ${p.message}`);
}
});
});
srv.on('listening', ({ port }) => console.log(`Listening on :${port}`));Minimal bot (client)
const { createClient } = require('classic-node-protocol');
const bot = createClient({ host: 'localhost', port: 25565 });
bot.on('connect', () => {
bot.sendIdentification('MyBot');
});
bot.on('level', ({ blocks, xSize, ySize, zSize }) => {
console.log(`Map received: ${xSize}×${ySize}×${zSize}`);
bot.sendMessage('Hello from the bot!');
});
bot.on('packet', (p) => {
if (p.name === 'message') console.log('[Chat]', p.message);
});2. Architecture Overview
classic-node-protocol
│
├── createServer() → ClassiCubeServer
│ └── emits ClientConnection per player
│
├── createClient() → ClassiCubeClient
│ └── auto-assembles level, emits 'level' event
│
├── PacketDecoder → streaming parser (EventEmitter, emits 'packet')
│
├── encoder → pure functions → Buffer (no I/O)
│ └── thin façade over codec.js
│
├── codec → generic encode/decode engine ← NEW in v3
│ └── driven by protocol.json
│
├── types → primitive type handlers (u8, i16, string, …) ← NEW in v3
│
├── protocol.json → single source of truth for ALL packet schemas ← NEW in v3
│ (Classic v7 + full CPE)
│
├── protocol → JS constants: packet IDs, sizes, BLOCKS, CPE_EXTENSIONS…
│
├── level → map builders + gzip compression pipeline
│
├── auth → ClassiCubeAuth (server) + ClassiCubeAccount (bot login)
│
├── cpe → all 24+ CPE extension encoders/decoders
│ └── thin façade over codec.js
│
└── jugadorUUID → deterministic UUID generation from usernameEverything is CommonJS (require()), zero runtime dependencies, Node ≥ 18.
v3 internals
In v3 all serialization logic lives in protocol.json (packet field schemas) and lib/codec.js (generic read/write engine). encoder.js and cpe.js are now thin wrappers that map positional arguments to codec.encode() / codec.decode() calls — the public API is fully backward-compatible.
To add a new packet you only need to edit protocol.json:
// protocol.json → "server" → "toClient"
"myNewPacket": {
"id": 99,
"fields": [
{ "name": "entityId", "type": "u8" },
{ "name": "label", "type": "string" },
{ "name": "value", "type": "i32" }
]
}Then use it immediately:
const { codec } = require('classic-node-protocol');
const buf = codec.encode('server', 'toClient', 'myNewPacket', {
entityId: 3,
label: 'score',
value: 1000,
});
const pkt = codec.decode('server', 'toClient', 99, buf);
// → { name: 'myNewPacket', id: 99, entityId: 3, label: 'score', value: 1000 }Available primitive types: u8, i8, u16, i16, i32, string (64-byte padded ASCII), bytes (fixed-length raw), u8array (fixed-count), i32array (fixed-count).
3. createServer & ClassiCubeServer
Factory
const { createServer } = require('classic-node-protocol');
const server = createServer({
port: 25565, // if provided with autoListen !== false, starts listening immediately
host: '0.0.0.0',
maxClients: 20, // 0 = unlimited (default)
pingInterval: 2000, // ms between automatic pings to ALL clients (0 = off)
});Manual instantiation
const { ClassiCubeServer } = require('classic-node-protocol');
const server = new ClassiCubeServer({ maxClients: 10, pingInterval: 5000 });
await server.listen(25565, '0.0.0.0');ClassiCubeServer events
| Event | Callback signature | Description |
|---|---|---|
| 'listening' | ({ port, host }) | Server is accepting connections |
| 'connection' | (client: ClientConnection) | New TCP connection |
| 'disconnect' | (client: ClientConnection) | A client disconnected cleanly |
| 'clientError' | (client, err) | A client-level socket error |
| 'error' | (err) | Server-level error (e.g. EADDRINUSE) |
ClassiCubeServer properties & methods
server.clients // Map<number, ClientConnection> — all live connections
server.playerCount // number — shorthand for server.clients.size
await server.listen(port, host) // start accepting connections
await server.close() // stop accepting; existing sockets stay open
await server.shutdown(reason) // disconnect everyone then close
server.broadcast(buf) // send raw Buffer to every client
server.broadcastMessage(msg, senderId) // broadcast chat (senderId default 0xFF = server)
server.broadcastExcept(excludeId, buf) // broadcast to everyone except one client id
server.relayMessage(fromId, message) // relay a player's chat to everyone else
server.pingAll() // send ping packet to all clientsExample: complete multiplayer relay
const { createServer, level } = require('classic-node-protocol');
const { encoder } = require('classic-node-protocol');
const srv = createServer({ port: 25565, pingInterval: 3000 });
const world = level.buildFlatMap(128, 64, 128);
srv.on('connection', async (client) => {
// 1. Handshake
client.sendIdentification('Demo Server', 'Have fun!');
// 2. Wait for player identification
await new Promise((resolve) => {
client.once('packet', (p) => {
if (p.name === 'identification') {
client.username = p.username;
resolve();
}
});
});
// 3. Send map
await client.sendLevel(world, 128, 64, 128);
// 4. Spawn self + all existing players
client.spawnAt(-1, client.username, 64, 33, 64);
for (const [id, other] of srv.clients) {
if (id === client.id) continue;
client.spawnAt(other.id, other.username, 64, 33, 64);
other.spawnAt(client.id, client.username, 64, 33, 64);
}
// 5. Handle packets
client.on('packet', (p) => {
switch (p.name) {
case 'message':
srv.broadcastMessage(`<${client.username}> ${p.message}`);
break;
case 'setBlock':
world[level.blockIndex(p.x, p.z, p.y, 128, 128)] = p.mode === 1 ? p.blockType : 0;
srv.broadcast(encoder.encodeServerSetBlock(p.x, p.y, p.z,
p.mode === 1 ? p.blockType : 0));
break;
case 'position':
srv.broadcastExcept(client.id,
encoder.encodePosition(client.id, p.x, p.y, p.z, p.yaw, p.pitch));
break;
}
});
});
srv.on('disconnect', (client) => {
srv.broadcastMessage(`${client.username} left the game`);
srv.broadcast(encoder.encodeDespawnPlayer(client.id));
});4. ClientConnection
Every player that connects is represented by a ClientConnection instance. You receive it in the 'connection' event.
Properties
client.id // number — unique session ID (assigned by server)
client.socket // net.Socket
client.username // string | null — set it yourself after identification
client.state // 'login' | 'level' | 'play'
client.data // {} — free-form storage for your app (e.g. health, position)
client.remoteAddress // string — client IP
client.remotePort // number
client.isConnected // boolean
// CPE fields (populated during CPE negotiation)
client.supportsCpe // boolean — did client send unused=0x42 in identification?
client.cpeReady // boolean — CPE negotiation fully complete
client.extensions // Set<string> — extension names both sides agreed onSending packets (server → client)
// ── Core ──────────────────────────────────────────────────────────────────────
client.sendIdentification(serverName, motd, userType) // 0x00
client.sendPing() // 0x01
client.sendLevelInitialize() // 0x02 (sets state='level')
client.sendLevelDataChunk(chunkBuf, percentComplete) // 0x03
client.sendLevelFinalize(xSize, ySize, zSize) // 0x04 (sets state='play')
client.sendSetBlock(x, y, z, blockType) // 0x06
client.sendSpawnPlayer(id, name, x, y, z, yaw, pitch) // 0x07 (FShort coords)
client.spawnAt(id, name, bx, by, bz, yaw, pitch) // 0x07 (block coords, auto-converts)
client.sendPosition(id, x, y, z, yaw, pitch) // 0x08 teleport (FShort)
client.sendPositionOrientation(id, dx, dy, dz, yaw, pitch) // 0x09 relative+rotation
client.sendPositionUpdate(id, dx, dy, dz) // 0x0A relative only
client.sendOrientationUpdate(id, yaw, pitch) // 0x0B rotation only
client.sendDespawnPlayer(playerId) // 0x0C
client.sendMessage(senderId, message) // 0x0D any sender
client.sendServerMessage(message) // 0x0D sender = 0xFF (server)
client.disconnect(reason) // 0x0E + socket.destroy()
client.sendUpdateUserType(userType) // 0x0F
client.setOperator(isOp) // 0x0F convenience wrapper
// ── Level helper (all-in-one) ─────────────────────────────────────────────────
await client.sendLevel(blocks, xSize, ySize, zSize, zlibOpts)
// Sends: LevelInitialize → N×LevelDataChunk → LevelFinalize
// zlibOpts: e.g. { level: 9 } for maximum compression
// ── CPE (see section 11) ──────────────────────────────────────────────────────
client.sendCpeHandshake(appName, extensions)
client.sendExtInfo(appName, count)
client.sendExtEntry(extName, version)
client.sendSetClickDistance(distance)
client.sendCustomBlockSupportLevel(level)
client.sendHoldThis(blockToHold, preventChange)
client.sendSetTextHotKey(label, action, keyCode, keyMods)
client.sendExtAddPlayerName(nameId, playerName, listName, groupName, groupRank)
client.sendExtAddEntity2(entityId, inGameName, skinName, x, y, z, yaw, pitch)
client.sendExtRemovePlayerName(nameId)
client.sendEnvSetColor(variable, r, g, b)
client.sendMakeSelection(id, label, x1,y1,z1, x2,y2,z2, r,g,b,a)
client.sendRemoveSelection(selectionId)
client.sendSetBlockPermission(blockType, allowPlace, allowDestroy)
client.sendChangeModel(entityId, modelName)
client.sendSetMapEnvAppearance(textureUrl, sideBlock, edgeBlock, sideLevel)
client.sendEnvSetWeatherType(weatherType)
client.sendHackControl(flying, noClip, speeding, spawnControl, thirdPersonView, jumpHeight)
client.sendDefineBlock(def)
client.sendDefineBlockExt(def)
client.sendRemoveBlockDefinition(blockId)
client.sendBulkBlockUpdate(updates)
client.sendSetTextColor(code, r, g, b, a)
client.sendSetMapEnvUrl(textureUrl)
client.sendSetMapEnvProperty(property, value)
client.sendSetEntityProperty(entityId, propertyType, value)
client.sendTwoWayPing(direction, data)
client.sendSetInventoryOrder(order, blockType)Receiving packets
client.on('packet', (packet) => {
// packet.name — string key, e.g. 'identification', 'setBlock', 'position', 'message'
// packet.id — raw packet byte ID
console.log(packet);
});
client.on('end', () => console.log('client disconnected'));
client.on('error', (err) => console.error('client error:', err));Low-level write
const { encoder } = require('classic-node-protocol');
client.writeBuffer(encoder.encodePing()); // send any raw Buffer5. createClient & ClassiCubeClient
Use this to build bots or terminal clients that connect to any ClassiCube-compatible server.
Factory
const { createClient } = require('classic-node-protocol');
// Auto-connects immediately when host+port are provided:
const bot = createClient({
host: 'example.com',
port: 25565,
connectTimeout: 10000, // ms before giving up (default 10 s)
pingInterval: 0, // ms between client-side pings (0 = off)
});Manual instantiation + connect
const { ClassiCubeClient } = require('classic-node-protocol');
const client = new ClassiCubeClient({ connectTimeout: 5000 });
await client.connect('localhost', 25565);
client.sendIdentification('MyBot', '-');ClassiCubeClient events
| Event | Callback | Description |
|---|---|---|
| 'connect' | () | TCP handshake complete |
| 'end' | () | Connection closed |
| 'error' | (err) | Socket or decode error |
| 'packet' | (packet) | Any decoded server packet |
| 'level' | ({ blocks, xSize, ySize, zSize }) | Full map assembled & decompressed |
| 'levelProgress' | (percent: number) | Map download progress 0–100 |
| 'ping' | () | Ping interval tick (if pingInterval > 0) |
Sending packets (client → server)
// Core protocol
bot.sendIdentification(username, verificationKey, unused)
// verificationKey: use '-' for offline servers, or real MPPass for online mode
// unused: 0x00 for vanilla, 0x42 (CPE_MAGIC) to signal CPE support
bot.sendSetBlock(x, y, z, mode, blockType)
// mode: 0 = destroy, 1 = create
bot.sendPosition(x, y, z, yaw, pitch) // FShort coordinates
bot.sendPositionBlocks(bx, by, bz, yaw, pitch) // block coordinates (auto-converts)
bot.sendMessage(message)
// CPE
bot.sendCpeIdentification(username, verificationKey, extensions)
bot.sendExtInfo(appName, extensionCount)
bot.sendExtEntry(extName, version)
bot.sendCustomBlockSupportLevel(level)
bot.sendPlayerClicked(button, action, yaw, pitch, targetId, targetX, targetY, targetZ, targetFace)
bot.sendTwoWayPing(direction, data)
bot.disconnect() // destroy socket
bot.writeBuffer(buf) // raw sendFull bot example (online mode with auth)
const { ClassiCubeClient, ClassiCubeAccount } = require('classic-node-protocol');
async function main() {
const account = new ClassiCubeAccount();
await account.login('BotUsername', 'BotPassword');
const servers = await account.getServers();
const target = servers.find(s => s.name.includes('Survival'));
const bot = new ClassiCubeClient({ connectTimeout: 8000 });
await bot.connect(target.ip, target.port);
bot.sendIdentification(account.username, target.mppass);
bot.on('level', ({ xSize, ySize, zSize }) => {
console.log(`Joined! Map: ${xSize}×${ySize}×${zSize}`);
bot.sendMessage('Hello everyone!');
});
bot.on('packet', (p) => {
if (p.name === 'message') console.log('[Chat]', p.message);
if (p.name === 'disconnect') console.log('[Kick]', p.reason);
});
}
main().catch(console.error);6. PacketDecoder
The streaming packet parser used internally by both ClassiCubeClient and ClientConnection. You can also use it standalone for custom transports.
const { PacketDecoder } = require('classic-node-protocol');
// direction 'client' → you're the server, parsing packets FROM a client
// direction 'server' → you're the client, parsing packets FROM the server
const decoder = new PacketDecoder('server');
decoder.on('packet', (packet) => {
console.log(packet.name, packet);
});
decoder.on('error', (err) => {
console.error('Decode error:', err.message);
});
// Feed raw TCP data — handles fragmentation and coalescing automatically
socket.on('data', (data) => decoder.receive(data));0x1D disambiguation (ExtAddEntity2 vs ChangeModel)
Both packets share ID 0x1D but have different sizes (138 vs 66 bytes). The decoder resolves this automatically by matching the buffer length — no manual configuration needed.
7. encoder – Low-Level Packet Builders
Every function returns a Buffer ready to write to a socket. No side effects.
const { encoder } = require('classic-node-protocol');
// or
const enc = require('classic-node-protocol/encoder');Coordinate helpers
encoder.toFShort(blockPos) // block float → FShort integer (×32)
encoder.fromFShort(fshort) // FShort → block float (÷32)
// Example: block position 5.5 → FShort 176String helpers
encoder.writeString(buf, offset, str) // write 64-byte ASCII string into buf
encoder.readString(buf, offset) // read 64-byte ASCII string, trim trailing spacesServer → Client encoders
encoder.encodeServerIdentification(serverName, motd, userType)
// → 131-byte Buffer
encoder.encodePing()
// → 1-byte Buffer
encoder.encodeLevelInitialize()
// → 1-byte Buffer
encoder.encodeLevelDataChunk(chunkData, percentComplete)
// chunkData: Buffer ≤ 1024 bytes of gzipped map data
// → 1028-byte Buffer
encoder.encodeLevelFinalize(xSize, ySize, zSize)
// → 7-byte Buffer
encoder.encodeServerSetBlock(x, y, z, blockType)
// → 8-byte Buffer
encoder.encodeSpawnPlayer(playerId, playerName, x, y, z, yaw, pitch)
// x/y/z: FShort coordinates
// playerId: -1 (0xFF) = spawn as self
// → 74-byte Buffer
encoder.encodePosition(playerId, x, y, z, yaw, pitch)
// Absolute teleport — FShort coordinates
// → 10-byte Buffer
encoder.encodePositionOrientation(playerId, dx, dy, dz, yaw, pitch)
// Relative position (signed byte deltas) + absolute rotation
// → 7-byte Buffer
encoder.encodePositionUpdate(playerId, dx, dy, dz)
// Relative position only
// → 5-byte Buffer
encoder.encodeOrientationUpdate(playerId, yaw, pitch)
// → 4-byte Buffer
encoder.encodeDespawnPlayer(playerId)
// → 2-byte Buffer
encoder.encodeServerMessage(playerId, message)
// playerId: 0xFF = server message
// → 66-byte Buffer
encoder.encodeDisconnect(reason)
// → 65-byte Buffer
encoder.encodeUpdateUserType(userType)
// userType: 0x00 = normal, 0x64 = operator
// → 2-byte BufferClient → Server encoders
encoder.encodeClientIdentification(username, verificationKey, unused)
// unused: 0x00 = vanilla, 0x42 = CPE
// → 131-byte Buffer
encoder.encodeClientSetBlock(x, y, z, mode, blockType)
// mode: 0=destroy 1=create
// → 9-byte Buffer
encoder.encodeClientPosition(x, y, z, yaw, pitch)
// Player ID is auto-set to 0xFF (self)
// → 10-byte Buffer
encoder.encodeClientMessage(message)
// → 66-byte Buffer8. codec – Generic Encode/Decode Engine
The codec is the v3 engine that powers all serialization. You can use it directly to work with packets by name, without manual Buffer arithmetic.
const { codec } = require('classic-node-protocol');codec.encode(section, direction, name, data)
// section: 'server' | 'client' | 'cpe'
// direction: 'toClient' | 'toServer' | 'server' | 'client'
// name: packet name matching protocol.json
// data: field values object
const buf = codec.encode('server', 'toClient', 'spawnPlayer', {
playerId: 1,
playerName: 'Player1',
x: 320, y: 320, z: 320,
yaw: 128, pitch: 0,
});
// → 74-byte Buffer, identical to encoder.encodeSpawnPlayer(...)codec.decode(section, direction, id, buf)
const pkt = codec.decode('server', 'toClient', 0x08, buf);
// → { name: 'position', id: 8, playerId: 1, x: 320, y: 320, z: 320, yaw: 128, pitch: 0 }codec.decodeByName(section, direction, name, buf)
const pkt = codec.decodeByName('cpe', 'server', 'hackControl', buf);
// → { name: 'hackControl', id: 32, flying: 1, noClip: 0, ... }Query helpers
codec.packetSize('server', 'toClient', 0x03) // → 1028 (levelDataChunk)
codec.packetId('cpe', 'server', 'hackControl') // → 32
codec.sizeTable('server', 'toClient') // → { 0: 131, 1: 1, 2: 1, ... }codec.schema
Direct access to the parsed protocol.json object. Useful for tooling, validation, or generating documentation.
const { codec } = require('classic-node-protocol');
const spawnDef = codec.schema.server.toClient.spawnPlayer;
// → { id: 7, fields: [ { name: 'playerId', type: 'i8' }, ... ] }9. protocol – Constants
const { protocol, BLOCKS, BLOCK_NAMES, BLOCK_MODE, USER_TYPE, CPE_MAGIC,
CPE_EXTENSIONS, CPE_PACKETS } = require('classic-node-protocol');
// or
const protocol = require('classic-node-protocol/lib/protocol');Packet ID tables
protocol.CLIENT_PACKETS // { IDENTIFICATION: 0x00, SET_BLOCK: 0x05, ... }
protocol.SERVER_PACKETS // { IDENTIFICATION: 0x00, PING: 0x01, ... }
protocol.CLIENT_PACKET_SIZES // { 0x00: 131, 0x05: 9, 0x08: 10, 0x0D: 66 }
protocol.SERVER_PACKET_SIZES // { 0x00: 131, 0x01: 1, 0x02: 1, ... }
protocol.CPE_PACKETS // { EXT_INFO: 0x10, EXT_ENTRY: 0x11, ... }
protocol.CPE_SERVER_PACKET_SIZES
protocol.CPE_CLIENT_PACKET_SIZES
protocol.STRING_LENGTH // 64
protocol.PROTOCOL_VERSION // 7 (0x07)Block type constants
All 50 Classic blocks (IDs 0–49) are exposed as named constants:
BLOCKS.AIR // 0
BLOCKS.STONE // 1
BLOCKS.GRASS // 2
BLOCKS.DIRT // 3
BLOCKS.COBBLESTONE // 4
BLOCKS.WOOD_PLANKS // 5
BLOCKS.SAPLING // 6
BLOCKS.BEDROCK // 7
BLOCKS.WATER_FLOWING // 8
BLOCKS.WATER // 9
BLOCKS.LAVA_FLOWING // 10
BLOCKS.LAVA // 11
BLOCKS.SAND // 12
BLOCKS.GRAVEL // 13
BLOCKS.GOLD_ORE // 14
BLOCKS.IRON_ORE // 15
BLOCKS.COAL_ORE // 16
BLOCKS.LOG // 17
BLOCKS.LEAVES // 18
BLOCKS.SPONGE // 19
BLOCKS.GLASS // 20
BLOCKS.RED_CLOTH // 21
BLOCKS.ORANGE_CLOTH // 22
BLOCKS.YELLOW_CLOTH // 23
BLOCKS.LIME_CLOTH // 24
BLOCKS.GREEN_CLOTH // 25
BLOCKS.TEAL_CLOTH // 26
BLOCKS.AQUA_CLOTH // 27
BLOCKS.CYAN_CLOTH // 28
BLOCKS.BLUE_CLOTH // 29
BLOCKS.INDIGO_CLOTH // 30
BLOCKS.VIOLET_CLOTH // 31
BLOCKS.MAGENTA_CLOTH // 32
BLOCKS.PINK_CLOTH // 33
BLOCKS.BLACK_CLOTH // 34
BLOCKS.GRAY_CLOTH // 35
BLOCKS.WHITE_CLOTH // 36
BLOCKS.DANDELION // 37
BLOCKS.ROSE // 38
BLOCKS.BROWN_MUSHROOM // 39
BLOCKS.RED_MUSHROOM // 40
BLOCKS.GOLD_BLOCK // 41
BLOCKS.IRON_BLOCK // 42
BLOCKS.DOUBLE_SLAB // 43
BLOCKS.SLAB // 44
BLOCKS.BRICK // 45
BLOCKS.TNT // 46
BLOCKS.BOOKSHELF // 47
BLOCKS.MOSSY_COBBLE // 48
BLOCKS.OBSIDIAN // 49Reverse lookup (ID → name):
BLOCK_NAMES[2] // → 'GRASS'
BLOCK_NAMES[49] // → 'OBSIDIAN'Other constants
BLOCK_MODE.DESTROY // 0x00 — used in SetBlock
BLOCK_MODE.CREATE // 0x01
USER_TYPE.NORMAL // 0x00
USER_TYPE.OP // 0x64
CPE_MAGIC // 0x42 — send as `unused` in client identification to signal CPECPE extension registry
All 26 known extensions with their canonical version numbers:
CPE_EXTENSIONS = {
ClickDistance: 1,
CustomBlocks: 1,
HeldBlock: 1,
EmoteFix: 1,
TextHotKey: 1,
ExtPlayerList: 2,
EnvColors: 1,
SelectionCuboid: 1,
BlockPermissions: 1,
ChangeModel: 1,
EnvMapAppearance: 2,
EnvWeatherType: 1,
HackControl: 1,
MessageTypes: 1,
PlayerClick: 1,
LongerMessages: 1,
FullCP437: 1,
BlockDefinitions: 1,
BlockDefinitionsExt: 2,
BulkBlockUpdate: 1,
TextColors: 1,
EnvMapAspect: 1,
EntityProperty: 1,
ExtEntityPositions: 1,
TwoWayPing: 1,
InventoryOrder: 1,
}10. level – Map Generation & Transmission
const { level } = require('classic-node-protocol');
// or
const level = require('classic-node-protocol/level');Map coordinate formula
Classic uses x + z×xSize + y×xSize×zSize (X fastest, Y slowest = Y is up).
level.blockIndex(x, z, y, xSize, zSize)
// returns the buffer index for block at (x, y, z)
// Example:
const idx = level.blockIndex(10, 5, 20, 64, 64);
world[idx] = BLOCKS.STONE;Built-in map generators
buildFlatMap — classic superflat
const blocks = level.buildFlatMap(
xSize, // width
ySize, // height
zSize, // depth
groundY // optional: Y of grass surface (default: Math.floor(ySize / 2))
);
// Layout: bedrock at y=0, dirt below groundY-1, grass at groundY-1, air abovebuildSphereMap — hollow sphere
const blocks = level.buildSphereMap(
xSize, ySize, zSize,
radius, // optional: outer radius (default: min(dim)/2 - 2)
shell, // optional: shell thickness in blocks (default: 2)
blockType // optional: block for the shell (default: BLOCKS.STONE)
);
// Good for spaceship / planet demosbuildCheckerMap — checkerboard testing floor
const blocks = level.buildCheckerMap(xSize, ySize, zSize);
// y=0: alternating WHITE_CLOTH and BLACK_CLOTH
// y=ySize-1: solid STONE ceiling
// Everything else: AIRbuildMap — custom generator
const blocks = level.buildMap(
xSize, ySize, zSize,
(x, y, z) => {
// return block type for this position, 0 = air
if (y === 0) return BLOCKS.BEDROCK;
if (y < 10) return BLOCKS.DIRT;
return BLOCKS.AIR;
}
);Level compression pipeline (server side)
// Step 1: compress + prepend 4-byte header
const gzipped = await level.compressLevel(blocks, { level: 6 });
// Step 2: split into ≤1024-byte chunks
const chunks = level.chunkLevel(gzipped);
// chunks = [{ chunk: Buffer, percent: number }, ...]
// Or combine both steps:
const chunks = await level.prepareLevel(blocks, { level: 6 });Sending manually (what client.sendLevel() does internally):
client.sendLevelInitialize();
const chunks = await level.prepareLevel(blocks);
for (const { chunk, percent } of chunks) {
client.sendLevelDataChunk(chunk, percent);
}
client.sendLevelFinalize(xSize, ySize, zSize);Level reassembly (client side)
ClassiCubeClient does this automatically and emits 'level'. If you need it manually:
const assembler = new level.LevelAssembler();
// on 'levelInitialize':
assembler.reset();
// on each 'levelDataChunk':
assembler.push(p.chunkData, p.chunkLength);
console.log(`Progress: ${assembler.byteLength} bytes buffered`);
// on 'levelFinalize':
const rawBlocks = await assembler.decompress();
// rawBlocks is the flat Buffer of block IDsRaw zlib helpers
const gzipBuf = await level.gzip(inputBuffer, { level: 9 });
const rawBuf = await level.gunzip(gzipBuf);11. auth – Authentication & Heartbeat
const {
ClassiCubeAuth,
ClassiCubeAccount,
generateSalt,
computeMPPass,
verifyMPPass,
} = require('classic-node-protocol');Standalone MPPass helpers
These work without any class instance.
// Generate a random 16-char alphanumeric salt
const salt = generateSalt(); // e.g. 'aB3xQzLm9rTpWkYn'
const salt = generateSalt(32); // longer if you want
// Compute the expected MPPass: MD5(salt + username) as lowercase hex
const expected = computeMPPass(salt, 'PlayerName');
// Verify a player's submission (case-insensitive comparison)
const ok = verifyMPPass(salt, 'PlayerName', playerMppass);
// → true or falseClassiCubeAuth — server-side auth & heartbeat
const auth = new ClassiCubeAuth({
name: 'My ClassiCube Server', // required — shown in server list
port: 25565, // required
maxPlayers: 20, // default: 20
public: true, // default: true — show in public list
software: 'classic-node-protocol', // optional
web: false, // set true if accessible via CC web client
salt: 'customsalt', // optional — auto-generated if omitted
});
auth.salt // the salt string (share this with startHeartbeat)Verify a player on join:
client.on('packet', (p) => {
if (p.name === 'identification') {
if (!auth.verify(p.username, p.verificationKey)) {
client.disconnect('Invalid login. Please connect via classicube.net');
return;
}
// verified — proceed with login
}
});Register on the server list:
// Start periodic heartbeat (every 45 s) and get the play URL
const { url, error } = await auth.startHeartbeat(() => server.playerCount);
if (error) {
console.warn('Heartbeat warning:', error);
// Server still works on LAN. Usually means port not forwarded.
} else {
console.log('Server URL:', url);
// → https://www.classicube.net/server/play/abc123hash/
}
// Manual single heartbeat (optional):
const result = await auth.sendHeartbeat(playerCount);
// Stop heartbeats:
auth.stopHeartbeat();
// Get the last known URL:
auth.serverUrl // string | nullDebug: compute what MPPass you expect from a player:
const expected = auth.computeExpected('PlayerName');ClassiCubeAccount — bot / client login
const account = new ClassiCubeAccount();
// Log in with a real classicube.net account
const { verified } = await account.login('BotUsername', 'BotPassword');
// verified: false if account email is not confirmed (mppasses will be empty)
account.username // string — confirmed username from server
account.loggedIn // boolean
account.verified // booleanBrowse and join servers:
// Get all servers in the server list (with your personal mppass for each)
const servers = await account.getServers();
// servers: ClassiCubeServerInfo[]
// Fetch info for one specific server by its hash
const server = await account.getServer('abc123hash');
// Find a server by name substring
const server = await account.findServer('Survival');ClassiCubeServerInfo shape:
{
hash: 'abc123hash',
name: 'My Survival Server',
ip: '123.45.67.89',
port: 25565,
mppass: 'd3ad8ee0f...', // use this as verificationKey when connecting
software: 'MCGalaxy',
players: 5,
maxPlayers: 32,
online: true,
playUrl: 'https://www.classicube.net/server/play/abc123hash/'
}Full bot flow:
const account = new ClassiCubeAccount();
await account.login('BotUser', 'pass');
const server = await account.findServer('Build World');
const bot = new ClassiCubeClient();
await bot.connect(server.ip, server.port);
bot.sendIdentification(account.username, server.mppass);12. cpe – Classic Protocol Extensions
const { cpe, CPE_EXTENSIONS, CPE_PACKETS } = require('classic-node-protocol');CPE allows servers to negotiate optional features with clients. The handshake happens right after the client's identification packet (if unused === 0x42).
CPE negotiation flow
Server side:
srv.on('connection', (client) => {
client.on('packet', (p) => {
if (p.name === 'identification') {
if (p.unused === 0x42) {
// Client supports CPE
client.supportsCpe = true;
client.sendCpeHandshake('MyServer', ['EnvColors', 'HackControl', 'HeldBlock']);
}
// Continue with normal identification response...
client.sendIdentification('MyServer', 'Welcome');
}
if (p.name === 'extEntry') {
client.extensions.add(p.extName);
}
});
});Client side:
bot.sendCpeIdentification(
'BotName',
'-',
['EnvColors', 'HackControl'] // subset of extensions you want to use
// or omit to advertise all known extensions
);Low-level CPE encoding
All CPE packets are available as pure functions on the cpe module:
// Negotiation (both directions)
cpe.encodeExtInfo(appName, extensionCount) // → 67-byte Buffer
cpe.encodeExtEntry(extName, version) // → 69-byte Buffer
cpe.buildExtensionHandshake(appName, extensions) // → Buffer[] (ExtInfo + N×ExtEntry)
// Decoding
cpe.decodeExtInfo(buf) // → { name: 'extInfo', appName, extensionCount }
cpe.decodeExtEntry(buf) // → { name: 'extEntry', extName, version }Extension reference
ClickDistance
client.sendSetClickDistance(160) // 160 units = 5 blocks (default)
client.sendSetClickDistance(32) // 1 block range (very restrictive)CustomBlocks
client.sendCustomBlockSupportLevel(1) // unlock IDs 50–65HeldBlock
client.sendHoldThis(BLOCKS.STONE, 0) // give player stone, allow switching
client.sendHoldThis(BLOCKS.STONE, 1) // lock them to stone
client.sendHoldThis(0, 0) // hide handTextHotKey
client.sendSetTextHotKey(
'Say Hello', // label
'/say Hello', // text to type (use '\n' to auto-send)
0x48, // key code (LWJGL — H key)
0 // modifiers: 0=none, 1=Ctrl, 2=Shift, 4=Alt (combinable)
);ExtPlayerList
// Add player to tab list
client.sendExtAddPlayerName(
nameId, // unique short ID
'PlayerName', // in-game username
'&aPlayerName', // tab-list display name (supports color codes)
'Admins', // group name
0 // group rank (lower = higher in list)
);
// Spawn with custom skin
client.sendExtAddEntity2(entityId, 'PlayerName', 'SkinName', x, y, z, yaw, pitch);
// Remove from tab list
client.sendExtRemovePlayerName(nameId);EnvColors
const { ENV_COLOR } = require('classic-node-protocol');
// ENV_COLOR constants:
// SKY=0 CLOUD=1 FOG=2 AMBIENT=3 SUNLIGHT=4
client.sendEnvSetColor(ENV_COLOR.SKY, 135, 206, 235); // sky blue
client.sendEnvSetColor(ENV_COLOR.CLOUD, 255, 255, 255); // white clouds
client.sendEnvSetColor(ENV_COLOR.FOG, 180, 180, 180);
client.sendEnvSetColor(ENV_COLOR.AMBIENT, 40, 40, 40);
client.sendEnvSetColor(ENV_COLOR.SUNLIGHT, 255, 255, 200);
client.sendEnvSetColor(ENV_COLOR.SKY, -1, -1, -1); // reset to defaultSelectionCuboid
// Highlight a region in the world
client.sendMakeSelection(
0, // selectionId (0–255)
'My Region', // label shown on hover
0, 0, 0, // start corner (x1, y1, z1)
10, 10, 10, // end corner (x2, y2, z2)
255, 0, 0, // color RGB
128 // alpha (0–255)
);
client.sendRemoveSelection(0); // remove by idBlockPermissions
client.sendSetBlockPermission(BLOCKS.BEDROCK, 0, 0); // cannot place or break bedrock
client.sendSetBlockPermission(BLOCKS.GRASS, 1, 1); // can do bothChangeModel
client.sendChangeModel(entityId, 'chicken');
client.sendChangeModel(entityId, 'zombie');
client.sendChangeModel(entityId, 'humanoid'); // reset to default player modelEnvWeatherType
const { WEATHER } = require('classic-node-protocol');
// WEATHER.SUNNY=0 WEATHER.RAINING=1 WEATHER.SNOWING=2
client.sendEnvSetWeatherType(WEATHER.RAINING);HackControl
⚠️ ClassiCube only processes this packet AFTER the first
LevelDataChunkhas been sent.
client.sendHackControl(
1, // flying allowed (1/0)
1, // noclip allowed
1, // speeding allowed
1, // spawn control allowed
1, // third-person view allowed
-1 // jump height in player-space units (-1 = default)
);EnvMapAspect
const { MAP_ENV_PROPERTY } = require('classic-node-protocol');
// MAP_ENV_PROPERTY constants:
// SIDE_BLOCK=0 EDGE_BLOCK=1 EDGE_HEIGHT=2 CLOUD_HEIGHT=3
// MAX_FOG=4 CLOUD_SPEED=5 WEATHER_SPEED=6 WEATHER_FADE=7
// EXP_FOG=8 SIDE_OFFSET=9
client.sendSetMapEnvUrl('https://example.com/textures.zip');
client.sendSetMapEnvUrl(''); // reset to default
client.sendSetMapEnvProperty(MAP_ENV_PROPERTY.CLOUD_HEIGHT, 128);
client.sendSetMapEnvProperty(MAP_ENV_PROPERTY.EDGE_HEIGHT, 32);
client.sendSetMapEnvProperty(MAP_ENV_PROPERTY.MAX_FOG, 512);EntityProperty
const { ENTITY_PROPERTY } = require('classic-node-protocol');
// ENTITY_PROPERTY: ROT_X=0 ROT_Y=1 ROT_Z=2 SCALE_X=3 SCALE_Y=4 SCALE_Z=5
client.sendSetEntityProperty(entityId, ENTITY_PROPERTY.SCALE_X, 64); // 2× scale
client.sendSetEntityProperty(entityId, ENTITY_PROPERTY.ROT_Z, 45); // tiltBlockDefinitions
client.sendDefineBlock({
blockId: 50,
name: 'Glowstone',
solidity: 1, // 0=walk-through 1=solid 2=liquid
movementSpeed: 128, // 0–255 (128 = normal)
topTexture: 18,
sideTexture: 18,
bottomTexture: 18,
transmitsLight: 0,
walkSound: 1,
fullBright: 1, // 1 = emits light (glows)
shape: 0, // 0–8 (block shape)
blockDraw: 0, // 0=opaque 1=transparent 2=translucent 3=gas
fogDensity: 0,
fogR: 0, fogG: 0, fogB: 0,
});
client.sendRemoveBlockDefinition(50);BulkBlockUpdate
Update up to 256 blocks in a single packet:
const { level, BLOCKS } = require('classic-node-protocol');
client.sendBulkBlockUpdate([
{ index: level.blockIndex(5, 3, 10, 64, 64), blockType: BLOCKS.GOLD_BLOCK },
{ index: level.blockIndex(6, 3, 10, 64, 64), blockType: BLOCKS.GOLD_BLOCK },
{ index: level.blockIndex(7, 3, 10, 64, 64), blockType: BLOCKS.GOLD_BLOCK },
]);TextColors
// Redefine the '&c' color code (ASCII 0x63) to hot pink
client.sendSetTextColor(0x63, 255, 20, 147, 255);TwoWayPing
// Server sends ping, expects client to echo it back
client.sendTwoWayPing(0, 1234); // direction=0: server→client, data=1234
// Client echoes back (direction=1: client→server)
bot.on('packet', (p) => {
if (p.name === 'twoWayPing' && p.direction === 0) {
bot.sendTwoWayPing(1, p.data); // echo
}
});InventoryOrder
client.sendSetInventoryOrder(0, BLOCKS.STONE); // slot 0 = stone
client.sendSetInventoryOrder(1, BLOCKS.GRASS); // slot 1 = grass
client.sendSetInventoryOrder(2, BLOCKS.OBSIDIAN); // slot 2 = obsidian13. jugadorUUID – UUID Utilities
Generates deterministic MD5-based (v3-style) UUIDs from usernames, matching ClassiCube's player identification format.
const { jugadorUUID } = require('classic-node-protocol');jugadorUUID.generarUUID('PlayerName')
// → '550e8400-e29b-31d4-a716-446655440000'
// Same username always produces the same UUID
jugadorUUID.validarUUID('550e8400-e29b-31d4-a716-446655440000')
// → true (or false for malformed strings)
jugadorUUID.uuidToHex('550e8400-e29b-31d4-a716-446655440000')
// → '550e8400e29b31d4a716446655440000'
jugadorUUID.hexToUUID('550e8400e29b31d4a716446655440000')
// → '550e8400-e29b-31d4-a716-446655440000'14. TypeScript Support
Full type definitions are included at types/index.d.ts. No @types/ package needed.
import {
createServer,
createClient,
ClassiCubeServer,
ClassiCubeClient,
ClientConnection,
PacketDecoder,
ClassiCubeAuth,
ClassiCubeAccount,
ClassiCubeServerInfo,
BLOCKS,
BLOCK_NAMES,
BLOCK_MODE,
USER_TYPE,
CPE_MAGIC,
CPE_EXTENSIONS,
CPE_PACKETS,
ENV_COLOR,
WEATHER,
MAP_ENV_PROPERTY,
ENTITY_PROPERTY,
} from 'classic-node-protocol';
const server: ClassiCubeServer = createServer({ port: 25565 });
server.on('connection', async (client: ClientConnection) => {
const blocks = level.buildFlatMap(64, 64, 64);
await client.sendLevel(blocks, 64, 64, 64);
});15. Protocol Reference
FShort (Fixed-Point Coordinates)
The Classic protocol uses fixed-point integers for positions:
1 block = 32 units
blockPosition = fshort / 32
fshort = blockPosition × 32const { encoder } = require('classic-node-protocol');
encoder.toFShort(5.5) // → 176 (5.5 blocks → FShort)
encoder.fromFShort(176) // → 5.5 (FShort → blocks)When using sendSpawnPlayer, encodePosition, etc., always pass FShort values unless you're using the convenience methods (spawnAt, sendPositionBlocks) which convert automatically.
Packet byte sizes
| Packet | ID | Size | Direction | |---|---|---|---| | Identification | 0x00 | 131 | Both | | Ping | 0x01 | 1 | S→C | | LevelInitialize | 0x02 | 1 | S→C | | LevelDataChunk | 0x03 | 1028 | S→C | | LevelFinalize | 0x04 | 7 | S→C | | SetBlock (client) | 0x05 | 9 | C→S | | SetBlock (server) | 0x06 | 8 | S→C | | SpawnPlayer | 0x07 | 74 | S→C | | Position | 0x08 | 10 | Both | | PositionOrientation | 0x09 | 7 | S→C | | PositionUpdate | 0x0A | 5 | S→C | | OrientationUpdate | 0x0B | 4 | S→C | | DespawnPlayer | 0x0C | 2 | S→C | | Message | 0x0D | 66 | Both | | Disconnect | 0x0E | 65 | S→C | | UpdateUserType | 0x0F | 2 | S→C |
Map memory layout
index = x + (z × xSize) + (y × xSize × zSize)
Axes:
X = East/West (increases East)
Y = Up/Down (increases Up)
Z = North/South (increases South)
Spawn convention: place players at y = groundY + 1 (above the surface)Auth error codes
| Code | Meaning | Fatal? |
|---|---|---|
| token | CSRF token rejected | Yes |
| username | Account not found | Yes |
| password | Wrong password | Yes |
| verification | Email not verified — mppasses empty | No (warning) |
License
MIT © Rosendo Torres
