cubyz-node-client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,211 @@
1
+ # Cubyz Node.js Client Library
2
+
3
+ `cubyz-node-client` is a small TypeScript library that speaks the Cubyz networking protocol from Node.js. It exposes a high-level `CubyzConnection` class for establishing a UDP session with a running Cubyz server, handling the handshake, managing sequenced channels, and publishing client state packets.
4
+
5
+ ## Features
6
+
7
+ - Typed wrapper around the Cubyz UDP protocol (init negotiation, confirmations, keep-alives)
8
+ - Full handshake implementation and spawn data parsing
9
+ - Helpers for sending chat messages, teleport updates, and rotation changes
10
+ - Lightweight ZON parser for decoding server payloads without bundling Zig tooling
11
+ - Designed for embedding in other tooling, bots, or integration tests
12
+ - Configurable log level with typed disconnect events when the server closes the session
13
+
14
+ ## Requirements
15
+
16
+ - Node.js 18 or newer (modern UDP & BigInt APIs)
17
+ - Access to a Cubyz server (default UDP port `47649`)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install cubyz-node-client
23
+ ```
24
+
25
+ ## Building from source
26
+
27
+ ```bash
28
+ npm install
29
+ npm run build
30
+ ```
31
+
32
+ Compilation outputs ESM modules alongside type declarations in `dist/`.
33
+
34
+ ## Quick start example
35
+
36
+ You can run the included sandbox example to see the connection flow end-to-end:
37
+
38
+ ```bash
39
+ # Optional overrides for host/port/player name
40
+ export CUBYZ_HOST=127.0.0.1
41
+ export CUBYZ_PORT=47649
42
+ export CUBYZ_NAME=ExampleBot
43
+ export CUBYZ_LOG_LEVEL=debug
44
+
45
+ npm run sandbox
46
+ ```
47
+
48
+ The example connects to the configured server, logs chat/player events, emits a greeting, and then shuts down gracefully after a few seconds.
49
+
50
+ ## Programmatic usage
51
+
52
+ ```ts
53
+ import { CubyzConnection, DEFAULT_PORT } from "cubyz-node-client";
54
+
55
+ const connection = new CubyzConnection({
56
+ host: "127.0.0.1",
57
+ port: DEFAULT_PORT,
58
+ name: "ToolingBot",
59
+ logger: console,
60
+ logLevel: "warn",
61
+ });
62
+
63
+ connection.on("connected", () => {
64
+ console.log("Channel handshake ready");
65
+ });
66
+
67
+ connection.on("handshakeComplete", (spawnData) => {
68
+ console.log("Server spawn data received:", spawnData);
69
+ connection.sendChat("Hello world from tooling!");
70
+ });
71
+
72
+ connection.on("players", (players) => {
73
+ console.log("Known players:", players);
74
+ });
75
+
76
+ connection.on("chat", (message) => {
77
+ console.log("[chat]", message);
78
+ });
79
+
80
+ connection.on("protocol", (event) => {
81
+ console.log("Protocol event:", event.protocolId);
82
+ });
83
+
84
+ connection.on("disconnect", (details) => {
85
+ console.log("Server closed connection", details.reason);
86
+ });
87
+
88
+ await connection.start();
89
+
90
+ // Later: connection.close();
91
+ ```
92
+
93
+ ## API Reference
94
+
95
+ ### CubyzConnection
96
+
97
+ #### Constructor Options
98
+
99
+ ```ts
100
+ interface CubyzConnectionOptions {
101
+ host: string; // Server hostname or IP
102
+ port: number; // Server UDP port (default: 47649)
103
+ name: string; // Player/bot name
104
+ version?: string; // Protocol version (default: "0.1.0-dev")
105
+ logger?: CubyzConnectionLogger; // Custom logger (default: no-op)
106
+ logLevel?: LogLevel; // "debug" | "info" | "warn" | "error" | "silent"
107
+ }
108
+ ```
109
+
110
+ #### Events
111
+
112
+ - **`connected`**: Emitted when the channel handshake with the server completes
113
+ - **`handshakeComplete(spawnData: string)`**: Emitted when the server sends spawn/world data (ZON format)
114
+ - **`chat(message: string)`**: Emitted when a chat message is received from the server
115
+ - **`players(names: string[])`**: Emitted when the player list updates
116
+ - **`protocol(event: ProtocolEvent)`**: Emitted for other protocol messages (entity updates, etc.)
117
+ - **`disconnect(event: DisconnectEvent)`**: Emitted when the connection closes (reason: "server" | "timeout")
118
+
119
+ #### Methods
120
+
121
+ - **`async start()`**: Bind the UDP socket and initiate the connection
122
+ - **`close(options?: CloseOptions)`**: Close the connection gracefully (optionally skip server notification)
123
+ - **`sendChat(message: string)`**: Send a chat message to the server
124
+ - **`teleport(x: number, y: number, z: number)`**: Update player position
125
+ - **`setRotation(yawDeg: number, pitchDeg?: number, rollDeg?: number)`**: Update player rotation (degrees)
126
+ - **`getPlayerNames(): string[]`**: Get the current list of known player names
127
+ - **`publishPlayerState(force?: boolean)`**: Manually send a player state update
128
+
129
+ ## Key exports
130
+
131
+ ### Core Classes
132
+
133
+ - **`CubyzConnection`**: High-level client with typed events for managing server connections
134
+ - **`SendChannel`**: Low-level sequenced packet sender
135
+ - **`ReceiveChannel`**: Low-level sequenced packet receiver
136
+
137
+ ### ZON Parser
138
+
139
+ - **`parseZon(buffer: Buffer): ZonValue`**: Standalone ZON parser for inspecting server messages
140
+ - **`ZonValue`**: TypeScript type representing parsed ZON data structures
141
+
142
+ ### Binary Utilities
143
+
144
+ - **`encodeVarInt(value: number): Buffer`**: Encode a variable-length integer
145
+ - **`decodeVarInt(buffer: Buffer, offset?: number): { value: number; bytesRead: number }`**: Decode a variable-length integer
146
+ - **`seqLessThan(a: number, b: number): boolean`**: Compare sequence numbers with wraparound handling
147
+ - **`addSeq(seq: number, delta: number): number`**: Add to a sequence number with wraparound
148
+
149
+ ### Constants
150
+
151
+ - **`DEFAULT_PORT`**: Default Cubyz server port (47649)
152
+ - **`DEFAULT_VERSION`**: Default protocol version
153
+ - **`CHANNEL`**: Channel IDs (LOSSY, FAST, SLOW, etc.)
154
+ - **`PROTOCOL`**: Protocol IDs (HANDSHAKE, CHAT, ENTITY, etc.)
155
+
156
+ ### TypeScript Types
157
+
158
+ ```ts
159
+ export interface Vector3 {
160
+ x: number;
161
+ y: number;
162
+ z: number;
163
+ }
164
+
165
+ export interface PlayerState {
166
+ position: Vector3;
167
+ velocity: Vector3;
168
+ rotation: Vector3;
169
+ }
170
+
171
+ export interface ProtocolEvent {
172
+ channelId: number;
173
+ protocolId: number;
174
+ payload: Buffer;
175
+ }
176
+
177
+ export interface DisconnectEvent {
178
+ reason: "server" | "timeout";
179
+ }
180
+
181
+ export interface CloseOptions {
182
+ notify?: boolean; // Send disconnect packet to server (default: true)
183
+ }
184
+ ```
185
+
186
+ ## Development
187
+
188
+ - **`npm run build`**: Compile TypeScript sources to `dist/`
189
+ - **`npm run clean`**: Remove the `dist/` output directory
190
+ - **`npm run sandbox`**: Build and run the sandbox example
191
+ - **`npm run check`**: Run Biome linter/formatter checks
192
+ - **`npm run check:write`**: Auto-fix linting and formatting issues
193
+
194
+ ## Project Structure
195
+
196
+ ```
197
+ src/
198
+ index.ts - Main exports
199
+ connection.ts - CubyzConnection class
200
+ send_channel.ts - Sequenced packet sender
201
+ receive_channel.ts - Sequenced packet receiver
202
+ binary.ts - Binary encoding/decoding utilities
203
+ zon.ts - ZON format parser
204
+ constants.ts - Protocol constants
205
+ sandbox/
206
+ main.ts - Example usage
207
+ ```
208
+
209
+ ## Acknowledgments
210
+
211
+ This project was created with the assistance of Large Language Models (GPT-5 Codex and Claude Sonnet 4.5).
@@ -0,0 +1,11 @@
1
+ import { Buffer } from "node:buffer";
2
+ export declare function encodeVarInt(value: number): Buffer;
3
+ export declare function decodeVarInt(buffer: Buffer, offset?: number): {
4
+ value: number;
5
+ consumed: number;
6
+ };
7
+ export declare function toInt32(value: number): number;
8
+ export declare function addSeq(base: number, delta: number): number;
9
+ export declare function seqLessThan(a: number, b: number): boolean;
10
+ export declare function writeInt32BE(buffer: Buffer, offset: number, value: number): void;
11
+ export declare function readInt32BE(buffer: Buffer, offset: number): number;
package/dist/binary.js ADDED
@@ -0,0 +1,51 @@
1
+ import { Buffer } from "node:buffer";
2
+ export function encodeVarInt(value) {
3
+ if (value < 0) {
4
+ throw new RangeError("VarInt cannot encode negative values");
5
+ }
6
+ const bytes = [];
7
+ let remaining = value >>> 0;
8
+ do {
9
+ let byte = remaining & 0x7f;
10
+ remaining >>>= 7;
11
+ if (remaining !== 0) {
12
+ byte |= 0x80;
13
+ }
14
+ bytes.push(byte);
15
+ } while (remaining !== 0);
16
+ return Buffer.from(bytes);
17
+ }
18
+ export function decodeVarInt(buffer, offset = 0) {
19
+ let value = 0;
20
+ let consumed = 0;
21
+ let shift = 0;
22
+ while (offset + consumed < buffer.length) {
23
+ const byte = buffer[offset + consumed];
24
+ value |= (byte & 0x7f) << shift;
25
+ consumed += 1;
26
+ if ((byte & 0x80) === 0) {
27
+ return { value: value >>> 0, consumed };
28
+ }
29
+ shift += 7;
30
+ if (shift >= 35) {
31
+ throw new Error("VarInt decoding failed: value too large");
32
+ }
33
+ }
34
+ throw new Error("VarInt decoding failed: reached end of buffer");
35
+ }
36
+ export function toInt32(value) {
37
+ return (value << 0) | 0;
38
+ }
39
+ export function addSeq(base, delta) {
40
+ return toInt32((base + delta) | 0);
41
+ }
42
+ export function seqLessThan(a, b) {
43
+ return toInt32(a - b) < 0;
44
+ }
45
+ export function writeInt32BE(buffer, offset, value) {
46
+ buffer.writeInt32BE(toInt32(value), offset);
47
+ }
48
+ export function readInt32BE(buffer, offset) {
49
+ return buffer.readInt32BE(offset);
50
+ }
51
+ //# sourceMappingURL=binary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binary.js","sourceRoot":"","sources":["../src/binary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,UAAU,YAAY,CAAC,KAAa;IACxC,IAAI,KAAK,GAAG,CAAC,EAAE,CAAC;QACd,MAAM,IAAI,UAAU,CAAC,sCAAsC,CAAC,CAAC;IAC/D,CAAC;IACD,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,SAAS,GAAG,KAAK,KAAK,CAAC,CAAC;IAC5B,GAAG,CAAC;QACF,IAAI,IAAI,GAAG,SAAS,GAAG,IAAI,CAAC;QAC5B,SAAS,MAAM,CAAC,CAAC;QACjB,IAAI,SAAS,KAAK,CAAC,EAAE,CAAC;YACpB,IAAI,IAAI,IAAI,CAAC;QACf,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC,QAAQ,SAAS,KAAK,CAAC,EAAE;IAC1B,OAAO,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,MAAc,EACd,MAAM,GAAG,CAAC;IAEV,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,OAAO,MAAM,GAAG,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC;QACzC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,GAAG,QAAQ,CAAC,CAAC;QACvC,KAAK,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,CAAC;QAChC,QAAQ,IAAI,CAAC,CAAC;QACd,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACxB,OAAO,EAAE,KAAK,EAAE,KAAK,KAAK,CAAC,EAAE,QAAQ,EAAE,CAAC;QAC1C,CAAC;QACD,KAAK,IAAI,CAAC,CAAC;QACX,IAAI,KAAK,IAAI,EAAE,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,+CAA+C,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,OAAO,CAAC,KAAa;IACnC,OAAO,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AAC1B,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,KAAa;IAChD,OAAO,OAAO,CAAC,CAAC,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,CAAS,EAAE,CAAS;IAC9C,OAAO,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,MAAc,EACd,MAAc,EACd,KAAa;IAEb,MAAM,CAAC,YAAY,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,MAAc,EAAE,MAAc;IACxD,OAAO,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;AACpC,CAAC"}
@@ -0,0 +1,117 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { EventEmitter } from "node:events";
3
+ declare const LOG_LEVEL_ORDER: {
4
+ readonly debug: 0;
5
+ readonly info: 1;
6
+ readonly warn: 2;
7
+ readonly error: 3;
8
+ readonly silent: 4;
9
+ };
10
+ export type LogLevel = keyof typeof LOG_LEVEL_ORDER;
11
+ export interface Vector3 {
12
+ x: number;
13
+ y: number;
14
+ z: number;
15
+ }
16
+ export interface PlayerState {
17
+ position: Vector3;
18
+ velocity: Vector3;
19
+ rotation: Vector3;
20
+ }
21
+ export interface ProtocolEvent {
22
+ channelId: number;
23
+ protocolId: number;
24
+ payload: Buffer;
25
+ }
26
+ export interface CubyzConnectionLogger {
27
+ info?: (...args: unknown[]) => void;
28
+ warn?: (...args: unknown[]) => void;
29
+ error?: (...args: unknown[]) => void;
30
+ debug?: (...args: unknown[]) => void;
31
+ }
32
+ export interface CubyzConnectionOptions {
33
+ host: string;
34
+ port: number;
35
+ name: string;
36
+ version?: string;
37
+ logger?: CubyzConnectionLogger;
38
+ logLevel?: LogLevel;
39
+ }
40
+ export interface CloseOptions {
41
+ notify?: boolean;
42
+ }
43
+ type CubyzConnectionEvents = {
44
+ connected: [];
45
+ handshakeComplete: [string];
46
+ chat: [string];
47
+ players: [string[]];
48
+ protocol: [ProtocolEvent];
49
+ disconnect: [DisconnectEvent];
50
+ };
51
+ export interface DisconnectEvent {
52
+ reason: "server" | "timeout";
53
+ }
54
+ export declare class CubyzConnection extends EventEmitter {
55
+ readonly host: string;
56
+ readonly port: number;
57
+ readonly name: string;
58
+ readonly version: string;
59
+ private readonly baseLogger;
60
+ private readonly logLevel;
61
+ private readonly socket;
62
+ private readonly connectionId;
63
+ private remoteConnectionId;
64
+ private state;
65
+ private handshakeComplete;
66
+ private readonly sendChannels;
67
+ private readonly receiveChannels;
68
+ private readonly pendingConfirmations;
69
+ private readonly playerMap;
70
+ private lastKeepAliveSent;
71
+ private lastInbound;
72
+ private lastInitSent;
73
+ private tickTimer;
74
+ private playerStateTimer;
75
+ private readonly playerState;
76
+ private lastPlayerStateSent;
77
+ private disconnectSent;
78
+ private disconnectEmitted;
79
+ private initSent;
80
+ private handshakeQueued;
81
+ constructor({ host, port, name, version, logger, logLevel, }: CubyzConnectionOptions);
82
+ private log;
83
+ private emitDisconnect;
84
+ on<K extends keyof CubyzConnectionEvents>(event: K, listener: (...args: CubyzConnectionEvents[K]) => void): this;
85
+ once<K extends keyof CubyzConnectionEvents>(event: K, listener: (...args: CubyzConnectionEvents[K]) => void): this;
86
+ off<K extends keyof CubyzConnectionEvents>(event: K, listener: (...args: CubyzConnectionEvents[K]) => void): this;
87
+ emit<K extends keyof CubyzConnectionEvents>(event: K, ...args: CubyzConnectionEvents[K]): boolean;
88
+ start(): Promise<void>;
89
+ close(options?: CloseOptions): void;
90
+ private tick;
91
+ private flushSendQueues;
92
+ private queueConfirmation;
93
+ private flushConfirmations;
94
+ private sendKeepAlive;
95
+ private sendInit;
96
+ private sendInitAck;
97
+ private ensureReceiveChannels;
98
+ private handlePacket;
99
+ private handleInitPacket;
100
+ private queueHandshake;
101
+ private handleSequencedPacket;
102
+ private handleConfirmation;
103
+ private handleProtocol;
104
+ private handleHandshake;
105
+ private handleEntityUpdate;
106
+ private emitPlayers;
107
+ getPlayerNames(): string[];
108
+ sendChat(message: string): void;
109
+ teleport(x: number, y: number, z: number): void;
110
+ setRotation(yawDeg: number, pitchDeg?: number, rollDeg?: number): void;
111
+ setPosition(x: number, y: number, z: number): void;
112
+ publishPlayerState(force?: boolean): void;
113
+ private encodePlayerStatePacket;
114
+ private startPlayerStateLoop;
115
+ private sendDisconnectPacket;
116
+ }
117
+ export {};