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 +211 -0
- package/dist/binary.d.ts +11 -0
- package/dist/binary.js +51 -0
- package/dist/binary.js.map +1 -0
- package/dist/connection.d.ts +117 -0
- package/dist/connection.js +675 -0
- package/dist/connection.js.map +1 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.js +31 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/receiveChannel.d.ts +34 -0
- package/dist/receiveChannel.js +155 -0
- package/dist/receiveChannel.js.map +1 -0
- package/dist/sendChannel.d.ts +22 -0
- package/dist/sendChannel.js +97 -0
- package/dist/sendChannel.js.map +1 -0
- package/dist/zon.d.ts +4 -0
- package/dist/zon.js +300 -0
- package/dist/zon.js.map +1 -0
- package/package.json +33 -0
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { randomInt } from "node:crypto";
|
|
3
|
+
import dgram from "node:dgram";
|
|
4
|
+
import { EventEmitter } from "node:events";
|
|
5
|
+
import { readInt32BE, writeInt32BE } from "./binary.js";
|
|
6
|
+
import { CHANNEL, CONFIRMATION_BATCH_SIZE, DEFAULT_VERSION, HANDSHAKE_STATE, INIT_RESEND_INTERVAL_MS, KEEP_ALIVE_INTERVAL_MS, KEEP_ALIVE_TIMEOUT_MS, PROTOCOL, } from "./constants.js";
|
|
7
|
+
import { parseChannelPacket, ReceiveChannel } from "./receiveChannel.js";
|
|
8
|
+
import { SendChannel } from "./sendChannel.js";
|
|
9
|
+
import { parseZon } from "./zon.js";
|
|
10
|
+
const DEG_TO_RAD = Math.PI / 180;
|
|
11
|
+
const LOG_LEVEL_ORDER = {
|
|
12
|
+
debug: 0,
|
|
13
|
+
info: 1,
|
|
14
|
+
warn: 2,
|
|
15
|
+
error: 3,
|
|
16
|
+
silent: 4,
|
|
17
|
+
};
|
|
18
|
+
function randomSequence() {
|
|
19
|
+
return randomInt(0, 0x7fffffff);
|
|
20
|
+
}
|
|
21
|
+
function escapeZonString(value) {
|
|
22
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
|
|
23
|
+
}
|
|
24
|
+
function buildHandshakePayload(name, version) {
|
|
25
|
+
const safeName = escapeZonString(name);
|
|
26
|
+
const safeVersion = escapeZonString(version);
|
|
27
|
+
const zon = `.{.version = "${safeVersion}", .name = "${safeName}"}`;
|
|
28
|
+
const prefix = Buffer.from([HANDSHAKE_STATE.USER_DATA]);
|
|
29
|
+
return Buffer.concat([prefix, Buffer.from(zon, "utf8")]);
|
|
30
|
+
}
|
|
31
|
+
function parseHandshake(payload) {
|
|
32
|
+
if (!payload || payload.length === 0) {
|
|
33
|
+
throw new Error("Handshake payload empty");
|
|
34
|
+
}
|
|
35
|
+
const state = payload[0];
|
|
36
|
+
return { state, data: payload.slice(1) };
|
|
37
|
+
}
|
|
38
|
+
export class CubyzConnection extends EventEmitter {
|
|
39
|
+
host;
|
|
40
|
+
port;
|
|
41
|
+
name;
|
|
42
|
+
version;
|
|
43
|
+
baseLogger;
|
|
44
|
+
logLevel;
|
|
45
|
+
socket;
|
|
46
|
+
connectionId;
|
|
47
|
+
remoteConnectionId = null;
|
|
48
|
+
state = "awaitingServer";
|
|
49
|
+
handshakeComplete = false;
|
|
50
|
+
sendChannels;
|
|
51
|
+
receiveChannels = new Map();
|
|
52
|
+
pendingConfirmations = [];
|
|
53
|
+
playerMap = new Map();
|
|
54
|
+
lastKeepAliveSent = Date.now();
|
|
55
|
+
lastInbound = Date.now();
|
|
56
|
+
lastInitSent = 0;
|
|
57
|
+
tickTimer = null;
|
|
58
|
+
playerStateTimer = null;
|
|
59
|
+
playerState = {
|
|
60
|
+
position: { x: 0, y: 0, z: 0 },
|
|
61
|
+
velocity: { x: 0, y: 0, z: 0 },
|
|
62
|
+
rotation: { x: 0, y: 0, z: 0 },
|
|
63
|
+
};
|
|
64
|
+
lastPlayerStateSent = 0;
|
|
65
|
+
disconnectSent = false;
|
|
66
|
+
disconnectEmitted = false;
|
|
67
|
+
initSent = false;
|
|
68
|
+
handshakeQueued = false;
|
|
69
|
+
constructor({ host, port, name, version = DEFAULT_VERSION, logger = console, logLevel = "error", }) {
|
|
70
|
+
super();
|
|
71
|
+
this.host = host;
|
|
72
|
+
this.port = port;
|
|
73
|
+
this.name = name;
|
|
74
|
+
this.version = version;
|
|
75
|
+
this.baseLogger = logger ?? console;
|
|
76
|
+
this.logLevel = (logLevel in LOG_LEVEL_ORDER ? logLevel : "error");
|
|
77
|
+
this.socket = dgram.createSocket("udp4");
|
|
78
|
+
this.connectionId = BigInt.asIntN(64, (BigInt(Date.now()) << 20n) | BigInt(randomInt(0, 0xfffff)));
|
|
79
|
+
this.sendChannels = {
|
|
80
|
+
[CHANNEL.LOSSY]: new SendChannel(CHANNEL.LOSSY, randomSequence()),
|
|
81
|
+
[CHANNEL.FAST]: new SendChannel(CHANNEL.FAST, randomSequence()),
|
|
82
|
+
[CHANNEL.SLOW]: new SendChannel(CHANNEL.SLOW, randomSequence()),
|
|
83
|
+
};
|
|
84
|
+
this.socket.on("message", (msg) => {
|
|
85
|
+
try {
|
|
86
|
+
const maybePromise = this.handlePacket(msg);
|
|
87
|
+
if (maybePromise instanceof Promise) {
|
|
88
|
+
maybePromise.catch((err) => {
|
|
89
|
+
this.log("error", "Failed to process packet:", err);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
this.log("error", "Failed to process packet:", err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.socket.on("error", (err) => {
|
|
98
|
+
this.log("error", "Socket error:", err);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
log(level, ...args) {
|
|
102
|
+
if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[this.logLevel]) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (level === "silent") {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const method = level === "debug"
|
|
109
|
+
? this.baseLogger.debug
|
|
110
|
+
: level === "info"
|
|
111
|
+
? this.baseLogger.info
|
|
112
|
+
: level === "warn"
|
|
113
|
+
? this.baseLogger.warn
|
|
114
|
+
: this.baseLogger.error;
|
|
115
|
+
method?.(...args);
|
|
116
|
+
}
|
|
117
|
+
emitDisconnect(reason) {
|
|
118
|
+
if (this.disconnectEmitted) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.disconnectEmitted = true;
|
|
122
|
+
this.emit("disconnect", { reason });
|
|
123
|
+
}
|
|
124
|
+
on(event, listener) {
|
|
125
|
+
return super.on(event, listener);
|
|
126
|
+
}
|
|
127
|
+
once(event, listener) {
|
|
128
|
+
return super.once(event, listener);
|
|
129
|
+
}
|
|
130
|
+
off(event, listener) {
|
|
131
|
+
return super.off(event, listener);
|
|
132
|
+
}
|
|
133
|
+
emit(event, ...args) {
|
|
134
|
+
return super.emit(event, ...args);
|
|
135
|
+
}
|
|
136
|
+
async start() {
|
|
137
|
+
await new Promise((resolve, reject) => {
|
|
138
|
+
const onError = (err) => {
|
|
139
|
+
this.socket.off("listening", onListening);
|
|
140
|
+
reject(err);
|
|
141
|
+
};
|
|
142
|
+
const onListening = () => {
|
|
143
|
+
this.socket.off("error", onError);
|
|
144
|
+
resolve();
|
|
145
|
+
};
|
|
146
|
+
this.socket.once("error", onError);
|
|
147
|
+
this.socket.once("listening", onListening);
|
|
148
|
+
this.socket.bind(0);
|
|
149
|
+
});
|
|
150
|
+
const address = this.socket.address();
|
|
151
|
+
this.log("info", `UDP socket bound on ${address.address}:${address.port}`);
|
|
152
|
+
this.tickTimer = setInterval(() => this.tick(), 20);
|
|
153
|
+
this.sendInit();
|
|
154
|
+
}
|
|
155
|
+
close(options = {}) {
|
|
156
|
+
const { notify = true } = options;
|
|
157
|
+
if (this.state === "closed" || this.state === "closing") {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.state = "closing";
|
|
161
|
+
const finalize = () => {
|
|
162
|
+
if (this.state === "closed") {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (this.tickTimer !== null) {
|
|
166
|
+
clearInterval(this.tickTimer);
|
|
167
|
+
this.tickTimer = null;
|
|
168
|
+
}
|
|
169
|
+
if (this.playerStateTimer !== null) {
|
|
170
|
+
clearInterval(this.playerStateTimer);
|
|
171
|
+
this.playerStateTimer = null;
|
|
172
|
+
}
|
|
173
|
+
this.state = "closed";
|
|
174
|
+
this.socket.close();
|
|
175
|
+
};
|
|
176
|
+
if (notify) {
|
|
177
|
+
this.sendDisconnectPacket(finalize);
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
finalize();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
tick() {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
if (this.state === "awaitingServer" &&
|
|
186
|
+
(!this.initSent || now - this.lastInitSent >= INIT_RESEND_INTERVAL_MS)) {
|
|
187
|
+
this.sendInit();
|
|
188
|
+
}
|
|
189
|
+
if (this.state === "connected" &&
|
|
190
|
+
now - this.lastInbound >= KEEP_ALIVE_TIMEOUT_MS) {
|
|
191
|
+
this.log("warn", "Connection timed out due to inactivity");
|
|
192
|
+
this.emitDisconnect("timeout");
|
|
193
|
+
this.close({ notify: false });
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (now - this.lastKeepAliveSent >= KEEP_ALIVE_INTERVAL_MS) {
|
|
197
|
+
this.sendKeepAlive();
|
|
198
|
+
}
|
|
199
|
+
this.flushConfirmations();
|
|
200
|
+
this.flushSendQueues(now);
|
|
201
|
+
}
|
|
202
|
+
flushSendQueues(now) {
|
|
203
|
+
for (const channel of Object.values(this.sendChannels)) {
|
|
204
|
+
if (!channel.hasWork()) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const packet = channel.getPacket(now);
|
|
208
|
+
if (!packet) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const buffer = Buffer.alloc(5 + packet.payload.length);
|
|
212
|
+
buffer[0] = channel.channelId;
|
|
213
|
+
writeInt32BE(buffer, 1, packet.start);
|
|
214
|
+
packet.payload.copy(buffer, 5);
|
|
215
|
+
this.socket.send(buffer, this.port, this.host);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
queueConfirmation(channelId, start) {
|
|
219
|
+
this.pendingConfirmations.push({ channelId, start, timestamp: Date.now() });
|
|
220
|
+
}
|
|
221
|
+
flushConfirmations() {
|
|
222
|
+
if (this.pendingConfirmations.length === 0) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const batch = this.pendingConfirmations.splice(0, CONFIRMATION_BATCH_SIZE);
|
|
226
|
+
const buffer = Buffer.alloc(1 + batch.length * (1 + 2 + 4));
|
|
227
|
+
buffer[0] = CHANNEL.CONFIRMATION;
|
|
228
|
+
let offset = 1;
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
for (const entry of batch) {
|
|
231
|
+
buffer[offset] = entry.channelId;
|
|
232
|
+
offset += 1;
|
|
233
|
+
const dt = now - entry.timestamp;
|
|
234
|
+
const half = Math.max(0, Math.min(0xffff, Math.floor(dt / 2)));
|
|
235
|
+
buffer.writeUInt16BE(half, offset);
|
|
236
|
+
offset += 2;
|
|
237
|
+
writeInt32BE(buffer, offset, entry.start);
|
|
238
|
+
offset += 4;
|
|
239
|
+
}
|
|
240
|
+
this.socket.send(buffer, this.port, this.host);
|
|
241
|
+
}
|
|
242
|
+
sendKeepAlive() {
|
|
243
|
+
this.lastKeepAliveSent = Date.now();
|
|
244
|
+
const packet = Buffer.from([CHANNEL.KEEP_ALIVE]);
|
|
245
|
+
this.socket.send(packet, this.port, this.host);
|
|
246
|
+
}
|
|
247
|
+
sendInit() {
|
|
248
|
+
this.lastInitSent = Date.now();
|
|
249
|
+
const payload = Buffer.alloc(1 + 8 + 12);
|
|
250
|
+
payload[0] = CHANNEL.INIT;
|
|
251
|
+
payload.writeBigInt64BE(this.connectionId, 1);
|
|
252
|
+
writeInt32BE(payload, 9, this.sendChannels[CHANNEL.LOSSY].initialSequence);
|
|
253
|
+
writeInt32BE(payload, 13, this.sendChannels[CHANNEL.FAST].initialSequence);
|
|
254
|
+
writeInt32BE(payload, 17, this.sendChannels[CHANNEL.SLOW].initialSequence);
|
|
255
|
+
this.socket.send(payload, this.port, this.host);
|
|
256
|
+
this.initSent = true;
|
|
257
|
+
}
|
|
258
|
+
sendInitAck() {
|
|
259
|
+
const buffer = Buffer.alloc(1 + 8);
|
|
260
|
+
buffer[0] = CHANNEL.INIT;
|
|
261
|
+
buffer.writeBigInt64BE(this.connectionId, 1);
|
|
262
|
+
this.socket.send(buffer, this.port, this.host);
|
|
263
|
+
}
|
|
264
|
+
ensureReceiveChannels(lossyStart, fastStart, slowStart) {
|
|
265
|
+
if (!this.receiveChannels.has(CHANNEL.LOSSY)) {
|
|
266
|
+
this.receiveChannels.set(CHANNEL.LOSSY, new ReceiveChannel(CHANNEL.LOSSY, lossyStart));
|
|
267
|
+
}
|
|
268
|
+
if (!this.receiveChannels.has(CHANNEL.FAST)) {
|
|
269
|
+
this.receiveChannels.set(CHANNEL.FAST, new ReceiveChannel(CHANNEL.FAST, fastStart));
|
|
270
|
+
}
|
|
271
|
+
if (!this.receiveChannels.has(CHANNEL.SLOW)) {
|
|
272
|
+
this.receiveChannels.set(CHANNEL.SLOW, new ReceiveChannel(CHANNEL.SLOW, slowStart));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
handlePacket(buffer) {
|
|
276
|
+
if (!buffer || buffer.length === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const channelId = buffer[0];
|
|
280
|
+
this.lastInbound = Date.now();
|
|
281
|
+
switch (channelId) {
|
|
282
|
+
case CHANNEL.INIT:
|
|
283
|
+
this.handleInitPacket(buffer);
|
|
284
|
+
break;
|
|
285
|
+
case CHANNEL.CONFIRMATION:
|
|
286
|
+
this.handleConfirmation(buffer.slice(1));
|
|
287
|
+
break;
|
|
288
|
+
case CHANNEL.KEEP_ALIVE:
|
|
289
|
+
break;
|
|
290
|
+
case CHANNEL.DISCONNECT:
|
|
291
|
+
this.log("warn", "Server requested disconnect");
|
|
292
|
+
this.emitDisconnect("server");
|
|
293
|
+
this.close({ notify: false });
|
|
294
|
+
break;
|
|
295
|
+
default:
|
|
296
|
+
return this.handleSequencedPacket(buffer);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
handleInitPacket(buffer) {
|
|
300
|
+
if (buffer.length === 1 + 8) {
|
|
301
|
+
const remoteId = buffer.readBigInt64BE(1);
|
|
302
|
+
if (this.remoteConnectionId === null && remoteId === this.connectionId) {
|
|
303
|
+
this.log("debug", "Server acknowledged init");
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (buffer.length < 1 + 8 + 12) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const remoteId = buffer.readBigInt64BE(1);
|
|
311
|
+
this.remoteConnectionId = remoteId;
|
|
312
|
+
const lossyStart = readInt32BE(buffer, 9);
|
|
313
|
+
const fastStart = readInt32BE(buffer, 13);
|
|
314
|
+
const slowStart = readInt32BE(buffer, 17);
|
|
315
|
+
this.ensureReceiveChannels(lossyStart, fastStart, slowStart);
|
|
316
|
+
if (this.state !== "connected") {
|
|
317
|
+
this.state = "connected";
|
|
318
|
+
this.lastInbound = Date.now();
|
|
319
|
+
this.log("info", "Channel handshake completed with server");
|
|
320
|
+
this.sendInitAck();
|
|
321
|
+
this.queueHandshake();
|
|
322
|
+
this.emit("connected");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
queueHandshake() {
|
|
326
|
+
if (this.handshakeQueued) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const payload = buildHandshakePayload(this.name, this.version);
|
|
330
|
+
this.sendChannels[CHANNEL.FAST].queue(PROTOCOL.HANDSHAKE, payload);
|
|
331
|
+
this.handshakeQueued = true;
|
|
332
|
+
}
|
|
333
|
+
async handleSequencedPacket(buffer) {
|
|
334
|
+
const parsed = parseChannelPacket(buffer);
|
|
335
|
+
const channel = this.receiveChannels.get(parsed.channelId);
|
|
336
|
+
if (!channel) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const result = channel.handlePacket(parsed.start, parsed.payload);
|
|
340
|
+
if (!result.accepted) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
this.queueConfirmation(parsed.channelId, result.ackStart);
|
|
344
|
+
for (const message of result.messages) {
|
|
345
|
+
try {
|
|
346
|
+
await this.handleProtocol(parsed.channelId, message.protocolId, message.payload);
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
this.log("error", `Protocol ${message.protocolId} failed:`, err);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
handleConfirmation(buffer) {
|
|
354
|
+
let offset = 0;
|
|
355
|
+
while (offset + 7 <= buffer.length) {
|
|
356
|
+
const channelId = buffer[offset];
|
|
357
|
+
offset += 1;
|
|
358
|
+
offset += 2;
|
|
359
|
+
const start = buffer.readInt32BE(offset);
|
|
360
|
+
offset += 4;
|
|
361
|
+
const channel = this.sendChannels[channelId];
|
|
362
|
+
if (channel) {
|
|
363
|
+
channel.handleAck(start);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async handleProtocol(channelId, protocolId, payload) {
|
|
368
|
+
switch (protocolId) {
|
|
369
|
+
case PROTOCOL.HANDSHAKE:
|
|
370
|
+
await this.handleHandshake(payload);
|
|
371
|
+
break;
|
|
372
|
+
case PROTOCOL.ENTITY:
|
|
373
|
+
this.handleEntityUpdate(payload);
|
|
374
|
+
this.emit("protocol", { channelId, protocolId, payload });
|
|
375
|
+
break;
|
|
376
|
+
case PROTOCOL.CHAT:
|
|
377
|
+
this.emit("chat", payload.toString("utf8"));
|
|
378
|
+
break;
|
|
379
|
+
default:
|
|
380
|
+
this.emit("protocol", { channelId, protocolId, payload });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async handleHandshake(payload) {
|
|
384
|
+
const { state, data } = parseHandshake(payload);
|
|
385
|
+
switch (state) {
|
|
386
|
+
case HANDSHAKE_STATE.ASSETS: {
|
|
387
|
+
// Assets are compressed with zlib's raw DEFLATE
|
|
388
|
+
// Skipping asset storage for brevity
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
case HANDSHAKE_STATE.SERVER_DATA: {
|
|
392
|
+
this.handshakeComplete = true;
|
|
393
|
+
const zonText = data.toString("utf8");
|
|
394
|
+
let selfInserted = false;
|
|
395
|
+
try {
|
|
396
|
+
const parsed = parseZon(zonText);
|
|
397
|
+
const playerId = typeof parsed === "object" && parsed !== null
|
|
398
|
+
? parsed.player_id
|
|
399
|
+
: null;
|
|
400
|
+
if (typeof playerId === "number") {
|
|
401
|
+
this.playerMap.set(playerId, this.name);
|
|
402
|
+
selfInserted = true;
|
|
403
|
+
}
|
|
404
|
+
const playerData = typeof parsed === "object" && parsed !== null
|
|
405
|
+
? parsed.player
|
|
406
|
+
: null;
|
|
407
|
+
if (playerData && typeof playerData === "object") {
|
|
408
|
+
const position = Array.isArray(playerData.position)
|
|
409
|
+
? playerData
|
|
410
|
+
.position
|
|
411
|
+
: null;
|
|
412
|
+
if (position && position.length >= 3) {
|
|
413
|
+
this.playerState.position = {
|
|
414
|
+
x: Number(position[0]) || 0,
|
|
415
|
+
y: Number(position[1]) || 0,
|
|
416
|
+
z: Number(position[2]) || 0,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
else if (Array.isArray(parsed.spawn) &&
|
|
420
|
+
parsed.spawn
|
|
421
|
+
.length >= 3) {
|
|
422
|
+
const spawn = parsed
|
|
423
|
+
.spawn;
|
|
424
|
+
this.playerState.position = {
|
|
425
|
+
x: Number(spawn[0]) || 0,
|
|
426
|
+
y: Number(spawn[1]) || 0,
|
|
427
|
+
z: Number(spawn[2]) || 0,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
const velocity = Array.isArray(playerData.velocity)
|
|
431
|
+
? playerData
|
|
432
|
+
.velocity
|
|
433
|
+
: null;
|
|
434
|
+
if (velocity && velocity.length >= 3) {
|
|
435
|
+
this.playerState.velocity = {
|
|
436
|
+
x: Number(velocity[0]) || 0,
|
|
437
|
+
y: Number(velocity[1]) || 0,
|
|
438
|
+
z: Number(velocity[2]) || 0,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const rotation = Array.isArray(playerData.rotation)
|
|
442
|
+
? playerData
|
|
443
|
+
.rotation
|
|
444
|
+
: null;
|
|
445
|
+
if (rotation && rotation.length >= 3) {
|
|
446
|
+
this.playerState.rotation = {
|
|
447
|
+
x: Number(rotation[0]) || 0,
|
|
448
|
+
y: Number(rotation[1]) || 0,
|
|
449
|
+
z: Number(rotation[2]) || 0,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
else if (parsed &&
|
|
454
|
+
typeof parsed === "object" &&
|
|
455
|
+
Array.isArray(parsed.spawn)) {
|
|
456
|
+
const spawn = parsed
|
|
457
|
+
.spawn;
|
|
458
|
+
if (spawn.length >= 3) {
|
|
459
|
+
this.playerState.position = {
|
|
460
|
+
x: Number(spawn[0]) || 0,
|
|
461
|
+
y: Number(spawn[1]) || 0,
|
|
462
|
+
z: Number(spawn[2]) || 0,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
catch (err) {
|
|
468
|
+
this.log("warn", "Failed to parse server data handshake:", err);
|
|
469
|
+
}
|
|
470
|
+
if (!selfInserted && !this.playerMap.has(`self:${this.name}`)) {
|
|
471
|
+
this.playerMap.set(`self:${this.name}`, this.name);
|
|
472
|
+
}
|
|
473
|
+
this.emitPlayers();
|
|
474
|
+
this.startPlayerStateLoop();
|
|
475
|
+
this.publishPlayerState(true);
|
|
476
|
+
this.emit("handshakeComplete", zonText);
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case HANDSHAKE_STATE.USER_DATA:
|
|
480
|
+
this.log("debug", "Server echoed user data");
|
|
481
|
+
break;
|
|
482
|
+
default:
|
|
483
|
+
this.log("debug", `Unhandled handshake state ${state}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
handleEntityUpdate(payload) {
|
|
487
|
+
const text = payload.toString("utf8");
|
|
488
|
+
let parsed;
|
|
489
|
+
try {
|
|
490
|
+
parsed = parseZon(text);
|
|
491
|
+
this.log("debug", "Entity payload parsed successfully:", parsed);
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
this.log("warn", "Failed to parse entity payload:", err);
|
|
495
|
+
this.log("debug", "Entity payload raw:", text);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!Array.isArray(parsed)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
let changed = false;
|
|
502
|
+
for (const entry of parsed) {
|
|
503
|
+
if (entry === null) {
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
if (typeof entry === "number") {
|
|
507
|
+
if (this.playerMap.delete(entry)) {
|
|
508
|
+
changed = true;
|
|
509
|
+
}
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (entry &&
|
|
513
|
+
typeof entry === "object" &&
|
|
514
|
+
typeof entry.id === "number") {
|
|
515
|
+
const incomingName = typeof entry.name === "string"
|
|
516
|
+
? entry.name
|
|
517
|
+
: null;
|
|
518
|
+
const id = entry.id;
|
|
519
|
+
const previous = this.playerMap.get(id);
|
|
520
|
+
if (incomingName !== previous) {
|
|
521
|
+
this.playerMap.set(id, incomingName);
|
|
522
|
+
changed = true;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (changed) {
|
|
527
|
+
if (this.playerMap.has(`self:${this.name}`)) {
|
|
528
|
+
const placeholderName = this.playerMap.get(`self:${this.name}`);
|
|
529
|
+
if (typeof placeholderName === "string") {
|
|
530
|
+
for (const [key, value] of this.playerMap) {
|
|
531
|
+
if (key === `self:${this.name}`) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (value === placeholderName) {
|
|
535
|
+
this.playerMap.delete(`self:${this.name}`);
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
this.playerMap.delete(`self:${this.name}`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
this.emitPlayers();
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
emitPlayers() {
|
|
548
|
+
const players = this.getPlayerNames();
|
|
549
|
+
this.emit("players", players);
|
|
550
|
+
}
|
|
551
|
+
getPlayerNames() {
|
|
552
|
+
const names = [];
|
|
553
|
+
for (const value of this.playerMap.values()) {
|
|
554
|
+
if (typeof value === "string" && value.length > 0) {
|
|
555
|
+
names.push(value);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return names;
|
|
559
|
+
}
|
|
560
|
+
sendChat(message) {
|
|
561
|
+
const payload = Buffer.from(message, "utf8");
|
|
562
|
+
this.sendChannels[CHANNEL.LOSSY].queue(PROTOCOL.CHAT, payload);
|
|
563
|
+
}
|
|
564
|
+
teleport(x, y, z) {
|
|
565
|
+
const coords = [x, y, z].map((value) => Number(value));
|
|
566
|
+
if (coords.some((value) => Number.isNaN(value) || !Number.isFinite(value))) {
|
|
567
|
+
this.log("warn", "Ignoring teleport with invalid coordinates", {
|
|
568
|
+
x,
|
|
569
|
+
y,
|
|
570
|
+
z,
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
this.log("info", "Updating position via state packet", {
|
|
575
|
+
x: coords[0],
|
|
576
|
+
y: coords[1],
|
|
577
|
+
z: coords[2],
|
|
578
|
+
});
|
|
579
|
+
this.setPosition(coords[0], coords[1], coords[2]);
|
|
580
|
+
}
|
|
581
|
+
setRotation(yawDeg, pitchDeg = 0, rollDeg = 0) {
|
|
582
|
+
const yaw = Number(yawDeg);
|
|
583
|
+
const pitch = Number(pitchDeg);
|
|
584
|
+
const roll = Number(rollDeg);
|
|
585
|
+
if ([yaw, pitch, roll].some((value) => Number.isNaN(value) || !Number.isFinite(value))) {
|
|
586
|
+
this.log("warn", "Ignoring rotation with invalid values", {
|
|
587
|
+
yaw: yawDeg,
|
|
588
|
+
pitch: pitchDeg,
|
|
589
|
+
roll: rollDeg,
|
|
590
|
+
});
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
this.playerState.rotation = {
|
|
594
|
+
x: pitch * DEG_TO_RAD,
|
|
595
|
+
y: roll * DEG_TO_RAD,
|
|
596
|
+
z: yaw * DEG_TO_RAD,
|
|
597
|
+
};
|
|
598
|
+
this.log("info", "Updated rotation", {
|
|
599
|
+
yaw,
|
|
600
|
+
pitch,
|
|
601
|
+
roll,
|
|
602
|
+
mapping: "pitch→x, roll→y, yaw→z",
|
|
603
|
+
});
|
|
604
|
+
this.publishPlayerState(true);
|
|
605
|
+
}
|
|
606
|
+
setPosition(x, y, z) {
|
|
607
|
+
this.playerState.position = { x, y, z };
|
|
608
|
+
this.playerState.velocity = { x: 0, y: 0, z: 0 };
|
|
609
|
+
this.publishPlayerState(true);
|
|
610
|
+
}
|
|
611
|
+
publishPlayerState(force = false) {
|
|
612
|
+
if (!this.handshakeComplete) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const now = Date.now();
|
|
616
|
+
if (!force && now - this.lastPlayerStateSent < 50) {
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
this.lastPlayerStateSent = now;
|
|
620
|
+
const payload = this.encodePlayerStatePacket(this.playerState);
|
|
621
|
+
this.sendChannels[CHANNEL.LOSSY].queue(PROTOCOL.PLAYER_STATE, payload);
|
|
622
|
+
}
|
|
623
|
+
encodePlayerStatePacket(state) {
|
|
624
|
+
const buffer = Buffer.alloc(62);
|
|
625
|
+
let offset = 0;
|
|
626
|
+
const writeDouble = (value) => {
|
|
627
|
+
buffer.writeDoubleBE(Number.isFinite(value) ? value : 0, offset);
|
|
628
|
+
offset += 8;
|
|
629
|
+
};
|
|
630
|
+
const writeFloat = (value) => {
|
|
631
|
+
buffer.writeFloatBE(Number.isFinite(value) ? value : 0, offset);
|
|
632
|
+
offset += 4;
|
|
633
|
+
};
|
|
634
|
+
writeDouble(state.position.x ?? 0);
|
|
635
|
+
writeDouble(state.position.y ?? 0);
|
|
636
|
+
writeDouble(state.position.z ?? 0);
|
|
637
|
+
writeDouble(state.velocity.x ?? 0);
|
|
638
|
+
writeDouble(state.velocity.y ?? 0);
|
|
639
|
+
writeDouble(state.velocity.z ?? 0);
|
|
640
|
+
writeFloat(state.rotation.x ?? 0);
|
|
641
|
+
writeFloat(state.rotation.y ?? 0);
|
|
642
|
+
writeFloat(state.rotation.z ?? 0);
|
|
643
|
+
buffer.writeUInt16BE(Date.now() & 0xffff, offset);
|
|
644
|
+
return buffer;
|
|
645
|
+
}
|
|
646
|
+
startPlayerStateLoop() {
|
|
647
|
+
if (this.playerStateTimer !== null) {
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
this.playerStateTimer = setInterval(() => {
|
|
651
|
+
this.publishPlayerState();
|
|
652
|
+
}, 100);
|
|
653
|
+
}
|
|
654
|
+
sendDisconnectPacket(done) {
|
|
655
|
+
if (this.disconnectSent) {
|
|
656
|
+
done?.();
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
this.disconnectSent = true;
|
|
660
|
+
const buffer = Buffer.from([CHANNEL.DISCONNECT]);
|
|
661
|
+
try {
|
|
662
|
+
this.socket.send(buffer, this.port, this.host, (err) => {
|
|
663
|
+
if (err) {
|
|
664
|
+
this.log("warn", "Failed to send disconnect packet:", err);
|
|
665
|
+
}
|
|
666
|
+
done?.();
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
catch (err) {
|
|
670
|
+
this.log("warn", "Failed to queue disconnect packet:", err);
|
|
671
|
+
done?.();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
//# sourceMappingURL=connection.js.map
|