JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 6
  • Score
    100M100P100Q62130F
  • License GPL-3.0-only

AFK bot for Minecraft Pocket Edition 0.14.x / PocketMine-MP. Custom RakNet, zero game-protocol dependencies.

Package Exports

  • draconium

Readme

Draconium

AFK bot library for Minecraft Pocket Edition 0.14.x running on PocketMine-MP servers. Implements a full custom RakNet client from scratch — no external game-protocol dependencies. All packet structures and physics constants are verified directly against the PocketMine-MP and RakLib source code.


Requirements

  • Node.js >= 14.0.0
  • A PocketMine-MP server on protocol 70 (MCPE 0.14.x)
  • For the Discord bot: npm install discord.js

Installation

git clone <repo>
cd draconium
npm install          # no dependencies for the core bot
npm install discord.js   # only needed for discord-bot.js

Quick Start

const { Bot } = require('draconium');

const bot = new Bot({
  host:     'play.example.com',
  port:     19132,
  username: 'MyBot',
});

bot.on('ready',  () => bot.chat('Online.'));
bot.on('chat',   ({ source, message }) => console.log(`<${source}> ${message}`));
bot.on('health', (hp) => { if (hp <= 0) setTimeout(() => bot.respawn(), 1000); });

bot.connect();

Running the Examples

# Full AFK bot with in-game chat commands
node example/bot.js

# Multi-bot join script
node example/join.js

# Discord bot controller
DISCORD_TOKEN=your_token node discord-bot.js

Environment variables accepted by example/bot.js:

Variable Default Description
MCPE_HOST 127.0.0.1 Server IP or hostname
MCPE_PORT 19132 Server UDP port
MCPE_USERNAME DraconiumBot_N In-game display name

API

Constructor

new Bot(options)
Option Type Default Description
host string '127.0.0.1' Server IP or hostname
port number 19132 Server UDP port
username string 'DraconiumBot' In-game display name
version string '0.14.3' MCPE version string

Methods

Method Description
bot.connect() Resolve host, open UDP socket, complete RakNet handshake, and send login packet
bot.chat(message) Send a chat message in-game
bot.teleport(x, y, z) Move the bot to exact coordinates and update ground level
bot.jump() Jump — only fires when the bot is on the ground
bot.respawn() Send a respawn packet — call this when health reaches 0
bot.disconnect(reason) Clean disconnect — closes socket and emits disconnect with intentional = true

Events

Event Arguments Description
ready Bot is fully spawned and in-game
spawn { entityId } Spawn packet received from server
chat { source, message } Chat message received. source is empty for server messages
health hp (number) Health changed (0–20)
move { x, y, z } Bot position updated via teleport
jump Bot jumped
respawn Respawn packet sent
disconnect reason, intentional Bot disconnected. intentional is true when bot.disconnect() was called by your code, false when the server disconnected the bot
error Error Connection or protocol error

State Properties

Property Type Description
bot.connected boolean Whether the RakNet connection is live
bot.spawned boolean Whether the bot has spawned in-game
bot.entity number Entity ID assigned by the server
bot.health number Current health (0 to 20)
bot.position {x, y, z} Current feet position
bot.onGround boolean Whether physics considers the bot grounded

Multiple Bots

Each Bot instance is fully independent — its own UDP socket, RakNet state machine, reliable ordering window, and physics loop. They do not share any state. To run multiple bots:

const { Bot } = require('draconium');

const SERVER = { host: 'play.example.com', port: 19132 };

['AFK1', 'AFK2', 'AFK3'].forEach((username, i) => {
  setTimeout(() => {
    const bot = new Bot({ ...SERVER, username });
    bot.on('ready', () => bot.chat('Online.'));
    bot.on('disconnect', (reason, intentional) => {
      if (!intentional) console.log(`${username} disconnected: ${reason}`);
    });
    bot.connect();
  }, i * 4000);
});

example/join.js provides a full multi-bot script with comma-separated usernames, auto-retry on failure, auth handling, and clean shutdown on SIGINT.


Discord Bot

discord-bot.js is a Discord controller for the bots. It requires discord.js v14.

Setup

  1. Create a Discord application and bot at https://discord.com/developers/applications
  2. Under Bot > Privileged Gateway Intents, enable Message Content Intent
  3. Invite the bot with the bot scope and permissions: Send Messages, Read Message History, View Channels
  4. Set your token and start:
DISCORD_TOKEN=your_token node discord-bot.js
PREFIX=!  # optional, default is .

Commands

Command Description
.help Full command reference
.configure Interactive wizard: register password, login password, join messages
.config View current global config
.join <ip:port> <bot1,bot2,...> Connect one or more bots to a server
.leave <username|*> Disconnect a specific bot or all bots (*)
.list All active bots with status, server, and uptime
.info <username> Detailed info: server, status, uptime, health, position
.chat * <message> Broadcast a message from all online bots
.chat <username> <message> Send a message from a specific bot
.chatarea <username> #channel on Bridge this bot's MC chat to a Discord channel
.chatarea <username> #channel off Disable that bridge
.chatarea list List all configured bridges and their state
.logs <username> Paginated log viewer (Last 50 entries, Prev/Next buttons)
.credits About

Chat Bridge

When a bridge is enabled for a bot, all MC chat messages that bot receives are forwarded to the linked Discord channel as embeds. Any message sent in that Discord channel that does not start with the command prefix is forwarded into MC via that bot, prefixed with the Discord sender's username.


Project Structure

draconium/
  src/
    index.js                  Entry point — exports { Bot }
    Bot.js                    Main Bot class (242 lines)
    RakNet.js                 RakNet UDP client (540 lines)
    physics/
      Physics.js              Client-side gravity simulation (98 lines)
    protocol/
      ids.js                  Packet IDs and constants — mirrors PocketMine Info.php (53 lines)
      binary.js               getString / putString — mirrors BinaryStream.php (27 lines)
      packets.js              Encode/decode per packet — mirrors protocol/*.php (311 lines)
      skin.js                 Steve skin builder, valid 64x32 RGBA (111 lines)
  example/
    bot.js                    Full AFK bot with in-game chat commands (114 lines)
    join.js                   Multi-bot join script with retry and clean shutdown (146 lines)
  discord-bot.js              Discord bot controller (990 lines)
  package.json
  README.md

Total: 2,637 lines across 11 source files

Bug Fixes and Technical Notes

This section documents every bug fixed during development, for reference.

Physics

Floating bot (groundY sentinel value) The original code initialised groundY = -999 as a sentinel. The landing condition newY <= groundY was never satisfied because the bot was always above -999, so it floated indefinitely. Fixed by initialising groundY = null and only applying the landing snap when groundY !== null. The server always sends StartGamePacket or SetSpawnPositionPacket before PlayStatus.PLAYER_SPAWN, so groundY is guaranteed to be set before the physics loop starts.

StartGame position parsed at wrong offset StartGamePacket encodes x/y/z at byte offsets 33/37/41 (after seed, dim, generator, gamemode, entityId, spawnX, spawnY, spawnZ). The old code checked data.length >= 41 but needed 45 to safely read the z float. Fixed with the correct minimum length check.

MovePlayer Y is eye Y, not feet Y Player.php confirms $pk->y = $pos->y + $this->getEyeHeight(). The server sends eye position. We subtract 1.62 on receive to get feet Y, and add 1.62 when sending MovePlayerPacket back. StartGamePacket sends raw feet Y directly — no offset needed there.

teleport() did not update groundY Calling bot.teleport(x, y, z) updated pos but left groundY at the previous floor level. On the next physics tick the bot would snap back to the old floor. Fixed by having Physics.setPos() also update groundY.

Physics drag formula The original code applied drag as vy *= 0.98 which is slightly wrong. Entity.php shows motionY = (motionY - gravity) * (1 - drag). Fixed to match: vy = (vy - 0.08) * 0.98.

RakNet

Hostname crash in encodeAddress encodeAddress() only handles dotted-decimal IPv4. Passing a hostname like play.example.com caused Number('play') = NaN, then ~NaN = -1 (0xff) for all address bytes. The server rejects the port check in CLIENT_HANDSHAKE and the handshake silently fails. Fixed by calling dns.lookup() before opening the socket and using the resolved IP for all address encoding.

recoveryQueue unbounded memory growth Packets added to the recovery queue for NACK retransmission were only removed when the server ACKed them. Packets the server never ACKed accumulated indefinitely. For a long- running bot this continuously consumed memory. Fixed by capping: when a new entry is stored at sequence N, the entry at N-2048 is deleted since the server window can never NACK that far back.

recoveryQueue not cleared on close close() cleared timers but left the recovery queue object populated, preventing GC of all the buffer allocations for the lifetime of the Bot object. Fixed by resetting the object in close().

Stale incomplete split packets If the last fragment of a split packet was dropped by the network, the partial entry sat in splitPackets indefinitely. After 4 such stale entries, the 4-slot cap caused all subsequent split packets to be silently dropped. This broke the LOGIN packet (which is always fragmented) after network degradation. Fixed by adding a 10-second timeout per split entry: incomplete splits are deleted after 10 seconds, freeing the slot.

Fragment messageIndex double-increment The original _sendFragmented incremented messageIndex twice for fragment 0 — once via firstMessageIndex = this.messageIndex and again via this.messageIndex++. This left a gap in the sequence that stalled the server's reliable ordering window. Fixed by using a single increment per fragment.

NACK retransmission not implemented The server was sending NACK packets (requesting retransmission of lost datagrams) but the client was ignoring them. Added _onNack() to retransmit from the recovery queue.

Split packet timer leaks on disconnect Split packet expiry timers were not cleared when the connection closed. Fixed by iterating splitPackets in close() and clearing all pending timers.

Batch / Protocol

decodeBatch used signed readInt32BE for compLen readInt32BE(0) on a malformed or truncated packet returns a negative number. data.slice(4, 4 + negative) silently returns an empty buffer which fails inflate with no diagnostic output. Fixed by using readUInt32BE so the value is always non-negative.

encodeChat buffer used incorrect slice encodeChat allocated the buffer to the exact required size but returned buf.slice(0, o) where o was only updated through the first writeString call. The second call's bytes were always included (the allocation was exact) but the return value was fragile. Fixed by returning the full pre-sized buffer directly.

Bot / Connection

Server disconnect did not close RakNet socket _onServerDisconnect called _cleanup() but never closed or nulled this._raknet. The UDP socket and keepalive interval stayed open indefinitely after a server-side kick, leaking resources across multiple bot instances. Fixed by closing and nulling _raknet in _onServerDisconnect.

Double disconnect emit If the RakNet layer emitted disconnect and a timeout fired in the same tick, _onServerDisconnect could be called twice, emitting the event twice. Fixed by guarding with an early return if connected and spawned are both already false.

Auth spam loop in join.js and discord-bot.js The chat event re-auth trigger fired on every message containing "register" or "login", including messages from other players and the bot's own echoed chat. This caused /login and /register to be sent repeatedly. Fixed by only re-authing when source is empty (server message) and the bot has not yet completed the join sequence.

SIGINT leaked connections in join.js The original SIGINT handler exited after a short delay without calling bot.disconnect() on any active bots, leaving ghost connections on the server until it timed them out. Fixed by tracking all active bots in a Set and disconnecting each cleanly on shutdown.

Discord bot disconnect handler ignored intentional flag The original discord-bot.js disconnect handler only accepted (reason), ignoring the new second argument intentional. Intentional disconnects from .leave triggered the retry logic. Fixed by checking the intentional flag and returning early when true.


Protocol Reference

Field Value
Protocol version 70 (MCPE 0.14.x)
Transport UDP / custom RakNet
Compression zlib deflate (BatchPacket)
Auth mode Offline (no Xbox Live)
Skin format 64x32 RGBA (8192 bytes)
Server tick rate 20 ticks/s (50ms)
Physics gravity 0.08 blocks/tick squared
Physics drag 0.02 (multiplier 0.98)
Jump velocity 0.42 blocks/tick
Anti-AFK jump Every 55 seconds

License

GNU