Skip to main content

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

PortPurposeConfigurable
3000Raw TCP (legacy Flash client)game.port in config.ini
ws.nitro.portWebSocket (Nitro)emulator_settings.ws.nitro.port
3001RCON (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 │ │
└──────────────┴───────────────┴───────────────────────────┘
OffsetSizeTypeDescription
04 bytesint32 BETotal byte count of the payload (header + body). Does NOT include these 4 bytes.
42 bytesint16 BEMessage header - identifies the message type (see ID tables).
6variable-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 typeWire encodingSizeNotes
intsigned 32-bit integer4 bytesDefault numeric type
shortsigned 16-bit integer2 bytesUsed for header; also Short wrapper in client
byteunsigned 8-bit integer1 byteUsed for Byte wrapper in client
boolean1 byte1 byte0x01 = true, 0x00 = false
string[length: int16][utf-8 bytes]2 + N bytesLength prefix is int16 (max 32 767 chars)
double64-bit IEEE 7548 bytes
float32-bit IEEE 7544 bytes
null/undefinedwriteShort(0)2 bytesEncodes as empty short
ArrayBufferraw bytesN bytesNo 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

ComponentAlgorithmClasses
Key exchangeDiffie-HellmanHabboDiffieHellman.java
Server identityRSA-signed DH parametersHabboRSACrypto.java
Stream cipherRC4HabboRC4.java

Encrypted handshake flow

When enc.enabled=true:

  1. Client → Server: InitDiffieHandshake (C→S 3110) - request DH params
  2. Server → Client: InitDiffieHandshake (S→C 1347) - sends RSA-signed prime + generator
  3. Client → Server: CompleteDiffieHandshake (C→S 773) - sends client public key
  4. Server → Client: CompleteDiffieHandshake (S→C 3885) - sends server public key + clientEncryption: bool
  5. Both sides derive shared secret → initialize RC4 stream cipher
  6. 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] ←────────────────────────────────────────────────────────────────