From 1d9a38253a28a515d82fffa13806cb0874c5b36c Mon Sep 17 00:00:00 2001 From: William Gaylord Date: Sun, 25 Feb 2024 18:14:49 -0600 Subject: [PATCH] 1.20.3 / 1.20.4 support (#1275) * Inital 1.20.3 update, bring in minecraft-data with 1.20.3 support and add it to the versions * Add data for packetTest for explosion packet. * Add 1.20.4 to version list. * Add fix for 1.20.3+ NBT isntead of strings for chat. Not happy with this but it does work. * Fix linting * Update version.js Remove 1.20.3 since its the same as 1.20.4. (As suggested) * Comment how handleNBTStrings works. * Removed debug console.log * Update README.md * chat packet nbt handling fix, use feature * big endian UUID, add back `text` wrapper * use prismarine-chat in client test * expose _handleNbtComponent * use prismarine-chat exposed processNbtMessage * fix pre-1.20.4 * Update package.json * Update server.js * Update server.js add missing import * update server hello world --------- Co-authored-by: Romain Beaumont Co-authored-by: extremeheat --- docs/API.md | 2 +- docs/README.md | 48 ++++++++++++++----- examples/server/server.js | 8 +++- .../server_helloworld/server_helloworld.js | 29 ++++++++++- package.json | 3 +- src/client/chat.js | 20 ++++---- src/datatypes/uuid.js | 8 +++- src/version.js | 4 +- test/clientTest.js | 16 ++++--- test/common/clientHelpers.js | 19 ++++++++ test/packetTest.js | 4 ++ 11 files changed, 126 insertions(+), 35 deletions(-) diff --git a/docs/API.md b/docs/API.md index 3d6bd3c..88067bc 100644 --- a/docs/API.md +++ b/docs/API.md @@ -238,7 +238,7 @@ The client's protocol version ### client.version -The client's version +The client's version, as a string ### `packet` event diff --git a/docs/README.md b/docs/README.md index 4b738c8..ed5d875 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,7 @@ Parse and serialize minecraft packets, plus authentication and encryption. * Supports Minecraft PC version 1.7.10, 1.8.8, 1.9 (15w40b, 1.9, 1.9.1-pre2, 1.9.2, 1.9.4), 1.10 (16w20a, 1.10-pre1, 1.10, 1.10.1, 1.10.2), 1.11 (16w35a, 1.11, 1.11.2), 1.12 (17w15a, 17w18b, 1.12-pre4, 1.12, 1.12.1, 1.12.2), and 1.13 (17w50a, 1.13, 1.13.1, 1.13.2-pre1, 1.13.2-pre2, 1.13.2), 1.14 (1.14, 1.14.1, 1.14.3, 1.14.4) - , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2) + , 1.15 (1.15, 1.15.1, 1.15.2) and 1.16 (20w13b, 20w14a, 1.16-rc1, 1.16, 1.16.1, 1.16.2, 1.16.3, 1.16.4, 1.16.5), 1.17 (21w07a, 1.17, 1.17.1), 1.18 (1.18, 1.18.1 and 1.18.2), 1.19 (1.19, 1.19.1, 1.19.2, 1.19.3, 1.19.4), 1.20 (1.20, 1.20.1, 1.20.2, 1.20.3 and 1.20.4) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. @@ -118,16 +118,23 @@ const client = mc.createClient({ For a more up to date example, see examples/server/server.js. ```js -const mc = require('minecraft-protocol'); +const mc = require('minecraft-protocol') +const nbt = require('prismarine-nbt') const server = mc.createServer({ 'online-mode': true, // optional encryption: true, // optional host: '0.0.0.0', // optional port: 25565, // optional - version: '1.16.3' -}); + version: '1.20.4' +}) const mcData = require('minecraft-data')(server.version) +function chatText (text) { + return mcData.supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(text) }) + : JSON.stringify({ text }) +} + server.on('playerJoin', function(client) { const loginPacket = mcData.loginPacket @@ -141,7 +148,7 @@ server.on('playerJoin', function(client) { enableRespawnScreen: true, isDebug: false, isFlat: false - }); + }) client.write('position', { x: 0, @@ -150,18 +157,35 @@ server.on('playerJoin', function(client) { yaw: 0, pitch: 0, flags: 0x00 - }); + }) - const msg = { + const message = { translate: 'chat.type.announcement', - "with": [ + with: [ 'Server', 'Hello, world!' ] - }; - - client.write("chat", { message: JSON.stringify(msg), position: 0, sender: '0' }); -}); + } + if (mcData.supportFeature('signedChat')) { + client.write('player_chat', { + plainMessage: message, + signedChatContent: '', + unsignedChatContent: chatText(message), + type: 0, + senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + senderName: JSON.stringify({ text: 'me' }), + senderTeam: undefined, + timestamp: Date.now(), + salt: 0n, + signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + previousMessages: [], + filterType: 0, + networkName: JSON.stringify({ text: 'me' }) + }) + } else { + client.write('chat', { message: JSON.stringify({ text: message }), position: 0, sender: 'me' }) + } +}) ``` ## Testing diff --git a/examples/server/server.js b/examples/server/server.js index 482531a..bf63970 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -1,4 +1,5 @@ const mc = require('minecraft-protocol') +const nbt = require('prismarine-nbt') const options = { motd: 'Vox Industries', @@ -10,6 +11,11 @@ const options = { const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket +function chatText (text) { + return mcData.supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(text) }) + : JSON.stringify({ text }) +} server.on('playerJoin', function (client) { broadcast(client.username + ' joined the game.') @@ -67,7 +73,7 @@ function sendBroadcastMessage (server, clients, message, sender) { server.writeToClients(clients, 'player_chat', { plainMessage: message, signedChatContent: '', - unsignedChatContent: JSON.stringify({ text: message }), + unsignedChatContent: chatText(message), type: 0, senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random senderName: JSON.stringify({ text: sender }), diff --git a/examples/server_helloworld/server_helloworld.js b/examples/server_helloworld/server_helloworld.js index 0dd0223..3391717 100644 --- a/examples/server_helloworld/server_helloworld.js +++ b/examples/server_helloworld/server_helloworld.js @@ -8,6 +8,13 @@ const options = { const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket +const nbt = require('prismarine-nbt') + +function chatText (text) { + return mcData.supportFeature('chatPacketsUseNbtComponents') + ? nbt.comp({ text: nbt.string(text) }) + : JSON.stringify({ text }) +} server.on('playerJoin', function (client) { const addr = client.socket.remoteAddress @@ -49,14 +56,32 @@ server.on('playerJoin', function (client) { flags: 0x00 }) - const msg = { + const message = { translate: 'chat.type.announcement', with: [ 'Server', 'Hello, world!' ] } - client.write('chat', { message: JSON.stringify(msg), position: 0, sender: '0' }) + if (mcData.supportFeature('signedChat')) { + client.write('player_chat', { + plainMessage: message, + signedChatContent: '', + unsignedChatContent: chatText(message), + type: 0, + senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + senderName: JSON.stringify({ text: 'me' }), + senderTeam: undefined, + timestamp: Date.now(), + salt: 0n, + signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + previousMessages: [], + filterType: 0, + networkName: JSON.stringify({ text: 'me' }) + }) + } else { + client.write('chat', { message: JSON.stringify({ text: message }), position: 0, sender: 'me' }) + } }) server.on('error', function (error) { diff --git a/package.json b/package.json index 856abf2..ab69744 100644 --- a/package.json +++ b/package.json @@ -51,11 +51,12 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.53.0", + "minecraft-data": "^3.55.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", "prismarine-auth": "^2.2.0", + "prismarine-chat": "^1.10.0", "prismarine-nbt": "^2.5.0", "prismarine-realms": "^1.2.0", "protodef": "^1.8.0", diff --git a/src/client/chat.js b/src/client/chat.js index d35cbab..5cad995 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -1,5 +1,6 @@ const crypto = require('crypto') const concat = require('../transforms/binaryStream').concat +const { processNbtMessage } = require('prismarine-chat') const messageExpireTime = 420000 // 7 minutes (ms) function isFormatted (message) { @@ -25,6 +26,10 @@ module.exports = function (client, options) { // This stores the last n (5 or 20) messages that the player has seen, from unique players if (mcData.supportFeature('chainedChatWithHashing')) client._lastSeenMessages = new LastSeenMessages() else client._lastSeenMessages = new LastSeenMessagesWithInvalidation() + // 1.20.3+ serializes chat components in either NBT or JSON. If the chat is sent as NBT, then the structure read will differ + // from the normal JSON structure, so it needs to be normalized. prismarine-chat processNbtMessage will do that by default + // on a fromNotch call. Since we don't call fromNotch here (done in mineflayer), we manually call processNbtMessage + const processMessage = (msg) => mcData.supportFeature('chatPacketsUseNbtComponents') ? processNbtMessage(msg) : msg // This stores the last 128 inbound (signed) messages for 1.19.3 chat validation client._signatureCache = new SignatureCache() @@ -139,12 +144,11 @@ module.exports = function (client, options) { client.on('profileless_chat', (packet) => { // Profileless chat is parsed as an unsigned player chat message but logged as a system message - client.emit('playerChat', { - formattedMessage: packet.message, + formattedMessage: processMessage(packet.message), type: packet.type, - senderName: packet.name, - targetName: packet.target, + senderName: processMessage(packet.name), + targetName: processMessage(packet.target), verified: false }) @@ -160,7 +164,7 @@ module.exports = function (client, options) { client.on('system_chat', (packet) => { client.emit('systemChat', { positionId: packet.isActionBar ? 2 : 1, - formattedMessage: packet.content + formattedMessage: processMessage(packet.content) }) client._lastChatHistory.push({ @@ -198,11 +202,11 @@ module.exports = function (client, options) { if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { plainMessage: packet.plainMessage, - unsignedContent: packet.unsignedChatContent, + unsignedContent: processMessage(packet.unsignedChatContent), type: packet.type, sender: packet.senderUuid, - senderName: packet.networkName, - targetName: packet.networkTargetName, + senderName: processMessage(packet.networkName), + targetName: processMessage(packet.networkTargetName), verified }) diff --git a/src/datatypes/uuid.js b/src/datatypes/uuid.js index 7298230..23e9bbd 100644 --- a/src/datatypes/uuid.js +++ b/src/datatypes/uuid.js @@ -15,4 +15,10 @@ function nameToMcOfflineUUID (name) { return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() } -module.exports = { nameToMcOfflineUUID } +function fromIntArray (arr) { + const buf = Buffer.alloc(16) + arr.forEach((num, index) => { buf.writeInt32BE(num, index * 4) }) + return buf.toString('hex') +} + +module.exports = { nameToMcOfflineUUID, fromIntArray } diff --git a/src/version.js b/src/version.js index 0bba3cd..f4707bd 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.20.2', - supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2'] + defaultVersion: '1.20.4', + supportedVersions: ['1.7', '1.8.8', '1.9.4', '1.10.2', '1.11.2', '1.12.2', '1.13.2', '1.14.4', '1.15.2', '1.16.5', '1.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1', '1.20.2', '1.20.4'] } diff --git a/test/clientTest.js b/test/clientTest.js index fc1dc25..891988b 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -59,6 +59,7 @@ for (const supportedVersion of mc.supportedVersions) { 'server-port': PORT, motd: 'test1234', 'max-players': 120, + // 'level-type': 'flat', 'use-native-transport': 'false' // java 16 throws errors without this, https://www.spigotmc.org/threads/unable-to-access-address-of-buffer.311602 }, (err) => { if (err) reject(err) @@ -165,21 +166,22 @@ for (const supportedVersion of mc.supportedVersions) { } } else { // 1.19+ - - const message = JSON.parse(data.formattedMessage || JSON.stringify({ text: data.plainMessage })) + console.log('Chat Message', data) + const sender = JSON.parse(data.senderName) + const msgPayload = data.formattedMessage ? JSON.parse(data.formattedMessage) : data.plainMessage + const plainMessage = client.parseMessage(msgPayload).toString() if (chatCount === 1) { - assert.strictEqual(message.text, 'hello everyone; I have logged in.') - const sender = JSON.parse(data.senderName) + assert.strictEqual(plainMessage, 'hello everyone; I have logged in.') assert.deepEqual(sender.clickEvent, { action: 'suggest_command', value: '/tell Player ' }) assert.strictEqual(sender.text, 'Player') } else if (chatCount === 2) { - assert.strictEqual(message.text, 'hello') - const sender = JSON.parse(data.senderName) - assert.strictEqual(sender.text, 'Server') + const plainSender = client.parseMessage(sender).toString() + assert.strictEqual(plainMessage, 'hello') + assert.strictEqual(plainSender, 'Server') wrap.removeListener('line', lineListener) client.end() done() diff --git a/test/common/clientHelpers.js b/test/common/clientHelpers.js index 0125378..2fb78d2 100644 --- a/test/common/clientHelpers.js +++ b/test/common/clientHelpers.js @@ -1,3 +1,4 @@ +const Registry = require('prismarine-registry') module.exports = client => { client.nextMessage = (containing) => { return new Promise((resolve) => { @@ -20,5 +21,23 @@ module.exports = client => { }) } + client.on('login', (packet) => { + client.registry ??= Registry(client.version) + if (packet.dimensionCodec) { + client.registry.loadDimensionCodec(packet.dimensionCodec) + } + }) + client.on('registry_data', (data) => { + client.registry ??= Registry(client.version) + client.registry.loadDimensionCodec(data.codec) + }) + + client.on('playerJoin', () => { + const ChatMessage = require('prismarine-chat')(client.registry || client.version) + client.parseMessage = (comp) => { + return new ChatMessage(comp) + } + }) + return client } diff --git a/test/packetTest.js b/test/packetTest.js index e998ad4..893d3ab 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -208,6 +208,10 @@ const values = { packedChunkPos: { x: 10, z: 12 + }, + particle: { + particleId: 0, + data: null } }