Protocol
This page documents the NeoPixel (originally Arcturus ↔ Nitro) wire protocol. It covers the transport and framing layer, the type encoding, and the complete message header (ID) lists. Detailed per-domain message bodies are covered in the sub-pages linked in the sidebar.
Protocol
Transport
Client (Browser)
│
│ WebSocket ws:// or wss://
│
arcturus-ws plugin (Netty WebSocket server)
│
│ in-process channel pipeline forwarding
│
Arcturus game server (Netty TCP pipeline)
- The arcturus-ws plugin upgrades incoming HTTP connections to WebSocket.
- WebSocket binary frames are forwarded directly into the Arcturus Netty pipeline.
- The game logic never sees WebSocket framing, it only sees raw EvaWire bytes.
- WebSocket text frames are ignored; only binary frames carry protocol data.
Ports
| Port | Purpose | Configurable |
|---|---|---|
| 3000 | Raw TCP (legacy Flash client) | game.port in config.ini |
| ws.nitro.port | WebSocket (Nitro) | emulator_settings.ws.nitro.port |
| 3001 | RCON (CMS → server) | rcon.port in config.ini |
Wire Format (EvaWire)
Every message uses the EvaWire framing:
┌──────────────────────────────────────────────────────────┐
│ EvaWire Frame │
├──────────────┬───────────────────────────────────────────┤
│ Header │ Payload │
│──────────────│───────────────────────────────────────────│
│ Length (4B) │ Msg ID (2B) │ Body (variable) │
│ big-endian │ big-endian │ type-encoded fields │
│ int32 │ int16 │ │
└──────────────┴───────────────┴───────────────────────────┘
| Offset | Size | Type | Description |
|---|---|---|---|
| 0 | 4 bytes | int32 BE | Total byte count of the payload (header + body). Does NOT include these 4 bytes. |
| 4 | 2 bytes | int16 BE | Message header - identifies the message type (see ID tables). |
| 6 | variable | - | Body - zero or more type-encoded fields, serialized in order. |
Decoding algorithm
From EvaWireFormat.ts + GameByteFrameDecoder.java.
loop:
if buffer.length < 4: wait for more data
length = readInt32BE(buffer[0..3])
if buffer.length < 4 + length: wait for more data
payload = buffer[4 .. 4+length]
header = readInt16BE(payload[0..1])
body = payload[2 .. length]
dispatch(header, body)
buffer = buffer[4+length ..]
Encoding algorithm
From ServerMessage.java + EvaWireFormat.ts.
Server-side (Java):
// ServerMessage.init(id):
stream.writeInt(0); // placeholder - length filled in on get()
stream.writeShort(id); // message header
// then body fields via appendInt/appendString/appendBoolean/…
Client-side (TypeScript):
// EvaWireFormat.encode(header, values):
writer.writeShort(header); // 2-byte header
for value in values: writeTyped(value);
buffer = writer.getBuffer();
return new BinaryWriter().writeInt(buffer.byteLength).writeBytes(buffer);
Maximum frame size
The server accepts frames up to 417 792 bytes (MAX_PACKET_LENGTH in
GameByteFrameDecoder). This accommodates the largest expected payload: camera
PNG images (320×320 × 4 bytes = 409 600 bytes + overhead).
Type Encoding Reference
All types are big-endian.
| Logical type | Wire encoding | Size | Notes |
|---|---|---|---|
int | signed 32-bit integer | 4 bytes | Default numeric type |
short | signed 16-bit integer | 2 bytes | Used for header; also Short wrapper in client |
byte | unsigned 8-bit integer | 1 byte | Used for Byte wrapper in client |
boolean | 1 byte | 1 byte | 0x01 = true, 0x00 = false |
string | [length: int16][utf-8 bytes] | 2 + N bytes | Length prefix is int16 (max 32 767 chars) |
double | 64-bit IEEE 754 | 8 bytes | |
float | 32-bit IEEE 754 | 4 bytes | |
null/undefined | writeShort(0) | 2 bytes | Encodes as empty short |
ArrayBuffer | raw bytes | N bytes | No length prefix; used for binary blobs |
Encryption (Optional RC4)
Encryption is disabled by default (enc.enabled=false in config.ini). When
enabled, the protocol adds a Diffie-Hellman key exchange phase before login.
Crypto components
| Component | Algorithm | Classes |
|---|---|---|
| Key exchange | Diffie-Hellman | HabboDiffieHellman.java |
| Server identity | RSA-signed DH parameters | HabboRSACrypto.java |
| Stream cipher | RC4 | HabboRC4.java |
Encrypted handshake flow
When enc.enabled=true:
- Client → Server:
InitDiffieHandshake(C→S 3110) - request DH params - Server → Client:
InitDiffieHandshake(S→C 1347) - sends RSA-signed prime + generator - Client → Server:
CompleteDiffieHandshake(C→S 773) - sends client public key - Server → Client:
CompleteDiffieHandshake(S→C 3885) - sends server public key +clientEncryption: bool - Both sides derive shared secret → initialize RC4 stream cipher
- All subsequent frames are RC4-encrypted before length-framing
RC4 key derivation
sharedKey = DiffieHellman.getSharedKey(remotePublicKey)
// sharedKey is a byte[] derived from (remotePublicKey ^ privateKey) mod prime
clientRC4 = new HabboRC4(sharedKey)
serverRC4 = new HabboRC4(sharedKey)
Netty pipeline with encryption enabled:
[GameByteEncryption] → [GameByteFrameDecoder] → [GameByteDecoder] → [GameMessageHandler]
[GameByteDecryption] ←────────────────────────────────────────────────────────────────