cubyz-node-client 1.1.0 → 1.3.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/LICENSE +21 -0
- package/README.md +19 -1
- package/dist/authentication.d.ts +20 -0
- package/dist/authentication.js +150 -0
- package/dist/authentication.js.map +1 -0
- package/dist/binary.js +1 -1
- package/dist/binary.js.map +1 -1
- package/dist/connection.d.ts +10 -4
- package/dist/connection.js +360 -30
- package/dist/connection.js.map +1 -1
- package/dist/connectionTypes.d.ts +64 -0
- package/dist/connectionTypes.js +18 -0
- package/dist/connectionTypes.js.map +1 -1
- package/dist/constants.d.ts +10 -6
- package/dist/constants.js +9 -5
- package/dist/constants.js.map +1 -1
- package/dist/entityParser.d.ts +10 -2
- package/dist/entityParser.js +2 -2
- package/dist/entityParser.js.map +1 -1
- package/dist/handshakeUtils.d.ts +2 -1
- package/dist/handshakeUtils.js +10 -2
- package/dist/handshakeUtils.js.map +1 -1
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -5
- package/dist/index.js.map +1 -1
- package/dist/receiveChannel.d.ts +2 -0
- package/dist/receiveChannel.js +20 -0
- package/dist/receiveChannel.js.map +1 -1
- package/dist/secureChannel.d.ts +91 -0
- package/dist/secureChannel.js +722 -0
- package/dist/secureChannel.js.map +1 -0
- package/dist/sendChannel.d.ts +1 -0
- package/dist/sendChannel.js +4 -1
- package/dist/sendChannel.js.map +1 -1
- package/dist/wordlist.d.ts +1 -0
- package/dist/wordlist.js +2051 -0
- package/dist/wordlist.js.map +1 -0
- package/package.json +4 -1
package/dist/connection.js
CHANGED
|
@@ -2,15 +2,18 @@ import { Buffer } from "node:buffer";
|
|
|
2
2
|
import { randomInt } from "node:crypto";
|
|
3
3
|
import dgram from "node:dgram";
|
|
4
4
|
import { EventEmitter } from "node:events";
|
|
5
|
-
import {
|
|
5
|
+
import { loadOrCreateIdentity, signEd25519, signMlDsa44, signP256, } from "./authentication.js";
|
|
6
|
+
import { decodeVarInt, readInt32BE, writeInt32BE } from "./binary.js";
|
|
6
7
|
import { prepareChatMessage } from "./chatFormat.js";
|
|
7
|
-
import { DEG_TO_RAD, LOG_LEVEL_ORDER, } from "./connectionTypes.js";
|
|
8
|
+
import { DEG_TO_RAD, GENERIC_UPDATE_TYPE, LOG_LEVEL_ORDER, WORLD_EDIT_POSITION, } from "./connectionTypes.js";
|
|
8
9
|
import { AWAITING_SERVER_TIMEOUT_MS, CHANNEL, CONFIRMATION_BATCH_SIZE, DEFAULT_VERSION, HANDSHAKE_STATE, INIT_RESEND_INTERVAL_MS, KEEP_ALIVE_INTERVAL_MS, KEEP_ALIVE_TIMEOUT_MS, PROTOCOL, } from "./constants.js";
|
|
9
10
|
import { parseEntityPositionPacket } from "./entityParser.js";
|
|
10
11
|
import { buildHandshakePayload, parseHandshake, randomSequence, } from "./handshakeUtils.js";
|
|
11
12
|
import { parseChannelPacket, ReceiveChannel } from "./receiveChannel.js";
|
|
13
|
+
import { SecureChannelHandler } from "./secureChannel.js";
|
|
12
14
|
import { SendChannel } from "./sendChannel.js";
|
|
13
15
|
import { parseZon } from "./zon.js";
|
|
16
|
+
export { GAMEMODE } from "./connectionTypes.js";
|
|
14
17
|
export class CubyzConnection extends EventEmitter {
|
|
15
18
|
host;
|
|
16
19
|
port;
|
|
@@ -43,9 +46,11 @@ export class CubyzConnection extends EventEmitter {
|
|
|
43
46
|
disconnectSent = false;
|
|
44
47
|
disconnectEmitted = false;
|
|
45
48
|
initSent = false;
|
|
46
|
-
handshakeQueued = false;
|
|
47
49
|
awaitingServerSince = null;
|
|
48
|
-
|
|
50
|
+
identityFile;
|
|
51
|
+
identity = null;
|
|
52
|
+
secureChannel = null;
|
|
53
|
+
constructor({ host, port, name, version = DEFAULT_VERSION, logger = console, logLevel = "error", identityFile = "./cubyz-identity.txt", }) {
|
|
49
54
|
super();
|
|
50
55
|
this.host = host;
|
|
51
56
|
this.port = port;
|
|
@@ -53,11 +58,12 @@ export class CubyzConnection extends EventEmitter {
|
|
|
53
58
|
this.version = version;
|
|
54
59
|
this.baseLogger = logger ?? console;
|
|
55
60
|
this.logLevel = (logLevel in LOG_LEVEL_ORDER ? logLevel : "error");
|
|
61
|
+
this.identityFile = identityFile;
|
|
56
62
|
this.socket = dgram.createSocket("udp4");
|
|
57
63
|
this.connectionId = BigInt.asIntN(64, (BigInt(Date.now()) << 20n) | BigInt(randomInt(0, 0xfffff)));
|
|
58
64
|
this.sendChannels = {
|
|
59
65
|
[CHANNEL.LOSSY]: new SendChannel(CHANNEL.LOSSY, randomSequence()),
|
|
60
|
-
[CHANNEL.
|
|
66
|
+
[CHANNEL.SECURE]: new SendChannel(CHANNEL.SECURE, randomSequence()),
|
|
61
67
|
[CHANNEL.SLOW]: new SendChannel(CHANNEL.SLOW, randomSequence()),
|
|
62
68
|
};
|
|
63
69
|
this.socket.on("message", (msg) => {
|
|
@@ -113,6 +119,8 @@ export class CubyzConnection extends EventEmitter {
|
|
|
113
119
|
return super.emit(event, ...args);
|
|
114
120
|
}
|
|
115
121
|
async start() {
|
|
122
|
+
this.identity = await loadOrCreateIdentity(this.identityFile);
|
|
123
|
+
this.log("info", `Loaded identity from ${this.identityFile}`);
|
|
116
124
|
await new Promise((resolve, reject) => {
|
|
117
125
|
const onError = (err) => {
|
|
118
126
|
this.socket.off("listening", onListening);
|
|
@@ -192,6 +200,10 @@ export class CubyzConnection extends EventEmitter {
|
|
|
192
200
|
}
|
|
193
201
|
flushSendQueues(now) {
|
|
194
202
|
for (const channel of Object.values(this.sendChannels)) {
|
|
203
|
+
// The SECURE channel is driven by SecureChannelHandler; skip normal queuing.
|
|
204
|
+
if (channel.channelId === CHANNEL.SECURE) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
195
207
|
if (!channel.hasWork()) {
|
|
196
208
|
continue;
|
|
197
209
|
}
|
|
@@ -241,28 +253,69 @@ export class CubyzConnection extends EventEmitter {
|
|
|
241
253
|
payload[0] = CHANNEL.INIT;
|
|
242
254
|
payload.writeBigInt64BE(this.connectionId, 1);
|
|
243
255
|
writeInt32BE(payload, 9, this.sendChannels[CHANNEL.LOSSY].initialSequence);
|
|
244
|
-
writeInt32BE(payload, 13, this.sendChannels[CHANNEL.
|
|
256
|
+
writeInt32BE(payload, 13, this.sendChannels[CHANNEL.SECURE].initialSequence);
|
|
245
257
|
writeInt32BE(payload, 17, this.sendChannels[CHANNEL.SLOW].initialSequence);
|
|
246
258
|
this.socket.send(payload, this.port, this.host);
|
|
247
259
|
this.initSent = true;
|
|
248
260
|
}
|
|
249
|
-
sendInitAck() {
|
|
261
|
+
sendInitAck(onSent) {
|
|
250
262
|
const buffer = Buffer.alloc(1 + 8);
|
|
251
263
|
buffer[0] = CHANNEL.INIT;
|
|
252
264
|
buffer.writeBigInt64BE(this.connectionId, 1);
|
|
253
|
-
this.socket.send(buffer, this.port, this.host)
|
|
265
|
+
this.socket.send(buffer, this.port, this.host, (err) => {
|
|
266
|
+
if (err) {
|
|
267
|
+
this.log("error", "Failed to send init ack:", err);
|
|
268
|
+
}
|
|
269
|
+
onSent?.();
|
|
270
|
+
});
|
|
254
271
|
}
|
|
255
|
-
ensureReceiveChannels(lossyStart,
|
|
272
|
+
ensureReceiveChannels(lossyStart, secureStart, slowStart) {
|
|
256
273
|
if (!this.receiveChannels.has(CHANNEL.LOSSY)) {
|
|
257
274
|
this.receiveChannels.set(CHANNEL.LOSSY, new ReceiveChannel(CHANNEL.LOSSY, lossyStart));
|
|
258
275
|
}
|
|
259
|
-
if (!this.receiveChannels.has(CHANNEL.
|
|
260
|
-
|
|
276
|
+
if (!this.receiveChannels.has(CHANNEL.SECURE)) {
|
|
277
|
+
const secureRecv = new ReceiveChannel(CHANNEL.SECURE, secureStart);
|
|
278
|
+
// Route raw bytes from the SECURE channel into the TLS handler.
|
|
279
|
+
secureRecv.rawBytesCallback = (data) => {
|
|
280
|
+
this.secureChannel?.feedRawBytes(data);
|
|
281
|
+
};
|
|
282
|
+
this.receiveChannels.set(CHANNEL.SECURE, secureRecv);
|
|
261
283
|
}
|
|
262
284
|
if (!this.receiveChannels.has(CHANNEL.SLOW)) {
|
|
263
285
|
this.receiveChannels.set(CHANNEL.SLOW, new ReceiveChannel(CHANNEL.SLOW, slowStart));
|
|
264
286
|
}
|
|
265
287
|
}
|
|
288
|
+
setupSecureChannel() {
|
|
289
|
+
if (this.secureChannel !== null) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const handler = new SecureChannelHandler({
|
|
293
|
+
socket: this.socket,
|
|
294
|
+
host: this.host,
|
|
295
|
+
port: this.port,
|
|
296
|
+
channelId: CHANNEL.SECURE,
|
|
297
|
+
mtu: 548,
|
|
298
|
+
initialSendSeq: this.sendChannels[CHANNEL.SECURE].initialSequence,
|
|
299
|
+
});
|
|
300
|
+
handler.onError = (err) => {
|
|
301
|
+
this.log("error", "Secure channel error:", err);
|
|
302
|
+
};
|
|
303
|
+
handler.onSecureConnect = (_verificationData) => {
|
|
304
|
+
this.log("debug", "TLS handshake complete, sending userData");
|
|
305
|
+
if (!this.identity) {
|
|
306
|
+
this.log("error", "No identity loaded before TLS handshake completed");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const payload = buildHandshakePayload(this.name, this.version, this.identity);
|
|
310
|
+
handler.sendMessage(PROTOCOL.HANDSHAKE, payload);
|
|
311
|
+
};
|
|
312
|
+
handler.onMessage = (msg) => {
|
|
313
|
+
this.handleProtocol(CHANNEL.SECURE, msg.protocolId, msg.payload).catch((err) => {
|
|
314
|
+
this.log("error", `Secure protocol ${msg.protocolId} failed:`, err);
|
|
315
|
+
});
|
|
316
|
+
};
|
|
317
|
+
this.secureChannel = handler;
|
|
318
|
+
}
|
|
266
319
|
handlePacket(buffer) {
|
|
267
320
|
if (!buffer || buffer.length === 0) {
|
|
268
321
|
return;
|
|
@@ -301,27 +354,24 @@ export class CubyzConnection extends EventEmitter {
|
|
|
301
354
|
const remoteId = buffer.readBigInt64BE(1);
|
|
302
355
|
this.remoteConnectionId = remoteId;
|
|
303
356
|
const lossyStart = readInt32BE(buffer, 9);
|
|
304
|
-
const
|
|
357
|
+
const secureStart = readInt32BE(buffer, 13);
|
|
305
358
|
const slowStart = readInt32BE(buffer, 17);
|
|
306
|
-
this.ensureReceiveChannels(lossyStart,
|
|
359
|
+
this.ensureReceiveChannels(lossyStart, secureStart, slowStart);
|
|
307
360
|
if (this.state !== "connected") {
|
|
308
361
|
this.state = "connected";
|
|
309
362
|
this.lastInbound = Date.now();
|
|
310
363
|
this.awaitingServerSince = null;
|
|
311
364
|
this.log("info", "Channel handshake completed with server");
|
|
312
|
-
this.
|
|
313
|
-
|
|
365
|
+
this.setupSecureChannel();
|
|
366
|
+
// Send the init-ACK and only start the TLS handshake once the packet
|
|
367
|
+
// has been handed to the OS, guaranteeing the server transitions to
|
|
368
|
+
// .connected before it receives the TLS ClientHello.
|
|
369
|
+
this.sendInitAck(() => {
|
|
370
|
+
this.secureChannel?.startHandshake();
|
|
371
|
+
});
|
|
314
372
|
this.emit("connected");
|
|
315
373
|
}
|
|
316
374
|
}
|
|
317
|
-
queueHandshake() {
|
|
318
|
-
if (this.handshakeQueued) {
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
const payload = buildHandshakePayload(this.name, this.version);
|
|
322
|
-
this.sendChannels[CHANNEL.FAST].queue(PROTOCOL.HANDSHAKE, payload);
|
|
323
|
-
this.handshakeQueued = true;
|
|
324
|
-
}
|
|
325
375
|
async handleSequencedPacket(buffer) {
|
|
326
376
|
const parsed = parseChannelPacket(buffer);
|
|
327
377
|
const channel = this.receiveChannels.get(parsed.channelId);
|
|
@@ -357,6 +407,7 @@ export class CubyzConnection extends EventEmitter {
|
|
|
357
407
|
}
|
|
358
408
|
}
|
|
359
409
|
async handleProtocol(channelId, protocolId, payload) {
|
|
410
|
+
// this.log("debug", `handleProtocol: channelId=${channelId}, protocolId=${protocolId}, payloadLen=${payload.length}`);
|
|
360
411
|
switch (protocolId) {
|
|
361
412
|
case PROTOCOL.HANDSHAKE:
|
|
362
413
|
await this.handleHandshake(payload);
|
|
@@ -365,10 +416,18 @@ export class CubyzConnection extends EventEmitter {
|
|
|
365
416
|
this.handleEntityPosition(payload);
|
|
366
417
|
this.emit("protocol", { channelId, protocolId, payload });
|
|
367
418
|
break;
|
|
419
|
+
case PROTOCOL.BLOCK_UPDATE:
|
|
420
|
+
this.handleBlockUpdate(payload);
|
|
421
|
+
this.emit("protocol", { channelId, protocolId, payload });
|
|
422
|
+
break;
|
|
368
423
|
case PROTOCOL.ENTITY:
|
|
369
424
|
this.handleEntityUpdate(payload);
|
|
370
425
|
this.emit("protocol", { channelId, protocolId, payload });
|
|
371
426
|
break;
|
|
427
|
+
case PROTOCOL.GENERIC_UPDATE:
|
|
428
|
+
this.handleGenericUpdate(payload);
|
|
429
|
+
this.emit("protocol", { channelId, protocolId, payload });
|
|
430
|
+
break;
|
|
372
431
|
case PROTOCOL.CHAT:
|
|
373
432
|
this.emit("chat", payload.toString("utf8"));
|
|
374
433
|
break;
|
|
@@ -379,6 +438,10 @@ export class CubyzConnection extends EventEmitter {
|
|
|
379
438
|
async handleHandshake(payload) {
|
|
380
439
|
const { state, data } = parseHandshake(payload);
|
|
381
440
|
switch (state) {
|
|
441
|
+
case HANDSHAKE_STATE.SIGNATURE_REQUEST: {
|
|
442
|
+
await this.handleSignatureRequest(data);
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
382
445
|
case HANDSHAKE_STATE.ASSETS: {
|
|
383
446
|
// Assets are compressed with zlib's raw DEFLATE
|
|
384
447
|
// Skipping asset storage for brevity
|
|
@@ -479,6 +542,63 @@ export class CubyzConnection extends EventEmitter {
|
|
|
479
542
|
this.log("debug", `Unhandled handshake state ${state}`);
|
|
480
543
|
}
|
|
481
544
|
}
|
|
545
|
+
async handleSignatureRequest(data) {
|
|
546
|
+
if (!this.identity) {
|
|
547
|
+
this.log("error", "No identity available for signature request");
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
if (!this.secureChannel) {
|
|
551
|
+
this.log("error", "No secure channel for signature request");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
const verificationData = this.secureChannel.verificationDataBuffer ?? Buffer.alloc(0);
|
|
555
|
+
// Parse signatureRequest:
|
|
556
|
+
// [varint: len of algo1 name][algo1 name bytes][varint: len of algo2 name (0 if none)][algo2 name bytes if present]
|
|
557
|
+
let offset = 0;
|
|
558
|
+
const algo1LenResult = decodeVarInt(data, offset);
|
|
559
|
+
offset += algo1LenResult.consumed;
|
|
560
|
+
const algo1Name = data
|
|
561
|
+
.slice(offset, offset + algo1LenResult.value)
|
|
562
|
+
.toString("utf8");
|
|
563
|
+
offset += algo1LenResult.value;
|
|
564
|
+
let algo2Name = "";
|
|
565
|
+
if (offset < data.length) {
|
|
566
|
+
const algo2LenResult = decodeVarInt(data, offset);
|
|
567
|
+
offset += algo2LenResult.consumed;
|
|
568
|
+
if (algo2LenResult.value > 0) {
|
|
569
|
+
algo2Name = data
|
|
570
|
+
.slice(offset, offset + algo2LenResult.value)
|
|
571
|
+
.toString("utf8");
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
this.log("debug", "Signature request: algo1=", algo1Name, "algo2=", algo2Name);
|
|
575
|
+
const signatureBuffers = [];
|
|
576
|
+
const identity = this.identity;
|
|
577
|
+
const signOne = async (algoName) => {
|
|
578
|
+
switch (algoName) {
|
|
579
|
+
case "ed25519":
|
|
580
|
+
signatureBuffers.push(signEd25519(identity.keys.ed25519PrivKey, verificationData));
|
|
581
|
+
break;
|
|
582
|
+
case "ecdsaP256Sha256":
|
|
583
|
+
signatureBuffers.push(signP256(identity.keys.p256PrivKey, verificationData));
|
|
584
|
+
break;
|
|
585
|
+
case "mldsa44":
|
|
586
|
+
signatureBuffers.push(await signMlDsa44(identity.keys.mlDsa44PrivKey, verificationData));
|
|
587
|
+
break;
|
|
588
|
+
default:
|
|
589
|
+
this.log("warn", "Unknown signature algorithm requested:", algoName);
|
|
590
|
+
}
|
|
591
|
+
};
|
|
592
|
+
await signOne(algo1Name);
|
|
593
|
+
if (algo2Name) {
|
|
594
|
+
await signOne(algo2Name);
|
|
595
|
+
}
|
|
596
|
+
// signatureResponse format: [state byte = 3][sig1_bytes][optional sig2_bytes]
|
|
597
|
+
const statePrefix = Buffer.from([HANDSHAKE_STATE.SIGNATURE_RESPONSE]);
|
|
598
|
+
const responsePayload = Buffer.concat([statePrefix, ...signatureBuffers]);
|
|
599
|
+
this.secureChannel.sendMessage(PROTOCOL.HANDSHAKE, responsePayload);
|
|
600
|
+
this.log("debug", "Sent signature response");
|
|
601
|
+
}
|
|
482
602
|
handleEntityUpdate(payload) {
|
|
483
603
|
const text = payload.toString("utf8");
|
|
484
604
|
let parsed;
|
|
@@ -495,8 +615,11 @@ export class CubyzConnection extends EventEmitter {
|
|
|
495
615
|
return;
|
|
496
616
|
}
|
|
497
617
|
let changed = false;
|
|
498
|
-
|
|
618
|
+
let nullIndex = -1;
|
|
619
|
+
for (let i = 0; i < parsed.length; i++) {
|
|
620
|
+
const entry = parsed[i];
|
|
499
621
|
if (entry === null) {
|
|
622
|
+
nullIndex = i;
|
|
500
623
|
break;
|
|
501
624
|
}
|
|
502
625
|
if (typeof entry === "number") {
|
|
@@ -539,24 +662,33 @@ export class CubyzConnection extends EventEmitter {
|
|
|
539
662
|
}
|
|
540
663
|
this.emitPlayers();
|
|
541
664
|
}
|
|
665
|
+
// Process item drops after the null sentinel.
|
|
666
|
+
// Number entries are removals (by u16 index); object entries are additions
|
|
667
|
+
// or updates whose positions will arrive via the entityPositions protocol.
|
|
668
|
+
if (nullIndex >= 0) {
|
|
669
|
+
for (let i = nullIndex + 1; i < parsed.length; i++) {
|
|
670
|
+
const entry = parsed[i];
|
|
671
|
+
if (typeof entry === "number") {
|
|
672
|
+
this.itemStates.delete(entry);
|
|
673
|
+
}
|
|
674
|
+
// Object entries (add/update) don't carry position data here;
|
|
675
|
+
// their positions are delivered via protocol 6 (ENTITY_POSITION).
|
|
676
|
+
}
|
|
677
|
+
}
|
|
542
678
|
}
|
|
543
679
|
handleEntityPosition(payload) {
|
|
544
680
|
const result = parseEntityPositionPacket(payload, this.log.bind(this));
|
|
545
681
|
if (!result) {
|
|
546
682
|
return;
|
|
547
683
|
}
|
|
548
|
-
// Update internal state with parsed entities
|
|
549
684
|
this.entityStates.clear();
|
|
550
|
-
const
|
|
551
|
-
for (const [id, state] of entityStatesMap) {
|
|
685
|
+
for (const [id, state] of result.entityStates) {
|
|
552
686
|
this.entityStates.set(id, state);
|
|
553
687
|
}
|
|
554
688
|
this.itemStates.clear();
|
|
555
|
-
const
|
|
556
|
-
for (const [index, state] of itemStatesMap) {
|
|
689
|
+
for (const [index, state] of result.itemStates) {
|
|
557
690
|
this.itemStates.set(index, state);
|
|
558
691
|
}
|
|
559
|
-
// Emit the packet without internal state maps
|
|
560
692
|
const packet = {
|
|
561
693
|
timestamp: result.timestamp,
|
|
562
694
|
basePosition: result.basePosition,
|
|
@@ -565,6 +697,204 @@ export class CubyzConnection extends EventEmitter {
|
|
|
565
697
|
};
|
|
566
698
|
this.emit("entityPositions", packet);
|
|
567
699
|
}
|
|
700
|
+
handleBlockUpdate(payload) {
|
|
701
|
+
const updates = [];
|
|
702
|
+
let offset = 0;
|
|
703
|
+
while (offset + 20 <= payload.length) {
|
|
704
|
+
const x = payload.readInt32BE(offset);
|
|
705
|
+
offset += 4;
|
|
706
|
+
const y = payload.readInt32BE(offset);
|
|
707
|
+
offset += 4;
|
|
708
|
+
const z = payload.readInt32BE(offset);
|
|
709
|
+
offset += 4;
|
|
710
|
+
const block = payload.readUInt32BE(offset);
|
|
711
|
+
offset += 4;
|
|
712
|
+
// blockEntityData length is written as usize (8 bytes, big-endian) by the Zig server.
|
|
713
|
+
if (offset + 8 > payload.length) {
|
|
714
|
+
this.log("warn", "Block update payload truncated (no room for length)");
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
const blockEntityDataLen = Number(payload.readBigUInt64BE(offset));
|
|
718
|
+
offset += 8;
|
|
719
|
+
if (offset + blockEntityDataLen > payload.length) {
|
|
720
|
+
this.log("warn", "Block update payload truncated");
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
const blockEntityData = payload.slice(offset, offset + blockEntityDataLen);
|
|
724
|
+
offset += blockEntityDataLen;
|
|
725
|
+
updates.push({
|
|
726
|
+
position: { x, y, z },
|
|
727
|
+
block,
|
|
728
|
+
blockEntityData,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
if (updates.length > 0) {
|
|
732
|
+
this.emit("blockUpdate", updates);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
handleGenericUpdate(payload) {
|
|
736
|
+
if (payload.length < 1) {
|
|
737
|
+
this.log("warn", "Generic update payload too short");
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
const updateType = payload.readUInt8(0);
|
|
741
|
+
let offset = 1;
|
|
742
|
+
this.log("debug", `handleGenericUpdate: type=${updateType}, payloadLen=${payload.length}`);
|
|
743
|
+
try {
|
|
744
|
+
switch (updateType) {
|
|
745
|
+
case GENERIC_UPDATE_TYPE.GAMEMODE: {
|
|
746
|
+
if (offset + 1 > payload.length) {
|
|
747
|
+
this.log("warn", "Gamemode update payload too short");
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const gamemode = payload.readUInt8(offset);
|
|
751
|
+
this.log("debug", `Gamemode update: ${gamemode}`);
|
|
752
|
+
this.emit("genericUpdate", {
|
|
753
|
+
type: "gamemode",
|
|
754
|
+
gamemode,
|
|
755
|
+
});
|
|
756
|
+
break;
|
|
757
|
+
}
|
|
758
|
+
case GENERIC_UPDATE_TYPE.TELEPORT: {
|
|
759
|
+
if (offset + 24 > payload.length) {
|
|
760
|
+
this.log("warn", "Teleport update payload too short");
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const x = payload.readDoubleBE(offset);
|
|
764
|
+
offset += 8;
|
|
765
|
+
const y = payload.readDoubleBE(offset);
|
|
766
|
+
offset += 8;
|
|
767
|
+
const z = payload.readDoubleBE(offset);
|
|
768
|
+
this.emit("genericUpdate", {
|
|
769
|
+
type: "teleport",
|
|
770
|
+
position: { x, y, z },
|
|
771
|
+
});
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
case GENERIC_UPDATE_TYPE.WORLD_EDIT_POS: {
|
|
775
|
+
if (offset + 1 > payload.length) {
|
|
776
|
+
this.log("warn", "World edit pos update payload too short");
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const posType = payload.readUInt8(offset);
|
|
780
|
+
offset += 1;
|
|
781
|
+
let position = null;
|
|
782
|
+
if (posType === WORLD_EDIT_POSITION.SELECTED_POS1 ||
|
|
783
|
+
posType === WORLD_EDIT_POSITION.SELECTED_POS2) {
|
|
784
|
+
if (offset + 12 > payload.length) {
|
|
785
|
+
this.log("warn", "World edit pos update payload too short");
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
const x = payload.readInt32BE(offset);
|
|
789
|
+
offset += 4;
|
|
790
|
+
const y = payload.readInt32BE(offset);
|
|
791
|
+
offset += 4;
|
|
792
|
+
const z = payload.readInt32BE(offset);
|
|
793
|
+
position = { x, y, z };
|
|
794
|
+
}
|
|
795
|
+
this.emit("genericUpdate", {
|
|
796
|
+
type: "worldEditPos",
|
|
797
|
+
positionType: posType,
|
|
798
|
+
position,
|
|
799
|
+
});
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
case GENERIC_UPDATE_TYPE.TIME: {
|
|
803
|
+
if (offset + 8 > payload.length) {
|
|
804
|
+
this.log("warn", "Time update payload too short");
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const time = payload.readBigInt64BE(offset);
|
|
808
|
+
this.emit("genericUpdate", {
|
|
809
|
+
type: "time",
|
|
810
|
+
time,
|
|
811
|
+
});
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
case GENERIC_UPDATE_TYPE.BIOME: {
|
|
815
|
+
if (offset + 4 > payload.length) {
|
|
816
|
+
this.log("warn", "Biome update payload too short");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const biomeId = payload.readUInt32BE(offset);
|
|
820
|
+
this.emit("genericUpdate", {
|
|
821
|
+
type: "biome",
|
|
822
|
+
biomeId,
|
|
823
|
+
});
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
case GENERIC_UPDATE_TYPE.PARTICLES: {
|
|
827
|
+
// [varint u16: particleIdLen][particleId][f64 x][f64 y][f64 z][u8 collides][varint u32: count][varint usize: spawnZonLen][spawnZon]
|
|
828
|
+
const particleIdLenResult = decodeVarInt(payload, offset);
|
|
829
|
+
offset += particleIdLenResult.consumed;
|
|
830
|
+
const particleIdLen = particleIdLenResult.value;
|
|
831
|
+
if (offset + particleIdLen > payload.length) {
|
|
832
|
+
this.log("warn", "Particles update payload truncated (particleId)");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const particleId = payload
|
|
836
|
+
.slice(offset, offset + particleIdLen)
|
|
837
|
+
.toString("utf8");
|
|
838
|
+
offset += particleIdLen;
|
|
839
|
+
if (offset + 24 > payload.length) {
|
|
840
|
+
this.log("warn", "Particles update payload truncated (position)");
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
const px = payload.readDoubleBE(offset);
|
|
844
|
+
offset += 8;
|
|
845
|
+
const py = payload.readDoubleBE(offset);
|
|
846
|
+
offset += 8;
|
|
847
|
+
const pz = payload.readDoubleBE(offset);
|
|
848
|
+
offset += 8;
|
|
849
|
+
if (offset + 1 > payload.length) {
|
|
850
|
+
this.log("warn", "Particles update payload truncated (collides)");
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const collides = payload.readUInt8(offset) !== 0;
|
|
854
|
+
offset += 1;
|
|
855
|
+
const countResult = decodeVarInt(payload, offset);
|
|
856
|
+
offset += countResult.consumed;
|
|
857
|
+
const count = countResult.value;
|
|
858
|
+
const spawnZonLenResult = decodeVarInt(payload, offset);
|
|
859
|
+
offset += spawnZonLenResult.consumed;
|
|
860
|
+
const spawnZonLen = spawnZonLenResult.value;
|
|
861
|
+
if (offset + spawnZonLen > payload.length) {
|
|
862
|
+
this.log("warn", "Particles update payload truncated (spawnZon)");
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const spawnZon = payload
|
|
866
|
+
.slice(offset, offset + spawnZonLen)
|
|
867
|
+
.toString("utf8");
|
|
868
|
+
this.emit("genericUpdate", {
|
|
869
|
+
type: "particles",
|
|
870
|
+
particleId,
|
|
871
|
+
position: { x: px, y: py, z: pz },
|
|
872
|
+
collides,
|
|
873
|
+
count,
|
|
874
|
+
spawnZon,
|
|
875
|
+
});
|
|
876
|
+
break;
|
|
877
|
+
}
|
|
878
|
+
case GENERIC_UPDATE_TYPE.CLEAR: {
|
|
879
|
+
// [u8 clearType: 0=chat]
|
|
880
|
+
if (offset + 1 > payload.length) {
|
|
881
|
+
this.log("warn", "Clear update payload too short");
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
const clearTypeByte = payload.readUInt8(offset);
|
|
885
|
+
// Only clearType 0 (chat) is defined by the server.
|
|
886
|
+
const clearType = clearTypeByte === 0 ? "chat" : "chat";
|
|
887
|
+
this.emit("genericUpdate", { type: "clear", clearType });
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
default:
|
|
891
|
+
this.log("debug", `Unknown generic update type: ${updateType}`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
catch (err) {
|
|
895
|
+
this.log("error", "Failed to parse generic update:", err);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
568
898
|
emitPlayers() {
|
|
569
899
|
const players = this.getPlayers();
|
|
570
900
|
this.emit("players", players);
|