From 112926da0cb2490934d122dd8ed7b79f3f6de8eb Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 27 Dec 2023 18:48:10 -0500 Subject: [PATCH] Pc1.20.2 (#1265) * Initial changes for 1.20.2 * add NBT serialize tag type handling * update tests * Update pnbt and mcdata for nbt change * lint * fix wrong param to sizeOfNbt * fix dupe NBT types * move nbt logic to prismarine-nbt * update tests * update tests * disable protodef validator in pluginChannel * Fix state desync * dump loginPacket.json in test output * enable validation * remove testing line in ci.yml * update pnbt to 2.5.0 * update doc for `playerJoin` * Update serializer.js * update examples * lint * disable client bundle handling if bundle becomes too big * Update client.js * bump mcdata * add soundSource and packedChunkPos example test values --------- Co-authored-by: Romain Beaumont --- docs/API.md | 9 +++ docs/README.md | 14 ++--- examples/server/server.js | 58 ++++++++--------- examples/server_channel/server_channel.js | 6 +- .../server_custom_channel.js | 6 +- .../server_helloworld/server_helloworld.js | 2 +- examples/server_world/mc.js | 6 +- package.json | 4 +- src/client.js | 7 ++- src/client/play.js | 58 ++++++++++++----- src/client/pluginChannels.js | 2 + src/client/setProtocol.js | 2 +- src/createClient.js | 3 + src/createServer.js | 1 + src/datatypes/compiler-minecraft.js | 6 -- src/datatypes/minecraft.js | 35 +---------- src/datatypes/uuid.js | 18 ++++++ src/index.d.ts | 4 ++ src/server/login.js | 37 +++++------ src/states.js | 1 + src/transforms/serializer.js | 3 + src/version.js | 4 +- test/clientTest.js | 22 ++++++- test/packetTest.js | 63 +++++++------------ test/serverTest.js | 19 +++--- 25 files changed, 212 insertions(+), 178 deletions(-) create mode 100644 src/datatypes/uuid.js diff --git a/docs/API.md b/docs/API.md index fd54820..3d6bd3c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -89,6 +89,11 @@ Called when a client connects, but before any login has happened. Takes a Called when a client is logged in against server. Takes a `Client` parameter. +### `playerJoin` event + +Emitted after a player joins and enters the PLAY protocol state and can send and recieve game packets. This is emitted after the `login` event. On 1.20.2 and above after we emit the `login` event, the player will enter a CONFIG state, as opposed to the PLAY state (where game packets can be sent), so you must instead now wait for `playerJoin`. + + ### `listening` event Called when the server is listening for connections. This means that the server is ready to accept incoming connections. @@ -261,6 +266,10 @@ Called when user authentication is resolved. Takes session data as parameter. Called when the protocol changes state. Takes the new state and old state as parameters. +### `playerJoin` event + +Emitted after the player enters the PLAY protocol state and can send and recieve game packets + ### `error` event Called when an error occurs within the client. Takes an Error as parameter. diff --git a/docs/README.md b/docs/README.md index 1a5e7a7..2005dbe 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.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.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.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) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. @@ -115,6 +115,8 @@ const client = mc.createClient({ ### Hello World server example +For a more up to date example, see examples/server/server.js. + ```js const mc = require('minecraft-protocol'); const server = mc.createServer({ @@ -126,18 +128,12 @@ const server = mc.createServer({ }); const mcData = require('minecraft-data')(server.version) -server.on('login', function(client) { +server.on('playerJoin', function(client) { const loginPacket = mcData.loginPacket client.write('login', { + ...loginPacket, entityId: client.id, - isHardcore: false, - gameMode: 0, - previousGameMode: 255, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, - worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, viewDistance: 10, diff --git a/examples/server/server.js b/examples/server/server.js index 4604c87..482531a 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -11,7 +11,7 @@ const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { broadcast(client.username + ' joined the game.') const addr = client.socket.remoteAddress + ':' + client.socket.remotePort console.log(client.username + ' connected', '(' + addr + ')') @@ -23,14 +23,11 @@ server.on('login', function (client) { // send init data so client will start rendering world client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, - worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, viewDistance: 10, @@ -48,11 +45,13 @@ server.on('login', function (client) { flags: 0x00 }) - client.on('chat', function (data) { + function handleChat (data) { const message = '<' + client.username + '>' + ' ' + data.message broadcast(message, null, client.username) console.log(message) - }) + } + client.on('chat', handleChat) // pre-1.19 + client.on('chat_message', handleChat) // post 1.19 }) server.on('error', function (error) { @@ -63,27 +62,28 @@ server.on('listening', function () { console.log('Server listening on port', server.socketServer.address().port) }) -function broadcast (message, exclude, username) { - let client - const translate = username ? 'chat.type.announcement' : 'chat.type.text' - username = username || 'Server' - for (const clientId in server.clients) { - if (server.clients[clientId] === undefined) continue - - client = server.clients[clientId] - if (client !== exclude) { - const msg = { - translate, - with: [ - username, - message - ] - } - client.write('chat', { - message: JSON.stringify(msg), - position: 0, - sender: '0' - }) - } +function sendBroadcastMessage (server, clients, message, sender) { + if (mcData.supportFeature('signedChat')) { + server.writeToClients(clients, 'player_chat', { + plainMessage: message, + signedChatContent: '', + unsignedChatContent: JSON.stringify({ text: message }), + type: 0, + senderUuid: 'd3527a0b-bc03-45d5-a878-2aafdd8c8a43', // random + senderName: JSON.stringify({ text: sender }), + senderTeam: undefined, + timestamp: Date.now(), + salt: 0n, + signature: mcData.supportFeature('useChatSessions') ? undefined : Buffer.alloc(0), + previousMessages: [], + filterType: 0, + networkName: JSON.stringify({ text: sender }) + }) + } else { + server.writeToClients(clients, 'chat', { message: JSON.stringify({ text: message }), position: 0, sender: sender || '0' }) } } + +function broadcast (message, exclude, username) { + sendBroadcastMessage(server, Object.values(server.clients).filter(client => client !== exclude), message) +} diff --git a/examples/server_channel/server_channel.js b/examples/server_channel/server_channel.js index 28e8429..72f00cf 100644 --- a/examples/server_channel/server_channel.js +++ b/examples/server_channel/server_channel.js @@ -8,18 +8,16 @@ const server = mc.createServer({ const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { client.registerChannel('minecraft:brand', ['string', []]) client.on('minecraft:brand', console.log) client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/examples/server_custom_channel/server_custom_channel.js b/examples/server_custom_channel/server_custom_channel.js index 040fe80..69f2b10 100644 --- a/examples/server_custom_channel/server_custom_channel.js +++ b/examples/server_custom_channel/server_custom_channel.js @@ -8,15 +8,13 @@ const server = mc.createServer({ const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/examples/server_helloworld/server_helloworld.js b/examples/server_helloworld/server_helloworld.js index d752a30..0dd0223 100644 --- a/examples/server_helloworld/server_helloworld.js +++ b/examples/server_helloworld/server_helloworld.js @@ -9,7 +9,7 @@ const server = mc.createServer(options) const mcData = require('minecraft-data')(server.version) const loginPacket = mcData.loginPacket -server.on('login', function (client) { +server.on('playerJoin', function (client) { const addr = client.socket.remoteAddress console.log('Incoming connection', '(' + addr + ')') diff --git a/examples/server_world/mc.js b/examples/server_world/mc.js index 7acf27c..eb0975e 100644 --- a/examples/server_world/mc.js +++ b/examples/server_world/mc.js @@ -22,15 +22,13 @@ for (let x = 0; x < 16; x++) { } } -server.on('login', function (client) { +server.on('playerJoin', function (client) { client.write('login', { + ...loginPacket, entityId: client.id, isHardcore: false, gameMode: 0, previousGameMode: 1, - worldNames: loginPacket.worldNames, - dimensionCodec: loginPacket.dimensionCodec, - dimension: loginPacket.dimension, worldName: 'minecraft:overworld', hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/package.json b/package.json index 9a3f7b2..4bddeeb 100644 --- a/package.json +++ b/package.json @@ -51,12 +51,12 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.37.0", + "minecraft-data": "^3.53.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", "prismarine-auth": "^2.2.0", - "prismarine-nbt": "^2.0.0", + "prismarine-nbt": "^2.5.0", "prismarine-realms": "^1.2.0", "protodef": "^1.8.0", "readable-stream": "^4.1.0", diff --git a/src/client.js b/src/client.js index 6f11823..c89375e 100644 --- a/src/client.js +++ b/src/client.js @@ -93,7 +93,7 @@ class Client extends EventEmitter { const s = JSON.stringify(parsed.data, null, 2) debug(s && s.length > 10000 ? parsed.data : s) } - if (parsed.metadata.name === 'bundle_delimiter') { + if (this._hasBundlePacket && parsed.metadata.name === 'bundle_delimiter') { if (this._mcBundle.length) { // End bundle this._mcBundle.forEach(emitPacket) emitPacket(parsed) @@ -103,6 +103,11 @@ class Client extends EventEmitter { } } else if (this._mcBundle.length) { this._mcBundle.push(parsed) + if (this._mcBundle.length > 32) { + this._mcBundle.forEach(emitPacket) + this._mcBundle = [] + this._hasBundlePacket = false + } } else { emitPacket(parsed) } diff --git a/src/client/play.js b/src/client/play.js index 14d2804..949a9e0 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -32,30 +32,54 @@ module.exports = function (client, options) { function onLogin (packet) { const mcData = require('minecraft-data')(client.version) - client.state = states.PLAY client.uuid = packet.uuid client.username = packet.username - if (mcData.supportFeature('signedChat')) { - if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { - throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') - } - signedChatPlugin(client, options) + if (mcData.supportFeature('hasConfigurationState')) { + client.write('login_acknowledged', {}) + enterConfigState() + // Server can tell client to re-enter config state + client.on('start_configuration', enterConfigState) } else { - client.on('chat', (packet) => { - client.emit(packet.position === 0 ? 'playerChat' : 'systemChat', { - formattedMessage: packet.message, - sender: packet.sender, - positionId: packet.position, - verified: false - }) + client.state = states.PLAY + onReady() + } + + function enterConfigState () { + if (client.state === states.CONFIGURATION) return + client.state = states.CONFIGURATION + // Server should send finish_configuration on its own right after sending the client a dimension codec + // for login (that has data about world height, world gen, etc) after getting a login success from client + client.once('finish_configuration', () => { + client.write('finish_configuration', {}) + client.state = states.PLAY + onReady() }) } - function unsignedChat (message) { - client.write('chat', { message }) - } + function onReady () { + client.emit('playerJoin') + if (mcData.supportFeature('signedChat')) { + if (options.disableChatSigning && client.serverFeatures.enforcesSecureChat) { + throw new Error('"disableChatSigning" was enabled in client options, but server is enforcing secure chat') + } + signedChatPlugin(client, options) + } else { + client.on('chat', (packet) => { + client.emit(packet.position === 0 ? 'playerChat' : 'systemChat', { + formattedMessage: packet.message, + sender: packet.sender, + positionId: packet.position, + verified: false + }) + }) + } - client.chat = client._signedChat || unsignedChat + function unsignedChat (message) { + client.write('chat', { message }) + } + + client.chat = client._signedChat || unsignedChat + } } } diff --git a/src/client/pluginChannels.js b/src/client/pluginChannels.js index dfcb2ff..671eb45 100644 --- a/src/client/pluginChannels.js +++ b/src/client/pluginChannels.js @@ -1,11 +1,13 @@ const ProtoDef = require('protodef').ProtoDef const minecraft = require('../datatypes/minecraft') const debug = require('debug')('minecraft-protocol') +const nbt = require('prismarine-nbt') module.exports = function (client, options) { const mcdata = require('minecraft-data')(options.version || require('../version').defaultVersion) const channels = [] const proto = new ProtoDef(options.validateChannelProtocol ?? true) + nbt.addTypesToInterpreter('big', proto) proto.addTypes(mcdata.protocol.types) proto.addTypes(minecraft) proto.addType('registerarr', [readDumbArr, writeDumbArr, sizeOfDumbArr]) diff --git a/src/client/setProtocol.js b/src/client/setProtocol.js index e656744..3842f45 100644 --- a/src/client/setProtocol.js +++ b/src/client/setProtocol.js @@ -37,7 +37,7 @@ module.exports = function (client, options) { : client.profileKeys.signature } : null, - playerUUID: client.session?.selectedProfile?.id + playerUUID: client.session?.selectedProfile?.id ?? client.uuid }) } } diff --git a/src/createClient.js b/src/createClient.js index 97a6336..10cacc0 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -14,6 +14,7 @@ const tcpDns = require('./client/tcp_dns') const autoVersion = require('./client/autoVersion') const pluginChannels = require('./client/pluginChannels') const versionChecking = require('./client/versionChecking') +const uuid = require('./datatypes/uuid') module.exports = createClient @@ -54,6 +55,8 @@ function createClient (options) { case 'offline': default: client.username = options.username + client.uuid = uuid.nameToMcOfflineUUID(client.username) + options.auth = 'offline' options.connect(client) break } diff --git a/src/createServer.js b/src/createServer.js index a81e34b..4a56c0a 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -46,6 +46,7 @@ function createServer (options = {}) { server.onlineModeExceptions = Object.create(null) server.favicon = favicon server.options = options + options.registryCodec = options.registryCodec || mcData.registryCodec || mcData.loginPacket?.dimensionCodec // The RSA keypair can take some time to generate // and is only needed for online-mode diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index cac2526..89cacde 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -16,8 +16,6 @@ module.exports = { size: buffer.length - offset } }], - nbt: ['native', minecraft.nbt[0]], - optionalNbt: ['native', minecraft.optionalNbt[0]], compressedNbt: ['native', minecraft.compressedNbt[0]], entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => { let code = 'let cursor = offset\n' @@ -55,8 +53,6 @@ module.exports = { value.copy(buffer, offset) return offset + value.length }], - nbt: ['native', minecraft.nbt[1]], - optionalNbt: ['native', minecraft.optionalNbt[1]], compressedNbt: ['native', minecraft.compressedNbt[1]], entityMetadataLoop: ['parametrizable', (compiler, { type, endVal }) => { let code = 'for (const i in value) {\n' @@ -84,8 +80,6 @@ module.exports = { restBuffer: ['native', (value) => { return value.length }], - nbt: ['native', minecraft.nbt[2]], - optionalNbt: ['native', minecraft.optionalNbt[2]], compressedNbt: ['native', minecraft.compressedNbt[2]], entityMetadataLoop: ['parametrizable', (compiler, { type }) => { let code = 'let size = 1\n' diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index eb9ef24..09e9035 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -8,8 +8,6 @@ const [readVarInt, writeVarInt, sizeOfVarInt] = require('protodef').types.varint module.exports = { varlong: [readVarLong, writeVarLong, sizeOfVarLong], UUID: [readUUID, writeUUID, 16], - nbt: [readNbt, writeNbt, sizeOfNbt], - optionalNbt: [readOptionalNbt, writeOptionalNbt, sizeOfOptionalNbt], compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt], restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], @@ -43,35 +41,8 @@ function writeUUID (value, buffer, offset) { return offset + 16 } -function readNbt (buffer, offset) { - return nbt.proto.read(buffer, offset, 'nbt') -} - -function writeNbt (value, buffer, offset) { - return nbt.proto.write(value, buffer, offset, 'nbt') -} - -function sizeOfNbt (value) { - return nbt.proto.sizeOf(value, 'nbt') -} - -function readOptionalNbt (buffer, offset) { - if (offset + 1 > buffer.length) { throw new PartialReadError() } - if (buffer.readInt8(offset) === 0) return { size: 1 } - return nbt.proto.read(buffer, offset, 'nbt') -} - -function writeOptionalNbt (value, buffer, offset) { - if (value === undefined) { - buffer.writeInt8(0, offset) - return offset + 1 - } - return nbt.proto.write(value, buffer, offset, 'nbt') -} - -function sizeOfOptionalNbt (value) { - if (value === undefined) { return 1 } - return nbt.proto.sizeOf(value, 'nbt') +function sizeOfNbt (value, { tagType } = { tagType: 'nbt' }) { + return nbt.proto.sizeOf(value, tagType) } // Length-prefixed compressed NBT, see differences: http://wiki.vg/index.php?title=Slot_Data&diff=6056&oldid=4753 @@ -111,7 +82,7 @@ function writeCompressedNbt (value, buffer, offset) { function sizeOfCompressedNbt (value) { if (value === undefined) { return 2 } - const nbtBuffer = Buffer.alloc(sizeOfNbt(value, 'nbt')) + const nbtBuffer = Buffer.alloc(sizeOfNbt(value, { tagType: 'nbt' })) nbt.proto.write(value, nbtBuffer, 0, 'nbt') const compressedNbt = zlib.gzipSync(nbtBuffer) // TODO: async diff --git a/src/datatypes/uuid.js b/src/datatypes/uuid.js new file mode 100644 index 0000000..7298230 --- /dev/null +++ b/src/datatypes/uuid.js @@ -0,0 +1,18 @@ +const crypto = require('crypto') +const UUID = require('uuid-1345') + +// https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163 +function javaUUID (s) { + const hash = crypto.createHash('md5') + hash.update(s, 'utf8') + const buffer = hash.digest() + buffer[6] = (buffer[6] & 0x0f) | 0x30 + buffer[8] = (buffer[8] & 0x3f) | 0x80 + return buffer +} + +function nameToMcOfflineUUID (name) { + return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() +} + +module.exports = { nameToMcOfflineUUID } diff --git a/src/index.d.ts b/src/index.d.ts index a788aec..0a5821c 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -51,6 +51,8 @@ declare module 'minecraft-protocol' { on(event: `raw.${string}`, handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this on(event: 'playerChat', handler: (data: { formattedMessage: string, message: string, type: string, sender: string, senderName: string, senderTeam: string, verified?: boolean }) => PromiseLike): this on(event: 'systemChat', handler: (data: { positionId: number, formattedMessage: string }) => PromiseLike): this + // Emitted after the player enters the PLAY state and can send and recieve game packets + on(event: 'playerJoin', handler: () => void): this once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this once(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this @@ -156,6 +158,8 @@ declare module 'minecraft-protocol' { on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'login', handler: (client: ServerClient) => PromiseLike): this on(event: 'listening', listener: () => PromiseLike): this + // Emitted after the player enters the PLAY state and can send and recieve game packets + on(event: 'playerJoin', handler: (client: ServerClient) => void): this once(event: 'connection', handler: (client: ServerClient) => PromiseLike): this once(event: 'error', listener: (error: Error) => PromiseLike): this once(event: 'login', handler: (client: ServerClient) => PromiseLike): this diff --git a/src/server/login.js b/src/server/login.js index df52a36..68dc27a 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -1,4 +1,4 @@ -const UUID = require('uuid-1345') +const uuid = require('../datatypes/uuid') const crypto = require('crypto') const pluginChannels = require('../client/pluginChannels') const states = require('../states') @@ -166,37 +166,28 @@ module.exports = function (client, server, options) { } } - // https://github.com/openjdk-mirror/jdk7u-jdk/blob/f4d80957e89a19a29bb9f9807d2a28351ed7f7df/src/share/classes/java/util/UUID.java#L163 - function javaUUID (s) { - const hash = crypto.createHash('md5') - hash.update(s, 'utf8') - const buffer = hash.digest() - buffer[6] = (buffer[6] & 0x0f) | 0x30 - buffer[8] = (buffer[8] & 0x3f) | 0x80 - return buffer - } - - function nameToMcOfflineUUID (name) { - return (new UUID(javaUUID('OfflinePlayer:' + name))).toString() - } - function loginClient () { const isException = !!server.onlineModeExceptions[client.username.toLowerCase()] if (onlineMode === false || isException) { - client.uuid = nameToMcOfflineUUID(client.username) + client.uuid = uuid.nameToMcOfflineUUID(client.username) } options.beforeLogin?.(client) if (client.protocolVersion >= 27) { // 14w28a (27) added whole-protocol compression (http://wiki.vg/Protocol_History#14w28a), earlier versions per-packet compressed TODO: refactor into minecraft-data client.write('compress', { threshold: 256 }) // Default threshold is 256 client.compressionThreshold = 256 } + // TODO: find out what properties are on 'success' packet client.write('success', { uuid: client.uuid, username: client.username, properties: [] }) - // TODO: find out what properties are on 'success' packet - client.state = states.PLAY + if (client.supportFeature('hasConfigurationState')) { + client.once('login_acknowledged', onClientLoginAck) + } else { + client.state = states.PLAY + server.emit('playerJoin', client) + } client.settings = {} if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ @@ -217,6 +208,16 @@ module.exports = function (client, server, options) { if (client.supportFeature('signedChat')) chatPlugin(client, server, options) server.emit('login', client) } + + function onClientLoginAck () { + client.state = states.CONFIGURATION + client.write('registry_data', { codec: options.registryCodec || {} }) + client.once('finish_configuration', () => { + client.state = states.PLAY + server.emit('playerJoin', client) + }) + client.write('finish_configuration', {}) + } } function mcPubKeyToPem (mcPubKeyBuffer) { diff --git a/src/states.js b/src/states.js index ba4792f..34bf360 100644 --- a/src/states.js +++ b/src/states.js @@ -4,6 +4,7 @@ const states = { HANDSHAKING: 'handshaking', STATUS: 'status', LOGIN: 'login', + CONFIGURATION: 'configuration', PLAY: 'play' } diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index 22a6b74..7cc127e 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -5,6 +5,7 @@ const Serializer = require('protodef').Serializer const Parser = require('protodef').FullPacketParser const { ProtoDefCompiler } = require('protodef').Compiler +const nbt = require('prismarine-nbt') const minecraft = require('../datatypes/minecraft') const states = require('../states') const merge = require('lodash.merge') @@ -30,6 +31,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr const compiler = new ProtoDefCompiler() compiler.addTypes(require('../datatypes/compiler-minecraft')) compiler.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) + nbt.addTypesToCompiler('big', compiler) const proto = compiler.compileProtoDefSync() protocols[key] = proto return proto @@ -38,6 +40,7 @@ function createProtocol (state, direction, version, customPackets, compiled = tr const proto = new ProtoDef(false) proto.addTypes(minecraft) proto.addProtocol(merge(mcData.protocol, get(customPackets, [mcData.version.majorVersion])), [state, direction]) + nbt.addTypesToInterperter('big', proto) protocols[key] = proto return proto } diff --git a/src/version.js b/src/version.js index 5b98d27..12a5fea 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - defaultVersion: '1.20.1', - supportedVersions: ['1.7', '1.8', '1.9', '1.10', '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'] + defaultVersion: '1.20.2', + supportedVersions: ['1.7', '1.8', '1.9', '1.10', '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'] } diff --git a/test/clientTest.js b/test/clientTest.js index 2c229f7..fc1dc25 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -2,6 +2,7 @@ const mc = require('../') const os = require('os') +const fs = require('fs') const path = require('path') const assert = require('power-assert') const util = require('util') @@ -20,7 +21,8 @@ for (const supportedVersion of mc.supportedVersions) { const version = mcData.version const MC_SERVER_JAR_DIR = process.env.MC_SERVER_JAR_DIR || os.tmpdir() const MC_SERVER_JAR = MC_SERVER_JAR_DIR + '/minecraft_server.' + version.minecraftVersion + '.jar' - const wrap = new Wrap(MC_SERVER_JAR, MC_SERVER_PATH + '_' + supportedVersion, { + const MC_SERVER_DIR = MC_SERVER_PATH + '_' + supportedVersion + const wrap = new Wrap(MC_SERVER_JAR, MC_SERVER_DIR, { minMem: 1024, maxMem: 1024 }) @@ -118,7 +120,23 @@ for (const supportedVersion of mc.supportedVersions) { assert.strictEqual(packet.gameMode, 0) client.chat('hello everyone; I have logged in.') }) - + // Dump some data for easier debugging + client.on('raw.registry_data', (buffer) => { + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.bin', buffer) + }) + client.on('registry_data', (json) => { + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json)) + }) + client.on('login', (packet) => { + fs.writeFileSync(MC_SERVER_DIR + '_login.json', JSON.stringify(packet)) + if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) { + // generate a loginPacket.json for minecraft-data + fs.writeFileSync(MC_SERVER_DIR + '_loginPacket.json', JSON.stringify({ + ...packet, + dimensionCodec: JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json')).codec + }, null, 2)) + } + }) client.on('playerChat', function (data) { chatCount += 1 assert.ok(chatCount <= 2) diff --git a/test/packetTest.js b/test/packetTest.js index cedfe37..e998ad4 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -35,6 +35,20 @@ const slotValue = { } } +const nbtValue = { + type: 'compound', + name: 'test', + value: { + test1: { type: 'int', value: 4 }, + test2: { type: 'long', value: [12, 42] }, + test3: { type: 'byteArray', value: [32] }, + test4: { type: 'string', value: 'ohi' }, + test5: { type: 'list', value: { type: 'int', value: [4] } }, + test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, + test7: { type: 'intArray', value: [12, 42] } + } +} + const values = { i32: 123456, i16: -123, @@ -110,46 +124,12 @@ const values = { f64: 99999.2222, f32: -333.444, slot: slotValue, - nbt: { - type: 'compound', - name: 'test', - value: { - test1: { type: 'int', value: 4 }, - test2: { type: 'long', value: [12, 42] }, - test3: { type: 'byteArray', value: [32] }, - test4: { type: 'string', value: 'ohi' }, - test5: { type: 'list', value: { type: 'int', value: [4] } }, - test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, - test7: { type: 'intArray', value: [12, 42] } - } - }, - optionalNbt: { - type: 'compound', - name: 'test', - value: { - test1: { type: 'int', value: 4 }, - test2: { type: 'long', value: [12, 42] }, - test3: { type: 'byteArray', value: [32] }, - test4: { type: 'string', value: 'ohi' }, - test5: { type: 'list', value: { type: 'int', value: [4] } }, - test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, - test7: { type: 'intArray', value: [12, 42] } - } - }, + nbt: nbtValue, + optionalNbt: nbtValue, + compressedNbt: nbtValue, + anonymousNbt: nbtValue, + anonOptionalNbt: nbtValue, previousMessages: [], - compressedNbt: { - type: 'compound', - name: 'test', - value: { - test1: { type: 'int', value: 4 }, - test2: { type: 'long', value: [12, 42] }, - test3: { type: 'byteArray', value: [32] }, - test4: { type: 'string', value: 'ohi' }, - test5: { type: 'list', value: { type: 'int', value: [4] } }, - test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, - test7: { type: 'intArray', value: [12, 42] } - } - }, i64: [0, 1], u64: [0, 1], entityMetadata: [ @@ -223,6 +203,11 @@ const values = { }, suggestionType: 'minecraft:summonable_entities' } + }, + soundSource: 'master', + packedChunkPos: { + x: 10, + z: 12 } } diff --git a/test/serverTest.js b/test/serverTest.js index b18a5ba..b2f6710 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -96,7 +96,7 @@ for (const supportedVersion of mc.supportedVersions) { describe('mc-server ' + supportedVersion + 'v', function () { this.timeout(5000) - this.beforeAll(async function () { + this.beforeEach(async function () { PORT = await getPort() console.log(`Using port for tests: ${PORT}`) }) @@ -299,7 +299,8 @@ for (const supportedVersion of mc.supportedVersions) { const username = ['player1', 'player2'] let index = 0 - server.on('login', function (client) { + server.on('playerJoin', function (client) { + console.log('ChatTest: Player has joined') assert.notEqual(client.id, null) assert.strictEqual(client.username, username[index++]) broadcast(client.username + ' joined the game.') @@ -322,8 +323,10 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT })) + console.log('ChatTest: Player1 is joining...') player1.on('login', async function (packet) { + console.log('ChatTest: Player 1 has joined') assert.strictEqual(packet.gameMode, 1) const player2 = applyClientHelpers(mc.createClient({ username: 'player2', @@ -332,14 +335,16 @@ for (const supportedVersion of mc.supportedVersions) { port: PORT })) + console.log('ChatTest: waiting for next message from P2') const p1Join = await player1.nextMessage('player2') assert.strictEqual(p1Join, '{"text":"player2 joined the game."}') - + console.log('ChatTest: Got message from P2') player2.chat('hi') const p2hi = await player1.nextMessage('player2') assert.strictEqual(p2hi, '{"text":" hi"}') + console.log('ChatTest: Waiting again for next message from P2') player1.chat('hello') const p1hello = await player2.nextMessage('player1') assert.strictEqual(p1hello, '{"text":" hello"}') @@ -389,7 +394,7 @@ for (const supportedVersion of mc.supportedVersions) { port: PORT }) let count = 2 - server.on('login', function (client) { + server.on('playerJoin', function (client) { client.on('end', function (reason) { assert.strictEqual(reason, 'ServerShutdown') resolve() @@ -404,7 +409,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - client.on('login', function () { + client.on('playerJoin', function () { server.close() }) }) @@ -420,7 +425,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - server.on('login', function (client) { + server.on('playerJoin', function (client) { client.write('login', loginPacket(client, server)) }) server.on('close', done) @@ -459,7 +464,7 @@ for (const supportedVersion of mc.supportedVersions) { version: version.minecraftVersion, port: PORT }) - server.on('login', function (client) { + server.on('playerJoin', function (client) { client.on('end', function (reason) { assert.strictEqual(reason, 'ServerShutdown') })