diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bf82ba..93ef806 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: - name: Setup Java JDK uses: actions/setup-java@v1.4.3 with: - java-version: '17' + java-version: '21' distribution: 'adopt' - name: Install dependencies run: npm install diff --git a/docs/README.md b/docs/README.md index 94bf117..94a6cd2 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.20.3 and 1.20.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, 1.20.2, 1.20.3, 1.20.4, 1.20.5 * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. @@ -142,6 +142,7 @@ server.on('playerJoin', function(client) { client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, hashedSeed: [0, 0], maxPlayers: server.maxPlayers, diff --git a/examples/server/server.js b/examples/server/server.js index bf63970..547c095 100644 --- a/examples/server/server.js +++ b/examples/server/server.js @@ -30,6 +30,7 @@ server.on('playerJoin', function (client) { // send init data so client will start rendering world client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, isHardcore: false, gameMode: 0, diff --git a/examples/server_channel/server_channel.js b/examples/server_channel/server_channel.js index 72f00cf..4dc4ff1 100644 --- a/examples/server_channel/server_channel.js +++ b/examples/server_channel/server_channel.js @@ -14,6 +14,7 @@ server.on('playerJoin', function (client) { client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, isHardcore: false, gameMode: 0, diff --git a/examples/server_custom_channel/server_custom_channel.js b/examples/server_custom_channel/server_custom_channel.js index 69f2b10..cc2c3b4 100644 --- a/examples/server_custom_channel/server_custom_channel.js +++ b/examples/server_custom_channel/server_custom_channel.js @@ -11,6 +11,7 @@ const loginPacket = mcData.loginPacket server.on('playerJoin', function (client) { client.write('login', { ...loginPacket, + enforceSecureChat: false, entityId: client.id, isHardcore: false, gameMode: 0, diff --git a/package.json b/package.json index f705215..47b3d02 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "minecraft-wrap": "^1.2.3", "mocha": "^10.0.0", "power-assert": "^1.0.0", - "standard": "^17.0.0" + "standard": "^17.0.0", + "prismarine-registry": "^1.8.0" }, "dependencies": { "@types/readable-stream": "^4.0.0", @@ -51,7 +52,7 @@ "endian-toggle": "^0.0.0", "lodash.get": "^4.1.2", "lodash.merge": "^4.3.0", - "minecraft-data": "^3.55.0", + "minecraft-data": "^3.71.0", "minecraft-folder-path": "^1.2.0", "node-fetch": "^2.6.1", "node-rsa": "^0.4.2", @@ -59,7 +60,7 @@ "prismarine-chat": "^1.10.0", "prismarine-nbt": "^2.5.0", "prismarine-realms": "^1.2.0", - "protodef": "^1.8.0", + "protodef": "^1.17.0", "readable-stream": "^4.1.0", "uuid-1345": "^1.0.1", "yggdrasil": "^1.4.0" diff --git a/src/client.js b/src/client.js index c89375e..5b63c29 100644 --- a/src/client.js +++ b/src/client.js @@ -137,6 +137,7 @@ class Client extends EventEmitter { this.splitter.pipe(this.deserializer) } else { this.serializer.pipe(this.compressor) + if (globalThis.debugNMP) this.decompressor.on('data', (data) => { console.log('DES>', data.toString('hex')) }) this.decompressor.pipe(this.deserializer) } diff --git a/src/client/chat.js b/src/client/chat.js index 5cad995..f14269b 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -176,7 +176,7 @@ module.exports = function (client, options) { }) }) - client.on('message_header', (packet) => { + client.on('message_header', (packet) => { // [1.19.2] updateAndValidateChat(packet.senderUuid, packet.previousSignature, packet.signature, packet.messageHash) client._lastChatHistory.push({ @@ -369,13 +369,14 @@ module.exports = function (client, options) { if (message.startsWith('/')) { const command = message.slice(1) - if (mcData.supportFeature('useChatSessions')) { + if (mcData.supportFeature('useChatSessions')) { // 1.19.3+ const { acknowledged, acknowledgements } = getAcknowledgements() - client.write('chat_command', { + const canSign = client.profileKeys && client._session + client.write((mcData.supportFeature('seperateSignedChatCommandPacket') && canSign) ? 'chat_command_signed' : 'chat_command', { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: (client.profileKeys && client._session) ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], + argumentSignatures: canSign ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], messageCount: client._lastSeenMessages.pending, acknowledged }) diff --git a/src/client/play.js b/src/client/play.js index 246556e..6e06dc1 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -7,11 +7,12 @@ module.exports = function (client, options) { client.on('server_data', (packet) => { client.serverFeatures = { chatPreview: packet.previewsChat, - enforcesSecureChat: packet.enforcesSecureChat + enforcesSecureChat: packet.enforcesSecureChat // in LoginPacket v>=1.20.5 } }) - client.once('login', () => { + client.once('login', (packet) => { + if (packet.enforcesSecureChat) client.serverFeatures.enforcesSecureChat = packet.enforcesSecureChat const mcData = require('minecraft-data')(client.version) if (mcData.supportFeature('useChatSessions') && client.profileKeys && client.cipher && client.session.selectedProfile.id === client.uuid.replace(/-/g, '')) { client._session = { @@ -52,6 +53,9 @@ module.exports = function (client, options) { client.write('configuration_acknowledged', {}) } client.state = states.CONFIGURATION + client.on('select_known_packs', () => { + client.write('select_known_packs', { packs: [] }) + }) // 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', () => { diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 89cacde..ba4db3e 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -40,6 +40,27 @@ module.exports = { code += ' if ((item & 128) === 0) return { value: data, size: cursor - offset }\n' code += '}' return compiler.wrapCode(code) + }], + arrayWithLengthOffset: ['parametrizable', (compiler, array) => { + let code = '' + if (array.countType) { + code += 'const { value: count, size: countSize } = ' + compiler.callType(array.countType) + '\n' + } else if (array.count) { + code += 'const count = ' + array.count + '\n' + code += 'const countSize = 0\n' + } else { + throw new Error('Array must contain either count or countType') + } + code += 'if (count > 0xffffff) throw new Error("array size is abnormally large, not reading: " + count)\n' + code += 'const data = []\n' + code += 'let size = countSize\n' + code += `for (let i = 0; i < count + ${array.lengthOffset}; i++) {\n` + code += ' const elem = ' + compiler.callType(array.type, 'offset + size') + '\n' + code += ' data.push(elem.value)\n' + code += ' size += elem.size\n' + code += '}\n' + code += 'return { value: data, size }' + return compiler.wrapCode(code) }] }, Write: { @@ -72,6 +93,19 @@ module.exports = { code += '}\n' code += 'return offset' return compiler.wrapCode(code) + }], + arrayWithLengthOffset: ['parametrizable', (compiler, array) => { + let code = '' + if (array.countType) { + code += 'offset = ' + compiler.callType('value.length', array.countType) + '\n' + } else if (array.count === null) { + throw new Error('Array must contain either count or countType') + } + code += 'for (let i = 0; i < value.length; i++) {\n' + code += ' offset = ' + compiler.callType('value[i]', array.type) + '\n' + code += '}\n' + code += 'return offset' + return compiler.wrapCode(code) }] }, SizeOf: { @@ -96,6 +130,25 @@ module.exports = { code += '}\n' code += 'return size' return compiler.wrapCode(code) + }], + arrayWithLengthOffset: ['parametrizable', (compiler, array) => { + let code = '' + if (array.countType) { + code += 'let size = ' + compiler.callType('value.length', array.countType) + '\n' + } else if (array.count) { + code += 'let size = 0\n' + } else { + throw new Error('Array must contain either count or countType') + } + if (!isNaN(compiler.callType('value[i]', array.type))) { + code += 'size += value.length * ' + compiler.callType('value[i]', array.type) + '\n' + } else { + code += 'for (let i = 0; i < value.length; i++) {\n' + code += ' size += ' + compiler.callType('value[i]', array.type) + '\n' + code += '}\n' + } + code += 'return size' + return compiler.wrapCode(code) }] } } diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 09e9035..39f5d86 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -11,7 +11,8 @@ module.exports = { compressedNbt: [readCompressedNbt, writeCompressedNbt, sizeOfCompressedNbt], restBuffer: [readRestBuffer, writeRestBuffer, sizeOfRestBuffer], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], - topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray] + topBitSetTerminatedArray: [readTopBitSetTerminatedArray, writeTopBitSetTerminatedArray, sizeOfTopBitSetTerminatedArray], + arrayWithLengthOffset: [readArrayWithLengthOffset, writeArrayWithLengthOffset, sizeOfArrayWithLengthOffset] } const PartialReadError = require('protodef').utils.PartialReadError @@ -180,3 +181,36 @@ function sizeOfTopBitSetTerminatedArray (value, { type }) { } return size } + +// +const { getCount, sendCount, calcCount, tryDoc } = require('protodef/src/utils') + +function readArrayWithLengthOffset (buffer, offset, typeArgs, rootNode) { + const results = { + value: [], + size: 0 + } + let value + let { count, size } = getCount.call(this, buffer, offset, typeArgs, rootNode) + offset += size + results.size += size + for (let i = 0; i < count + typeArgs.lengthOffset; i++) { + ({ size, value } = tryDoc(() => this.read(buffer, offset, typeArgs.type, rootNode), i)) + results.size += size + offset += size + results.value.push(value) + } + return results +} + +// no changes +function writeArrayWithLengthOffset (value, buffer, offset, typeArgs, rootNode) { + offset = sendCount.call(this, value.length, buffer, offset, typeArgs, rootNode) + return value.reduce((offset, v, index) => tryDoc(() => this.write(v, buffer, offset, typeArgs.type, rootNode), index), offset) +} + +function sizeOfArrayWithLengthOffset (value, typeArgs, rootNode) { + let size = calcCount.call(this, value.length, typeArgs, rootNode) + size = value.reduce((size, v, index) => tryDoc(() => size + this.sizeOf(v, typeArgs.type, rootNode), index), size) + return size + typeArgs +} diff --git a/src/server/login.js b/src/server/login.js index 68dc27a..ad143d3 100644 --- a/src/server/login.js +++ b/src/server/login.js @@ -193,7 +193,8 @@ module.exports = function (client, server, options) { if (client.supportFeature('chainedChatWithHashing')) { // 1.19.1+ client.write('server_data', { previewsChat: options.enableChatPreview, - enforceSecureProfile: options.enforceSecureProfile + // Note: in 1.20.5+ user must send this with `login` + enforcesSecureChat: options.enforceSecureProfile }) } @@ -211,7 +212,14 @@ module.exports = function (client, server, options) { function onClientLoginAck () { client.state = states.CONFIGURATION - client.write('registry_data', { codec: options.registryCodec || {} }) + if (client.supportFeature('segmentedRegistryCodecData')) { + for (const key in options.registryCodec) { + const entry = options.registryCodec[key] + client.write('registry_data', entry) + } + } else { + client.write('registry_data', { codec: options.registryCodec || {} }) + } client.once('finish_configuration', () => { client.state = states.PLAY server.emit('playerJoin', client) diff --git a/src/version.js b/src/version.js index f4707bd..b95f01d 100644 --- a/src/version.js +++ b/src/version.js @@ -1,6 +1,6 @@ 'use strict' module.exports = { - 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'] + defaultVersion: '1.20.5', + 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', '1.20.5'] } diff --git a/test/clientTest.js b/test/clientTest.js index 891988b..67eb8ec 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -107,6 +107,43 @@ for (const supportedVersion of mc.supportedVersions) { auth: 'offline' })) client.on('error', err => done(err)) + + client.on('state', (state) => { + console.log('Client now in state', state) + }) + + // ** Dump some server data ** + fs.rmSync(MC_SERVER_DIR + '_registry_data.json', { force: true }) + client.on('raw.registry_data', (buffer) => { + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.bin', buffer) + }) + client.on('registry_data', (json) => { + if (json.codec) { // Pre 1.20.5, codec is 1 json + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(json)) + } else { // 1.20.5+, codec is many nbt's each with their own ids, merge them + let currentData = {} + if (fs.existsSync(MC_SERVER_DIR + '_registry_data.json')) { + currentData = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json', 'utf8')) + } + currentData[json.id] = json + fs.writeFileSync(MC_SERVER_DIR + '_registry_data.json', JSON.stringify(currentData)) + } + console.log('Wrote registry data') + }) + 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 + const codec = JSON.parse(fs.readFileSync(MC_SERVER_DIR + '_registry_data.json')) + fs.writeFileSync(MC_SERVER_DIR + '_loginPacket.json', JSON.stringify({ + ...packet, + dimensionCodec: codec.codec || codec + }, null, 2)) + console.log('Wrote loginPacket.json') + } + }) + // ** End dumping code ** + const lineListener = function (line) { const match = line.match(/\[Server thread\/INFO\]: (?:\[Not Secure\] )?<(.+?)> (.+)/) if (!match) return @@ -117,26 +154,14 @@ for (const supportedVersion of mc.supportedVersions) { } wrap.on('line', lineListener) let chatCount = 0 + client.on('login', function (packet) { - 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)) + if (packet.worldState) { // 1.20.5+ + assert.strictEqual(packet.worldState.gamemode, 'survival') + } else { + assert.strictEqual(packet.gameMode, 0) } + client.chat('hello everyone; I have logged in.') }) client.on('playerChat', function (data) { chatCount += 1 diff --git a/test/common/clientHelpers.js b/test/common/clientHelpers.js index 2fb78d2..797b232 100644 --- a/test/common/clientHelpers.js +++ b/test/common/clientHelpers.js @@ -29,7 +29,7 @@ module.exports = client => { }) client.on('registry_data', (data) => { client.registry ??= Registry(client.version) - client.registry.loadDimensionCodec(data.codec) + client.registry.loadDimensionCodec(data.codec || data) }) client.on('playerJoin', () => { diff --git a/test/packetTest.js b/test/packetTest.js index 893d3ab..da03037 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -18,7 +18,6 @@ function evalCount (count, fields) { const slotValue = { present: true, blockId: 5, - itemCount: 56, itemDamage: 2, nbtData: { type: 'compound', @@ -32,7 +31,14 @@ const slotValue = { test6: { type: 'compound', value: { test: { type: 'int', value: 4 } } }, test7: { type: 'intArray', value: [12, 42] } } - } + }, + // 1.20.5 + itemCount: 1, + itemId: 1111, + addedComponentCount: 0, + removedComponentCount: 0, + components: [], + removeComponents: [] } const nbtValue = { @@ -49,6 +55,24 @@ const nbtValue = { } } +function getFixedPacketPayload (version, packetName) { + if (packetName === 'declare_recipes') { + if (version['>=']('1.20.5')) { + return { + recipes: [ + { + name: 'minecraft:crafting_decorated_pot', + type: 'minecraft:crafting_decorated_pot', + data: { + category: 0 + } + } + ] + } + } + } +} + const values = { i32: 123456, i16: -123, @@ -57,6 +81,7 @@ const values = { varlong: -20, i8: -10, u8: 8, + ByteArray: [], string: 'hi hi this is my client string', buffer: function (typeArgs, context) { let count @@ -124,6 +149,11 @@ const values = { f64: 99999.2222, f32: -333.444, slot: slotValue, + Slot: slotValue, + SlotComponent: { + type: 'hide_tooltip' + }, + SlotComponentType: 0, nbt: nbtValue, optionalNbt: nbtValue, compressedNbt: nbtValue, @@ -161,8 +191,8 @@ const values = { const i = typeArgs.fields[getField(typeArgs.compareTo, context)] if (i === undefined) { if (typeArgs.default === undefined) { - throw new Error("couldn't find the field " + typeArgs.compareTo + - ' of the compareTo and the default is not defined') + typeArgs.default = 'void' + // throw new Error("couldn't find the field " + typeArgs.compareTo + ' of the compareTo and the default is not defined') } return getValue(typeArgs.default, context) } else { return getValue(i, context) } @@ -177,6 +207,7 @@ const values = { }) return results }, + mapper: '', tags: [{ tagName: 'hi', entries: [1, 2, 3, 4, 5] }], ingredient: [slotValue], particleData: null, @@ -212,6 +243,20 @@ const values = { particle: { particleId: 0, data: null + }, + Particle: {}, + SpawnInfo: { + dimension: 0, + name: 'minecraft:overworld', + hashedSeed: [ + 572061085, + 1191958278 + ], + gamemode: 'survival', + previousGamemode: 255, + isDebug: false, + isFlat: false, + portalCooldown: 0 } } @@ -274,24 +319,28 @@ for (const supportedVersion of mc.supportedVersions) { .forEach(function (packetName) { packetInfo = packets[state][direction].types[packetName] packetInfo = packetInfo || null + if (packetName.includes('bundle_delimiter')) return // not a real packet + if (['packet_set_projectile_power', 'packet_debug_sample_subscription'].includes(packetName)) return it(state + ',' + (direction === 'toServer' ? 'Server' : 'Client') + 'Bound,' + packetName, - callTestPacket(packetName.substr(7), packetInfo, state, direction === 'toServer')) + callTestPacket(mcData, packetName.substr(7), packetInfo, state, direction === 'toServer')) }) }) }) - function callTestPacket (packetName, packetInfo, state, toServer) { + function callTestPacket (mcData, packetName, packetInfo, state, toServer) { return function (done) { client.state = state serverClient.state = state - testPacket(packetName, packetInfo, state, toServer, done) + testPacket(mcData, packetName, packetInfo, state, toServer, done) } } - function testPacket (packetName, packetInfo, state, toServer, done) { + function testPacket (mcData, packetName, packetInfo, state, toServer, done) { // empty object uses default values - const packet = getValue(packetInfo, {}) + const packet = getFixedPacketPayload(mcData.version, packetName) || getValue(packetInfo, {}) if (toServer) { + console.log('Writing to server', packetName, JSON.stringify(packet)) serverClient.once(packetName, function (receivedPacket) { + console.log('Recv', packetName) try { assertPacketsMatch(packet, receivedPacket) } catch (e) { @@ -302,7 +351,9 @@ for (const supportedVersion of mc.supportedVersions) { }) client.write(packetName, packet) } else { + console.log('Writing to client', packetName, JSON.stringify(packet)) client.once(packetName, function (receivedPacket) { + console.log('Recv', packetName) assertPacketsMatch(packet, receivedPacket) done() }) diff --git a/test/serverTest.js b/test/serverTest.js index f410597..30ed0b0 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -30,6 +30,9 @@ for (const supportedVersion of mc.supportedVersions) { const version = mcData.version const loginPacket = (client, server) => { + if (mcData.loginPacket) { + return mcData.loginPacket + } return { // 1.7 entityId: client.id, @@ -67,7 +70,9 @@ for (const supportedVersion of mc.supportedVersions) { value: {} }, worldType: 'minecraft:overworld', - death: undefined + death: undefined, + // 1.20.5 + enforceSecureChat: false // more to be added } } @@ -342,7 +347,6 @@ for (const supportedVersion of mc.supportedVersions) { 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', host: '127.0.0.1',