From 0ac8c087a28b3ccc73f8eea5941e4902e33c494e Mon Sep 17 00:00:00 2001 From: Evan Goode Date: Sun, 17 Dec 2023 11:57:34 -0500 Subject: [PATCH 01/18] chat: Only sign command args when profile keys defined (#1257) --- src/client/chat.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client/chat.js b/src/client/chat.js index 5674f1f1..e4a94667 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -371,7 +371,7 @@ module.exports = function (client, options) { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements), + argumentSignatures: client.profileKeys ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], messageCount: client._lastSeenMessages.pending, acknowledged }) @@ -381,7 +381,7 @@ module.exports = function (client, options) { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: signaturesForCommand(command, options.timestamp, options.salt), + argumentSignatures: client.profileKeys ? signaturesForCommand(command, options.timestamp, options.salt) : [], signedPreview: options.didPreview, previousMessages: client._lastSeenMessages.map((e) => ({ messageSender: e.sender, From 788bff289030fa66c980de82d82cb953bf76332b Mon Sep 17 00:00:00 2001 From: IceTank <61137113+IceTank@users.noreply.github.com> Date: Sun, 17 Dec 2023 19:08:24 +0100 Subject: [PATCH 02/18] Add chat typing to client (#1260) --- src/index.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index 7135cbb5..6333a6a7 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -5,7 +5,7 @@ import { Socket } from 'net' import * as Stream from 'stream' import { Agent } from 'http' import { Transform } from "readable-stream"; -import { KeyObject } from 'crypto'; +import { BinaryLike, KeyObject } from 'crypto'; import { Realm } from "prismarine-realms" type PromiseLike = Promise | void @@ -39,6 +39,7 @@ declare module 'minecraft-protocol' { signMessage(message: string, timestamp: BigInt, salt?: number, preview?: string, acknowledgements?: Buffer[]): Buffer verifyMessage(publicKey: Buffer | KeyObject, packet: object): boolean reportPlayer(uuid: string, reason: 'FALSE_REPORTING' | 'HATE_SPEECH' | 'TERRORISM_OR_VIOLENT_EXTREMISM' | 'CHILD_SEXUAL_EXPLOITATION_OR_ABUSE' | 'IMMINENT_HARM' | 'NON_CONSENSUAL_INTIMATE_IMAGERY' | 'HARASSMENT_OR_BULLYING' | 'DEFAMATION_IMPERSONATION_FALSE_INFORMATION' | 'SELF_HARM_OR_SUICIDE' | 'ALCOHOL_TOBACCO_DRUGS', signatures: Buffer[], comment?: string): Promise + chat(message: string, options?: { timestamp?: BigInt, salt?: BigInt, preview?: BinaryLike, didPreview?: boolean }): void on(event: 'error', listener: (error: Error) => PromiseLike): this on(event: 'packet', handler: (data: any, packetMeta: PacketMeta, buffer: Buffer, fullBuffer: Buffer) => PromiseLike): this on(event: 'raw', handler: (buffer: Buffer, packetMeta: PacketMeta) => PromiseLike): this From 066a2b3646cb8bef6be1fa974597b975aaf08d42 Mon Sep 17 00:00:00 2001 From: Ynfuien <88110072+Ynfuien@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:32:42 +0100 Subject: [PATCH 03/18] Fixed 'unsignedContent' field using nonexistent 'packet.unsignedContent' when emitting 'playerChat' event. (#1263) --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index e4a94667..3571b733 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -198,7 +198,7 @@ module.exports = function (client, options) { if (verified) client._signatureCache.push(packet.signature) client.emit('playerChat', { plainMessage: packet.plainMessage, - unsignedContent: packet.unsignedContent, + unsignedContent: packet.unsignedChatContent, type: packet.type, sender: packet.senderUuid, senderName: packet.networkName, From 9e991094761d51243cb28a33bb45630f3064511d Mon Sep 17 00:00:00 2001 From: Vitaly Date: Wed, 27 Dec 2023 21:04:45 +0530 Subject: [PATCH 04/18] Allow to create custom client & communication between clients (#1254) * allow to create custom client & communication between client * pass client context * customClient should not be required * allow to override Server impl to use * better docs & typings * refactor: add new client class for customCommunication * fix doc * move custom client to prismarine web client * restore customPackets --- docs/API.md | 24 +++++++++++++----------- src/createClient.js | 3 ++- src/createServer.js | 3 ++- src/index.d.ts | 18 ++++++++++-------- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/API.md b/docs/API.md index 85af2d65..fd548203 100644 --- a/docs/API.md +++ b/docs/API.md @@ -12,7 +12,7 @@ automatically logged in and validated against mojang's auth. * kickTimeout : default to `10*1000` (10s), kick client that doesn't answer to keepalive after that time * checkTimeoutInterval : default to `4*1000` (4s), send keepalive packet at that period * online-mode : default to true - * beforePing : allow customisation of the answer to ping the server does. + * beforePing : allow customisation of the answer to ping the server does. It takes a function with argument response and client, response is the default json response, and client is client who sent a ping. It can take as third argument a callback. If the callback is passed, the function should pass its result to the callback, if not it should return. If the result is `false` instead of a response object then the connection is terminated and no ping is returned to the client. @@ -34,7 +34,8 @@ automatically logged in and validated against mojang's auth. * enforceSecureProfile (optional) : Kick clients that do not have chat signing keys from Mojang (1.19+) * generatePreview (optional) : Function to generate chat previews. Takes the raw message string and should return the message preview as a string. (1.19-1.19.2) * socketType (optional) : either `tcp` or `ipc`. Switches from a tcp connection to a ipc socket connection (or named pipes on windows). With the `ipc` option `host` becomes the path off the ipc connection on the local filesystem. Example: `\\.\pipe\minecraft-ipc` (Windows) `/tmp/minecraft-ipc.sock` (unix based systems). See the ipcConnection example for an example. - + * Server : You can pass a custom server class to use instead of the default one. + ## mc.Server(version,[customPackets]) Create a server instance for `version` of minecraft. @@ -112,7 +113,7 @@ Returns a `Client` instance and perform login. is blank, and `profilesFolder` is specified, we auth with the tokens there instead. If neither `password` or `profilesFolder` are specified, we connect in offline mode. * host : default to localhost - * session : An object holding clientToken, accessToken and selectedProfile. Generated after logging in using username + password with mojang auth or after logging in using microsoft auth. `clientToken`, `accessToken` and `selectedProfile: {name: '', id: ''}` can be set inside of `session` when using createClient to login with a client and access Token instead of a password. `session` is also emitted by the `Client` instance with the event 'session' after successful authentication. + * session : An object holding clientToken, accessToken and selectedProfile. Generated after logging in using username + password with mojang auth or after logging in using microsoft auth. `clientToken`, `accessToken` and `selectedProfile: {name: '', id: ''}` can be set inside of `session` when using createClient to login with a client and access Token instead of a password. `session` is also emitted by the `Client` instance with the event 'session' after successful authentication. * clientToken : generated if a password is given or can be set when when using createClient * accessToken : generated if a password or microsoft account is given or can be set when using createBot * selectedProfile : generated if a password or microsoft account is given. Can be set as a object with property `name` and `id` that specifies the selected profile. @@ -129,21 +130,22 @@ Returns a `Client` instance and perform login. * hideErrors : do not display errors, default to false * skipValidation : do not try to validate given session, defaults to false * stream : a stream to use as connection - * connect : a function taking the client as parameter and that should client.setSocket(socket) + * connect : a function taking the client as parameter and that should client.setSocket(socket) and client.emit('connect') when appropriate (see the proxy examples for an example of use) - * agent : a http agent that can be used to set proxy settings for yggdrasil authentication (see proxy-agent on npm) + * agent : a http agent that can be used to set proxy settings for yggdrasil authentication (see proxy-agent on npm) * fakeHost : (optional) hostname to send to the server in the set_protocol packet * profilesFolder : optional - * (mojang account) the path to the folder that contains your `launcher_profiles.json`. defaults to your minecraft folder if it exists, otherwise the local directory. set to `false` to disable managing profiles + * (mojang account) the path to the folder that contains your `launcher_profiles.json`. defaults to your minecraft folder if it exists, otherwise the local directory. set to `false` to disable managing profiles * (microsoft account) the path to store authentication caches, defaults to .minecraft * onMsaCode(data) : (optional) callback called when signing in with a microsoft account with device code auth. `data` is an object documented [here](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-device-code#device-authorization-response) * id : a numeric client id used for referring to multiple clients in a server * validateChannelProtocol (optional) : whether or not to enable protocol validation for custom protocols using plugin channels. Defaults to true * disableChatSigning (optional) : Don't try obtaining chat signing keys from Mojang (1.19+) - * realms : An object which should contain one of the following properties: `realmId` or `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** - * realmId : The id of the Realm to join. - * pickRealm(realms) : A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. + * realms : An object which should contain one of the following properties: `realmId` or `pickRealm`. When defined will attempt to join a Realm without needing to specify host/port. **The authenticated account must either own the Realm or have been invited to it** + * realmId : The id of the Realm to join. + * pickRealm(realms) : A function which will have an array of the user Realms (joined/owned) passed to it. The function should return a Realm. + * Client : You can pass a custom client class to use instead of the default one, which would allow you to create completely custom communication. Also note that you can use the `stream` option instead where you can supply custom duplex, but this will still use serialization/deserialization of packets. ## mc.Client(isServer,version,[customPackets]) @@ -235,7 +237,7 @@ The client's version ### `packet` event -Called with every packet parsed. Takes four paramaters, the JSON data we parsed, the packet metadata (name, state), the buffer (raw data) and the full buffer (includes surplus data and may include the data of following packets on versions below 1.8) +Called with every packet parsed. Takes four paramaters, the JSON data we parsed, the packet metadata (name, state), the buffer (raw data) and the full buffer (includes surplus data and may include the data of following packets on versions below 1.8) ### `raw` event @@ -272,7 +274,7 @@ Called when a chat message from another player arrives. The emitted object conta * type -- the message type - on 1.19, which format string to use to render message ; below, the place where the message is displayed (for example chat or action bar) * sender -- the UUID of the player sending the message * senderTeam -- scoreboard team of the player (pre 1.19) -* senderName -- Name of the sender +* senderName -- Name of the sender * targetName -- Name of the target (for outgoing commands like /tell). Only in 1.19.2+ * verified -- true if message is signed, false if not signed, undefined on versions prior to 1.19 diff --git a/src/createClient.js b/src/createClient.js index 1c4f0330..97a6336d 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -1,6 +1,6 @@ 'use strict' -const Client = require('./client') +const DefaultClientImpl = require('./client') const assert = require('assert') const encrypt = require('./client/encrypt') @@ -31,6 +31,7 @@ function createClient (options) { options.majorVersion = version.majorVersion options.protocolVersion = version.version const hideErrors = options.hideErrors || false + const Client = options.Client || DefaultClientImpl const client = new Client(false, version.minecraftVersion, options.customPackets, hideErrors) diff --git a/src/createServer.js b/src/createServer.js index 4fa3477e..a81e34b9 100644 --- a/src/createServer.js +++ b/src/createServer.js @@ -1,6 +1,6 @@ 'use strict' -const Server = require('./server') +const DefaultServerImpl = require('./server') const NodeRSA = require('node-rsa') const plugins = [ require('./server/handshake'), @@ -20,6 +20,7 @@ function createServer (options = {}) { motd = 'A Minecraft server', 'max-players': maxPlayersOld = 20, maxPlayers: maxPlayersNew = 20, + Server = DefaultServerImpl, version, favicon, customPackets, diff --git a/src/index.d.ts b/src/index.d.ts index 6333a6a7..a788aecc 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -118,7 +118,7 @@ declare module 'minecraft-protocol' { authTitle?: string sessionServer?: string keepAlive?: boolean - closeTimeout?: number + closeTimeout?: number noPongTimeout?: number checkTimeoutInterval?: number version?: string @@ -137,6 +137,8 @@ declare module 'minecraft-protocol' { realms?: RealmsOptions // 1.19+ disableChatSigning?: boolean + /** Pass custom client implementation if needed. */ + Client?: Client } export class Server extends EventEmitter { @@ -162,9 +164,9 @@ declare module 'minecraft-protocol' { export interface ServerClient extends Client { id: number - // You must call this function when the server receives a message from a player and that message gets - // broadcast to other players in player_chat packets. This function stores these packets so the server - // can then verify a player's lastSeenMessages field in inbound chat packets to ensure chain integrity. + /** You must call this function when the server receives a message from a player and that message gets + broadcast to other players in player_chat packets. This function stores these packets so the server + can then verify a player's lastSeenMessages field in inbound chat packets to ensure chain integrity. */ logSentMessageFromPeer(packet: object): boolean } @@ -188,12 +190,12 @@ declare module 'minecraft-protocol' { hideErrors?: boolean agent?: Agent validateChannelProtocol?: boolean - // 1.19+ - // Require connecting clients to have chat signing support enabled + /** (1.19+) Require connecting clients to have chat signing support enabled */ enforceSecureProfile?: boolean - // 1.19.1 & 1.19.2 only: If client should send previews of messages they are typing to the server + /** 1.19.1 & 1.19.2 only: If client should send previews of messages they are typing to the server */ enableChatPreview?: boolean socketType?: 'tcp' | 'ipc' + Server?: Server } export interface SerializerOptions { @@ -202,7 +204,7 @@ declare module 'minecraft-protocol' { state?: States version: string } - + export interface MicrosoftDeviceAuthorizationResponse { device_code: string user_code: string From 1740124c4722c2c49f8aed0d708ff5ebecc7743c Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Thu, 28 Dec 2023 00:12:23 +0100 Subject: [PATCH 05/18] Improve CI setup for per version tests (#1267) * Improve CI setup: move lint out of per version * fix * Simplify go back to all per version. * refactor cycle test to use supported versions * fix cycle packet test * Add v to version to avoid 1.19 running every 1.19 minor. * Add quotes. * Use versions from js file in ci.yml * Fix ci.yml syntax. * Fix matrix read. * fix * fix * fix gitignore --- .github/workflows/ci.yml | 46 +++++++++++++++++++++------------- .gitignore | 2 +- package.json | 4 +-- test/benchmark.js | 2 +- test/clientTest.js | 2 +- test/cyclePacketTest.js | 53 ++++++++++++++++++++++++++++++++++++++++ test/non-par-test.js | 39 ----------------------------- test/packetTest.js | 2 +- test/serverTest.js | 2 +- 9 files changed, 89 insertions(+), 63 deletions(-) create mode 100644 test/cyclePacketTest.js delete mode 100644 test/non-par-test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deaedc4a..9bf82ba3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,30 +9,39 @@ on: - master jobs: - test: + Lint: runs-on: ubuntu-latest - strategy: - matrix: - mcVersion: ['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.17.1', '1.18.2', '1.19', '1.19.2', '1.19.3', '1.19.4', '1.20', '1.20.1'] - fail-fast: false steps: - uses: actions/checkout@v2 - name: Use Node.js 18.x - uses: actions/setup-node@v1 + uses: actions/setup-node@v1.4.4 with: node-version: 18.x - - name: Setup Java JDK - uses: actions/setup-java@v1.4.3 + - run: npm i && npm run lint + PrepareSupportedVersions: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js 18.x + uses: actions/setup-node@v1.4.4 with: - java-version: '17' - distribution: 'adopt' - - name: Install dependencies - run: npm install - - name: Run tests - run: npm test -- -g ${{ matrix.mcVersion }} - packet-cycle-test: + node-version: 18.x + - id: set-matrix + run: | + node -e " + const supportedVersions = require('./src/version').supportedVersions; + console.log('matrix='+JSON.stringify({'include': supportedVersions.map(mcVersion => ({mcVersion}))})) + " >> $GITHUB_OUTPUT + test: + needs: PrepareSupportedVersions runs-on: ubuntu-latest + strategy: + matrix: ${{fromJson(needs.PrepareSupportedVersions.outputs.matrix)}} + fail-fast: false steps: - uses: actions/checkout@v2 @@ -43,6 +52,9 @@ jobs: - name: Setup Java JDK uses: actions/setup-java@v1.4.3 with: - java-version: '16' + java-version: '17' distribution: 'adopt' - - run: npm install && npm run test-non-par + - name: Install dependencies + run: npm install + - name: Run tests + run: npm run mochaTest -- -g ${{ matrix.mcVersion }}v diff --git a/.gitignore b/.gitignore index 9afc6858..f6816b81 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ node_modules test/npm-debug.log -test/server* +test/server_* package-lock.json versions/ src/client/*.json \ No newline at end of file diff --git a/package.json b/package.json index 8dcff3d1..9a3f7b2a 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,8 @@ "url": "git://github.com/PrismarineJS/node-minecraft-protocol.git" }, "scripts": { - "test": "mocha --recursive --reporter spec --exit --exclude \"non-par-test.js\"", - "test-non-par": "mocha --recursive --reporter spec --exit \"test/non-par-test.js\"", + "test": "npm run mochaTest", + "mochaTest": "mocha --recursive --reporter spec --exit", "lint": "standard", "fix": "standard --fix", "pretest": "npm run lint", diff --git a/test/benchmark.js b/test/benchmark.js index 4dc87f68..a4610371 100644 --- a/test/benchmark.js +++ b/test/benchmark.js @@ -16,7 +16,7 @@ const testDataWrite = [ for (const supportedVersion of mc.supportedVersions) { const mcData = require('minecraft-data')(supportedVersion) const version = mcData.version - describe('benchmark ' + version.minecraftVersion, function () { + describe('benchmark ' + supportedVersion + 'v', function () { this.timeout(60 * 1000) const inputData = [] it('bench serializing', function (done) { diff --git a/test/clientTest.js b/test/clientTest.js index 91796264..2c229f7c 100644 --- a/test/clientTest.js +++ b/test/clientTest.js @@ -28,7 +28,7 @@ for (const supportedVersion of mc.supportedVersions) { console.log(line) }) - describe('client ' + version.minecraftVersion, function () { + describe('client ' + supportedVersion + 'v', function () { this.timeout(10 * 60 * 1000) before(async function () { diff --git a/test/cyclePacketTest.js b/test/cyclePacketTest.js new file mode 100644 index 00000000..9bd9f62b --- /dev/null +++ b/test/cyclePacketTest.js @@ -0,0 +1,53 @@ +/* eslint-env mocha */ +// Tests packet serialization/deserialization from with raw binary from minecraft-packets +const { createSerializer, createDeserializer, states, supportedVersions } = require('minecraft-protocol') +const mcPackets = require('minecraft-packets') +const assert = require('assert') + +const makeClientSerializer = version => createSerializer({ state: states.PLAY, version, isServer: true }) +const makeClientDeserializer = version => createDeserializer({ state: states.PLAY, version }) + +for (const supportedVersion of supportedVersions) { + let serializer, deserializer, data + const mcData = require('minecraft-data')(supportedVersion) + const version = mcData.version + + function convertBufferToObject (buffer) { + return deserializer.parsePacketBuffer(buffer) + } + + function convertObjectToBuffer (object) { + return serializer.createPacketBuffer(object) + } + + function testBuffer (buffer, [packetName, packetIx]) { + const parsed = convertBufferToObject(buffer).data + const parsedBuffer = convertObjectToBuffer(parsed) + const areEq = buffer.equals(parsedBuffer) + assert.strictEqual(areEq, true, `Error when testing ${+packetIx + 1} ${packetName} packet`) + } + describe(`Test cycle packet for version ${supportedVersion}v`, () => { + before(() => { + serializer = makeClientSerializer(version.minecraftVersion) + deserializer = makeClientDeserializer(version.minecraftVersion) + }) + data = mcPackets.pc[version.minecraftVersion] + it('Has packet data', () => { + if (data === undefined) { + // many version do not have data, so print a log for now + // assert when most versions have packet data + console.log(`Version ${version.minecraftVersion} has no packet dump.`) + } + }) + // server -> client + if (data !== undefined) { + Object.entries(data['from-server']).forEach(([packetName, packetData]) => { + it(`${packetName} packet`, () => { + for (const i in packetData) { + testBuffer(packetData[i].raw, [packetName, i]) + } + }) + }) + } + }) +} diff --git a/test/non-par-test.js b/test/non-par-test.js deleted file mode 100644 index 81c420cb..00000000 --- a/test/non-par-test.js +++ /dev/null @@ -1,39 +0,0 @@ -/* eslint-env mocha */ -// Tests packet serialization/deserialization from with raw binary from minecraft-packets -const { createSerializer, createDeserializer, states } = require('minecraft-protocol') -const mcPackets = require('minecraft-packets') -const assert = require('assert') - -const makeClientSerializer = version => createSerializer({ state: states.PLAY, version, isServer: true }) -const makeClientDeserializer = version => createDeserializer({ state: states.PLAY, version }) - -Object.entries(mcPackets.pc).forEach(([ver, data]) => { - let serializer, deserializer - - function convertBufferToObject (buffer) { - return deserializer.parsePacketBuffer(buffer) - } - - function convertObjectToBuffer (object) { - return serializer.createPacketBuffer(object) - } - - function testBuffer (buffer, [packetName, packetIx]) { - const parsed = convertBufferToObject(buffer).data - const parsedBuffer = convertObjectToBuffer(parsed) - const areEq = buffer.equals(parsedBuffer) - assert.strictEqual(areEq, true, `Error when testing ${+packetIx + 1} ${packetName} packet`) - } - describe(`Test version ${ver}`, () => { - serializer = makeClientSerializer(ver) - deserializer = makeClientDeserializer(ver) - // server -> client - Object.entries(data['from-server']).forEach(([packetName, packetData]) => { - it(`${packetName} packet`, () => { - for (const i in packetData) { - testBuffer(packetData[i].raw, [packetName, i]) - } - }) - }) - }) -}) diff --git a/test/packetTest.js b/test/packetTest.js index 0fc81af0..cedfe372 100644 --- a/test/packetTest.js +++ b/test/packetTest.js @@ -244,7 +244,7 @@ for (const supportedVersion of mc.supportedVersions) { const version = mcData.version const packets = mcData.protocol - describe('packets ' + version.minecraftVersion, function () { + describe('packets ' + supportedVersion + 'v', function () { let client, server, serverClient before(async function () { PORT = await getPort() diff --git a/test/serverTest.js b/test/serverTest.js index 7a8a5142..b18a5ba4 100644 --- a/test/serverTest.js +++ b/test/serverTest.js @@ -94,7 +94,7 @@ for (const supportedVersion of mc.supportedVersions) { } } - describe('mc-server ' + version.minecraftVersion, function () { + describe('mc-server ' + supportedVersion + 'v', function () { this.timeout(5000) this.beforeAll(async function () { PORT = await getPort() From 112926da0cb2490934d122dd8ed7b79f3f6de8eb Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 27 Dec 2023 18:48:10 -0500 Subject: [PATCH 06/18] 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 fd548203..3d6bd3cb 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 1a5e7a79..2005dbe3 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 4604c87a..482531ad 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 28e84293..72f00cf0 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 040fe80c..69f2b101 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 d752a301..0dd0223f 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 7acf27ca..eb0975ee 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 9a3f7b2a..4bddeebc 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 6f118233..c89375e3 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 14d28043..949a9e0c 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 unsignedChat (message) { - client.write('chat', { message }) + 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() + }) } - client.chat = client._signedChat || unsignedChat + 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 + }) + }) + } + + 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 dfcb2ffc..671eb452 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 e656744b..3842f45a 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 97a6336d..10cacc07 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 a81e34b9..4a56c0ad 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 cac25260..89cacde0 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 eb9ef249..09e90355 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 00000000..7298230d --- /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 a788aecc..0a5821c3 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 df52a363..68dc27a8 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 ba4792fd..34bf360d 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 22a6b744..7cc127e5 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 5b98d275..12a5fea5 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 2c229f7c..fc1dc256 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 cedfe372..e998ad4e 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 b18a5ba4..b2f67105 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') }) From eaf4c2e003e167788a90a3523a5ea7f91934f91f Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Thu, 28 Dec 2023 00:51:49 +0100 Subject: [PATCH 07/18] Release 1.45.0 (#1270) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 410d9d0e..cef148e4 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,13 @@ # History +## 1.45.0 +* [Pc1.20.2 (#1265)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/112926da0cb2490934d122dd8ed7b79f3f6de8eb) (thanks @extremeheat) +* [Improve CI setup for per version tests (#1267)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1740124c4722c2c49f8aed0d708ff5ebecc7743c) (thanks @rom1504) +* [Allow to create custom client & communication between clients (#1254)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/9e991094761d51243cb28a33bb45630f3064511d) (thanks @zardoy) +* [Fixed 'unsignedContent' field using nonexistent 'packet.unsignedContent' when emitting 'playerChat' event. (#1263)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/066a2b3646cb8bef6be1fa974597b975aaf08d42) (thanks @Ynfuien) +* [Add chat typing to client (#1260)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/788bff289030fa66c980de82d82cb953bf76332b) (thanks @IceTank) +* [chat: Only sign command args when profile keys defined (#1257)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/0ac8c087a28b3ccc73f8eea5941e4902e33c494e) (thanks @evan-goode) + ## 1.44.0 * [Send chat commands as chat commands instead of chat messages for 1.19.3-1.20.1 (#1241)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/41f9e4ac4a35b0ce241264a3f964c4874d96a119) (thanks @lkwilson) * [Fix end bundle bundle_delimiter packet not being emitted (#1248)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/35b2aa536a4739c11fe78f6e8e5c591abd0b0498) (thanks @PondWader) diff --git a/package.json b/package.json index 4bddeebc..56b5f989 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.44.0", + "version": "1.45.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From ccaf538ffd2ab1e25dabd752d721f97bd8bd188f Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 29 Dec 2023 21:49:42 +0100 Subject: [PATCH 08/18] Align supported versions with mineflayer (#1272) --- src/version.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/version.js b/src/version.js index 12a5fea5..0bba3cd8 100644 --- a/src/version.js +++ b/src/version.js @@ -2,5 +2,5 @@ module.exports = { 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'] + 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'] } From 614be919d0f20a43e238751c829a6d584ae636cd Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Fri, 29 Dec 2023 23:11:31 +0100 Subject: [PATCH 09/18] Print if there is a diff in packets in the cycle packet test (#1273) --- test/cyclePacketTest.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/cyclePacketTest.js b/test/cyclePacketTest.js index 9bd9f62b..cd8693e9 100644 --- a/test/cyclePacketTest.js +++ b/test/cyclePacketTest.js @@ -24,6 +24,10 @@ for (const supportedVersion of supportedVersions) { const parsed = convertBufferToObject(buffer).data const parsedBuffer = convertObjectToBuffer(parsed) const areEq = buffer.equals(parsedBuffer) + if (!areEq) { + console.log('original buffer', buffer.toString('hex')) + console.log('cycled buffer', parsedBuffer.toString('hex')) + } assert.strictEqual(areEq, true, `Error when testing ${+packetIx + 1} ${packetName} packet`) } describe(`Test cycle packet for version ${supportedVersion}v`, () => { From 80d038bd61d1933daa1e5e3251635be9ce2116b6 Mon Sep 17 00:00:00 2001 From: Romain Beaumont Date: Sat, 30 Dec 2023 23:42:17 +0100 Subject: [PATCH 10/18] =?UTF-8?q?Add=20test=20to=20make=20sure=20version?= =?UTF-8?q?=20that=20are=20tested=20are=20mentioned=20in=20the=20RE?= =?UTF-8?q?=E2=80=A6=20(#1276)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add test to make sure version that are tested are mentioned in the README * fix lint --- docs/README.md | 2 +- test/docTest.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 test/docTest.js diff --git a/docs/README.md b/docs/README.md index 2005dbe3..4b738c8d 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.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) * Parses all packets and emits events with packet fields as JavaScript objects. * Send a packet by supplying fields as a JavaScript object. diff --git a/test/docTest.js b/test/docTest.js new file mode 100644 index 00000000..1b3d80fe --- /dev/null +++ b/test/docTest.js @@ -0,0 +1,16 @@ +/* eslint-env mocha */ + +const mc = require('../') +const fs = require('fs') +const assert = require('assert') +const path = require('path') + +const readmeContent = fs.readFileSync(path.join(__dirname, '/../docs/README.md'), { encoding: 'utf8', flag: 'r' }) + +for (const supportedVersion of mc.supportedVersions) { + describe('doc ' + supportedVersion + 'v', function () { + it('mentions the supported version in the readme', () => { + assert.ok(readmeContent.includes(supportedVersion), `${supportedVersion} should be mentionned in the README.md but it is not`) + }) + }) +} From 21240f8ab2fd41c76f50b64e3b3a945f50b25b5e Mon Sep 17 00:00:00 2001 From: forester302 <109694860+forester302@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:24:49 +0000 Subject: [PATCH 11/18] Allow commands not to be signed (#1277) --- src/client/chat.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/chat.js b/src/client/chat.js index 3571b733..d35cbab6 100644 --- a/src/client/chat.js +++ b/src/client/chat.js @@ -371,7 +371,7 @@ module.exports = function (client, options) { command, timestamp: options.timestamp, salt: options.salt, - argumentSignatures: client.profileKeys ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], + argumentSignatures: (client.profileKeys && client._session) ? signaturesForCommand(command, options.timestamp, options.salt, options.preview, acknowledgements) : [], messageCount: client._lastSeenMessages.pending, acknowledged }) From 092e10c53d33a7b9be52b5cbb67b1e3e55ac2690 Mon Sep 17 00:00:00 2001 From: William Gaylord Date: Sat, 3 Feb 2024 17:07:30 -0600 Subject: [PATCH 12/18] Acknowledge returning to configuration state if in play state. (#1284) * Acknowledge returning to configuration state if in play state. * Fix packet spelling --- src/client/play.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/client/play.js b/src/client/play.js index 949a9e0c..2f2c5876 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -47,6 +47,10 @@ module.exports = function (client, options) { function enterConfigState () { if (client.state === states.CONFIGURATION) return + // If we are returning to the configuration state from the play state, we ahve to acknowledge it. + if (client.state === states.PLAY) { + client.write('configuration_acknowledged', {}) + } 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 From 85a26a52944c89af273bc974380b438073280981 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sun, 11 Feb 2024 09:55:15 -0500 Subject: [PATCH 13/18] Ensure `onReady` in client is called once (#1287) Fix https://github.com/PrismarineJS/node-minecraft-protocol/issues/1286 Server sending start_config packets will incorrectly cause onReady to call multiple times --- src/client/play.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/play.js b/src/client/play.js index 2f2c5876..246556e0 100644 --- a/src/client/play.js +++ b/src/client/play.js @@ -37,15 +37,15 @@ module.exports = function (client, options) { if (mcData.supportFeature('hasConfigurationState')) { client.write('login_acknowledged', {}) - enterConfigState() + enterConfigState(onReady) // Server can tell client to re-enter config state - client.on('start_configuration', enterConfigState) + client.on('start_configuration', () => enterConfigState()) } else { client.state = states.PLAY onReady() } - function enterConfigState () { + function enterConfigState (finishCb) { if (client.state === states.CONFIGURATION) return // If we are returning to the configuration state from the play state, we ahve to acknowledge it. if (client.state === states.PLAY) { @@ -57,7 +57,7 @@ module.exports = function (client, options) { client.once('finish_configuration', () => { client.write('finish_configuration', {}) client.state = states.PLAY - onReady() + finishCb?.() }) } From f97a2367bae29df4caa57c911ba39c1aa10ee221 Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Sun, 11 Feb 2024 16:09:25 +0100 Subject: [PATCH 14/18] Release 1.46.0 (#1285) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 8 ++++++++ package.json | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index cef148e4..33408f1d 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,13 @@ # History +## 1.46.0 +* [Ensure `onReady` in client is called once (#1287)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/85a26a52944c89af273bc974380b438073280981) (thanks @extremeheat) +* [Acknowledge returning to configuration state if in play state. (#1284)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/092e10c53d33a7b9be52b5cbb67b1e3e55ac2690) (thanks @wgaylord) +* [Allow commands not to be signed (#1277)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/21240f8ab2fd41c76f50b64e3b3a945f50b25b5e) (thanks @forester302) +* [Add test to make sure version that are tested are mentioned in the RE… (#1276)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/80d038bd61d1933daa1e5e3251635be9ce2116b6) (thanks @rom1504) +* [Print if there is a diff in packets in the cycle packet test (#1273)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/614be919d0f20a43e238751c829a6d584ae636cd) (thanks @rom1504) +* [Align supported versions with mineflayer (#1272)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/ccaf538ffd2ab1e25dabd752d721f97bd8bd188f) (thanks @rom1504) + ## 1.45.0 * [Pc1.20.2 (#1265)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/112926da0cb2490934d122dd8ed7b79f3f6de8eb) (thanks @extremeheat) * [Improve CI setup for per version tests (#1267)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1740124c4722c2c49f8aed0d708ff5ebecc7743c) (thanks @rom1504) diff --git a/package.json b/package.json index 56b5f989..856abf28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.45.0", + "version": "1.46.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From 1d9a38253a28a515d82fffa13806cb0874c5b36c Mon Sep 17 00:00:00 2001 From: William Gaylord Date: Sun, 25 Feb 2024 18:14:49 -0600 Subject: [PATCH 15/18] 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 3d6bd3cb..88067bca 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 4b738c8d..ed5d8753 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 482531ad..bf639702 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 0dd0223f..3391717e 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 856abf28..ab697445 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 d35cbab6..5cad9954 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 7298230d..23e9bbd8 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 0bba3cd8..f4707bdf 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 fc1dc256..891988b3 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 01253780..2fb78d2d 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 e998ad4e..893d3ab5 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 } } From e50b604a60403e7412e5124f29cc31a2c00d66ab Mon Sep 17 00:00:00 2001 From: rom1504bot Date: Mon, 26 Feb 2024 01:20:24 +0100 Subject: [PATCH 16/18] Release 1.47.0 (#1288) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- docs/HISTORY.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/HISTORY.md b/docs/HISTORY.md index 33408f1d..5e8aa544 100644 --- a/docs/HISTORY.md +++ b/docs/HISTORY.md @@ -1,5 +1,8 @@ # History +## 1.47.0 +* [1.20.3 / 1.20.4 support (#1275)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/1d9a38253a28a515d82fffa13806cb0874c5b36c) (thanks @wgaylord) + ## 1.46.0 * [Ensure `onReady` in client is called once (#1287)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/85a26a52944c89af273bc974380b438073280981) (thanks @extremeheat) * [Acknowledge returning to configuration state if in play state. (#1284)](https://github.com/PrismarineJS/node-minecraft-protocol/commit/092e10c53d33a7b9be52b5cbb67b1e3e55ac2690) (thanks @wgaylord) diff --git a/package.json b/package.json index ab697445..f705215b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minecraft-protocol", - "version": "1.46.0", + "version": "1.47.0", "description": "Parse and serialize minecraft packets, plus authentication and encryption.", "main": "src/index.js", "types": "src/index.d.ts", From ccab9fb39681f3ebe0d264e2a3f833aa3c5a1ac7 Mon Sep 17 00:00:00 2001 From: William Gaylord Date: Sun, 17 Mar 2024 09:52:25 -0500 Subject: [PATCH 17/18] Fix handling of disconnect in versionChecking on 1.20.3+. (#1291) * Fix handling of disconnect in versionChecking on 1.20.3+. Also handle newer translate string used in 1.20.2+ * Comments why we ignore PLAY and CONFIGURATION state disconnects, and un-inline an if * Add extra comment on why we are ignoring those states. * Change bitwise or to logical or --- src/client/versionChecking.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/client/versionChecking.js b/src/client/versionChecking.js index 9d27955a..ab9833fd 100644 --- a/src/client/versionChecking.js +++ b/src/client/versionChecking.js @@ -1,6 +1,11 @@ +const states = require('../states') + module.exports = function (client, options) { client.on('disconnect', message => { if (!message.reason) { return } + // Prevent the disconnect packet handler in the versionChecking code from triggering on PLAY or CONFIGURATION state disconnects + // Since version checking only happens during that HANDSHAKE / LOGIN state. + if (client.state === states.PLAY || client.state === states.CONFIGURATION) { return } let parsed try { parsed = JSON.parse(message.reason) @@ -11,7 +16,9 @@ module.exports = function (client, options) { let text = parsed.text ? parsed.text : parsed let versionRequired - if (text.translate && text.translate.startsWith('multiplayer.disconnect.outdated_')) { versionRequired = text.with[0] } else { + if (text.translate && (text.translate.startsWith('multiplayer.disconnect.outdated_') || text.translate.startsWith('multiplayer.disconnect.incompatible'))) { + versionRequired = text.with[0] + } else { if (text.extra) text = text.extra[0].text versionRequired = /(?:Outdated client! Please use|Outdated server! I'm still on) (.+)/.exec(text) versionRequired = versionRequired ? versionRequired[1] : null From 495eed56ab230b2615596590064671356d86a2dc Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 22 May 2024 02:12:48 -0400 Subject: [PATCH 18/18] Update doc (#1300) * Update README.md * Update index.d.ts --- docs/README.md | 40 +++++++++++++++++++++------------------- src/index.d.ts | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/README.md b/docs/README.md index ed5d8753..94bf117a 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,7 +22,8 @@ Parse and serialize minecraft packets, plus authentication and encryption. - Encryption - Compression - Both online and offline mode - - Respond to keep-alive packets. + - Respond to keep-alive packets + - Follow DNS service records (SRV) - Ping a server for status * Server - Online/Offline mode @@ -75,29 +76,30 @@ node-minecraft-protocol is pluggable. const mc = require('minecraft-protocol'); const client = mc.createClient({ host: "localhost", // optional - port: 25565, // optional - username: "email@example.com", - password: "12345678", - auth: 'microsoft' // optional; by default uses offline mode, if using a microsoft account, set to 'microsoft' + port: 25565, // set if you need a port that isn't 25565 + username: 'Bot', // username to join as if auth is `offline`, else a unique identifier for this account. Switch if you want to change accounts + // version: false, // only set if you need a specific version or snapshot (ie: "1.8.9" or "1.16.5"), otherwise it's set automatically + // password: '12345678' // set if you want to use password-based auth (may be unreliable). If specified, the `username` must be an email }); -client.on('chat', function(packet) { +client.on('playerChat', function (ev) { // Listen for chat messages and echo them back. - const jsonMsg = JSON.parse(packet.message); - - if (jsonMsg.translate == 'chat.type.announcement' || jsonMsg.translate == 'chat.type.text') { - const username = jsonMsg.with[0].text; - const msg = jsonMsg.with[1]; - - if (username === client.username) return; - - client.write('chat', {message: msg.text}); - } + const content = ev.formattedMessage + ? JSON.parse(ev.formattedMessage) + : ev.unsignedChat + ? JSON.parse(ev.unsignedContent) + : ev.plainMessage + const jsonMsg = JSON.parse(packet.message) + if (ev.senderName === client.username) return + client.chat(JSON.stringify(content)) }); ``` -If the server is in offline mode, you may leave out the `password` option and switch auth to `offline`. -You can also leave out `password` when using a Microsoft account. If provided, password based auth will be attempted first which may fail. *Note:* if using a Microsoft account, your account age must be >= 18 years old. +Set `auth` to `offline` if the server is in offline mode. If `auth` is set to `microsoft`, you will be prompted to login to microsoft.com with a code in your browser. After signing in on your browser, the client will automatically obtain and cache authentication tokens (under your specified username) so you don't have to sign-in again. + +To switch the account, update the supplied username. By default, cached tokens will be stored in your user's .minecraft folder, or if profilesFolder is specified, they'll instead be stored there. For more information on bot options see the [API doc](./API.md). + +Note: SRV records will only be looked up if the port is unspecified or set to 25565 and if the `host` is a valid non-local domain name. ### Client example joining a Realm @@ -125,7 +127,7 @@ const server = mc.createServer({ encryption: true, // optional host: '0.0.0.0', // optional port: 25565, // optional - version: '1.20.4' + version: '1.18' }) const mcData = require('minecraft-data')(server.version) diff --git a/src/index.d.ts b/src/index.d.ts index 0a5821c3..70b86cc4 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -49,7 +49,7 @@ declare module 'minecraft-protocol' { on(event: 'connect', handler: () => PromiseLike): this on(event: string, handler: (data: any, packetMeta: PacketMeta) => PromiseLike): this 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: 'playerChat', handler: (data: { formattedMessage: string, plainMessage: 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