npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-protocol

Requires Node.js ≥ 18 · v3.0.0


Table of Contents

  1. Quick Start
  2. Architecture Overview
  3. createServer & ClassiCubeServer
  4. ClientConnection
  5. createClient & ClassiCubeClient
  6. PacketDecoder
  7. encoder – Low-Level Packet Builders
  8. codec – Generic Encode/Decode Engine
  9. protocol – Constants
  10. level – Map Generation & Transmission
  11. auth – Authentication & Heartbeat
  12. cpe – Classic Protocol Extensions
  13. jugadorUUID – UUID Utilities
  14. TypeScript Support
  15. 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 username

Everything 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 clients

Example: 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 on

Sending 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 Buffer

5. 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 send

Full 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 176

String helpers

encoder.writeString(buf, offset, str)  // write 64-byte ASCII string into buf
encoder.readString(buf, offset)        // read 64-byte ASCII string, trim trailing spaces

Server → 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 Buffer

Client → 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 Buffer

8. 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         // 49

Reverse 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 CPE

CPE 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 above

buildSphereMap — 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 demos

buildCheckerMap — 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: AIR

buildMap — 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 IDs

Raw 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 false

ClassiCubeAuth — 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 | null

Debug: 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  // boolean

Browse 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–65

HeldBlock

client.sendHoldThis(BLOCKS.STONE, 0)  // give player stone, allow switching
client.sendHoldThis(BLOCKS.STONE, 1)  // lock them to stone
client.sendHoldThis(0, 0)             // hide hand

TextHotKey

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 default

SelectionCuboid

// 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 id

BlockPermissions

client.sendSetBlockPermission(BLOCKS.BEDROCK, 0, 0); // cannot place or break bedrock
client.sendSetBlockPermission(BLOCKS.GRASS,   1, 1); // can do both

ChangeModel

client.sendChangeModel(entityId, 'chicken');
client.sendChangeModel(entityId, 'zombie');
client.sendChangeModel(entityId, 'humanoid'); // reset to default player model

EnvWeatherType

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 LevelDataChunk has 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); // tilt

BlockDefinitions

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 = obsidian

13. 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 × 32
const { 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