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.
@@ -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