diff --git a/.github/helper-bot/github-helper.js b/.github/helper-bot/github-helper.js deleted file mode 100644 index 08759b2..0000000 --- a/.github/helper-bot/github-helper.js +++ /dev/null @@ -1,59 +0,0 @@ -if (!process.env.CI) { - // mock a bunch of things for testing locally -- https://github.com/actions/toolkit/issues/71 - process.env.GITHUB_REPOSITORY = 'PrismarineJS/bedrock-protocol' - process.env.GITHUB_EVENT_NAME = 'issue_comment' - process.env.GITHUB_SHA = 'cb2fd97b6eae9f2c7fee79d5a86eb9c3b4ac80d8' - process.env.GITHUB_REF = 'refs/heads/master' - process.env.GITHUB_WORKFLOW = 'Issue comments' - process.env.GITHUB_ACTION = 'run1' - process.env.GITHUB_ACTOR = 'test-user' - module.exports = { getIssueStatus: () => ({}), updateIssue: () => {}, createIssue: () => {} } - return -} - -// const { Octokit } = require('@octokit/rest') // https://github.com/octokit/rest.js -const github = require('@actions/github') - -const token = process.env.GITHUB_TOKEN -const octokit = github.getOctokit(token) -const context = github.context - -async function getIssueStatus (title) { - // https://docs.github.com/en/rest/reference/search#search-issues-and-pull-requests - const existingIssues = await octokit.rest.search.issuesAndPullRequests({ - q: `is:issue repo:${process.env.GITHUB_REPOSITORY} in:title ${title}` - }) - // console.log('Existing issues', existingIssues) - const existingIssue = existingIssues.data.items.find(issue => issue.title === title) - - if (!existingIssue) return {} - - return { open: existingIssue.state === 'open', closed: existingIssue.state === 'closed', id: existingIssue.number } -} - -async function updateIssue (id, payload) { - const issue = await octokit.rest.issues.update({ - ...context.repo, - issue_number: id, - body: payload.body - }) - console.log(`Updated issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) -} - -async function createIssue (payload) { - const issue = await octokit.rest.issues.create({ - ...context.repo, - ...payload - }) - console.log(`Created issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) -} - -async function close (id, reason) { - if (reason) await octokit.rest.issues.createComment({ ...context.repo, issue_number: id, body: reason }) - const issue = await octokit.rest.issues.update({ ...context.repo, issue_number: id, state: 'closed' }) - console.log(`Closed issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) -} - -if (process.env.CI) { - module.exports = { getIssueStatus, updateIssue, createIssue, close } -} diff --git a/.github/helper-bot/index.js b/.github/helper-bot/index.js index e9d5893..8dffc6c 100644 --- a/.github/helper-bot/index.js +++ b/.github/helper-bot/index.js @@ -1,7 +1,7 @@ // Automatic version update checker for Minecraft bedrock edition. const fs = require('fs') const cp = require('child_process') -const helper = require('./github-helper') +const helper = require('gh-helpers')() const latestVesionEndpoint = 'https://itunes.apple.com/lookup?bundleId=com.mojang.minecraftpe&time=' + Date.now() const changelogURL = 'https://feedback.minecraft.net/hc/en-us/sections/360001186971-Release-Changelogs' @@ -102,11 +102,10 @@ async function fetchLatest () { console.log(version, currentVersionReleaseDate, releaseNotes) const title = `Support Minecraft ${result.version}` - - const issueStatus = await helper.getIssueStatus(title) + const issueStatus = await helper.findIssue({ titleIncludes: title }) || {} if (supportedVersions.includes(version)) { - if (issueStatus.open) { + if (issueStatus.isOpen) { helper.close(issueStatus.id, `Closing as ${version} is now supported`) } console.log('Latest version is supported.') @@ -114,7 +113,7 @@ async function fetchLatest () { } - if (issueStatus.closed) { + if (issueStatus.isClosed) { // We already made an issue, but someone else already closed it, don't do anything else console.log('I already made an issue, but it was closed') return @@ -127,7 +126,7 @@ async function fetchLatest () { CloudburstMC: getCommitsInRepo('CloudburstMC/Protocol', version, currentVersionReleaseDate) }) - if (issueStatus.open) { + if (issueStatus.isOpen) { helper.updateIssue(issueStatus.id, issuePayload) } else { helper.createIssue(issuePayload) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2055cc..4176a31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - node-version: [18.x] + node-version: [22.x] runs-on: ${{ matrix.os }} timeout-minutes: 14 steps: @@ -26,5 +26,11 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + # Old versions of bedrock use old libssl that Ubuntu no longer ships with; need manual install + - name: (Linux) Install libssl 1.1 + if: runner.os == 'Linux' + run: | + wget http://archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb + sudo dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb - run: npm install - run: npm test diff --git a/.github/workflows/update-helper.yml b/.github/workflows/update-helper.yml index 84f2dfe..d9380da 100644 --- a/.github/workflows/update-helper.yml +++ b/.github/workflows/update-helper.yml @@ -14,9 +14,9 @@ jobs: - name: Set up Node.js uses: actions/setup-node@master with: - node-version: 18.0.0 - - name: Install Github Actions toolkit - run: npm i @actions/github + node-version: 22.0.0 + - name: Install Github Actions helper + run: npm i gh-helpers # The env vars contain the relevant trigger information, so we don't need to pass it - name: Runs helper run: cd .github/helper-bot && node index.js diff --git a/HISTORY.md b/HISTORY.md index a1054f2..799d0a9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,79 @@ +## 3.49.0 +* [1.21.111 (#649)](https://github.com/PrismarineJS/bedrock-protocol/commit/b48518a6e79e72101fe7136433cbd6277339fc5c) (thanks @Slauh) +* [Skin Data Changes (#647)](https://github.com/PrismarineJS/bedrock-protocol/commit/407756b93880cdda4fdbff194fc4163ceedf4e82) (thanks @thejfkvis) + +## 3.48.1 +* [Update login client skinData (#635)](https://github.com/PrismarineJS/bedrock-protocol/commit/6b1474d2c6f93b47dee9d4816de59579f82ed5a9) (thanks @TSL534) + +## 3.48.0 +* [1.21.100 (#632)](https://github.com/PrismarineJS/bedrock-protocol/commit/06fb3de3a0023d03201dbcee7e9178c269462766) (thanks @extremeheat) + +## 3.47.0 +* [1.21.93 support (#623)](https://github.com/PrismarineJS/bedrock-protocol/commit/14daa2d95aac90ffcc7b42d625e270020ec2f162) (thanks @CreeperG16) + +## 3.46.0 +* [1.21.90 support (#617)](https://github.com/PrismarineJS/bedrock-protocol/commit/c66cdd3d62d2fa9c581693d8c70d7b41f355b63e) (thanks @CreeperG16) + +## 3.45.0 +* [1.21.80 (#602)](https://github.com/PrismarineJS/bedrock-protocol/commit/e71fd513ddbd432983f221980080b61e11576965) (thanks @extremeheat) + +## 3.44.0 +* [1.21.70 (#594)](https://github.com/PrismarineJS/bedrock-protocol/commit/065f41db8cfc8cbd8106bd9e376c899ec25f3f77) (thanks @CreeperG16) + +## 3.43.1 +* [Fix server not correctly removing clients (#588)](https://github.com/PrismarineJS/bedrock-protocol/commit/47f342ca958ba87a7719783bd5c855cebdd4aa65) (thanks @EntifiedOptics) + +## 3.43.0 +* [1.21.60 support (#570)](https://github.com/PrismarineJS/bedrock-protocol/commit/eeb5e47e35f31cc571a9a8a491f5a89b27e637f1) (thanks @CreeperG16) +* [Fix version feature handling (#572)](https://github.com/PrismarineJS/bedrock-protocol/commit/0ed8e32be85f05926cd97d5f0317ed004ae5eefa) (thanks @ItsMax123) + +## 3.42.3 +* [Fix Server `maxPlayers` option (#565)](https://github.com/PrismarineJS/bedrock-protocol/commit/38dc5a256105a44786d5455570d5a130e64ef561) (thanks @extremeheat) + +## 3.42.2 +* Fix missing type serialization error + +## 3.42.1 +* [Add 1.21.40 login fields (#553)](https://github.com/PrismarineJS/bedrock-protocol/commit/24d3200181c060162b04fb233fef6e0d6d1a93aa) (thanks @extremeheat) +* [Remove protodef varint types (#552)](https://github.com/PrismarineJS/bedrock-protocol/commit/347e303ce422bdb6f6dfd4cba57d7d3937214707) (thanks @extremeheat) + +## 3.42.0 +* [1.21.50 support](https://github.com/PrismarineJS/bedrock-protocol/commit/1c0836bff03d50cb12a3e45763eac6c9f605e00c) (thanks @extremeheat) +* [Dynamic compression & batch header (#544)](https://github.com/PrismarineJS/bedrock-protocol/commit/911e0e890febc00102cd1e5406731e66f7bad0ef) (thanks @LucienHH) + +## 3.41.0 +* [1.21.42 support](https://github.com/PrismarineJS/bedrock-protocol/commit/dd5c4de4f2624c3654af66e9a40a65eb13de0850) (thanks @CreeperG16) + +## 3.40.0 +* [1.21.30 support (#527)](https://github.com/PrismarineJS/bedrock-protocol/commit/fc30c96135ec20dca1257f702152cba61d4a59be) (thanks @pokecosimo) +* [Update tests (#528)](https://github.com/PrismarineJS/bedrock-protocol/commit/cb530c8b45bf505f75e0e39241d88085c5564ae8) (thanks @extremeheat) + +## 3.39.0 +* [1.21.20](https://github.com/PrismarineJS/bedrock-protocol/commit/3be55777fab4949179d3a7108ee29bbd8fada5a7) (thanks @extremeheat) +* [update disconnect packet](https://github.com/PrismarineJS/bedrock-protocol/commit/4c3f62567e0f6ce20b70ea23238fce8606011e95) (thanks @extremeheat) + +## 3.38.0 +* [Support 1.21.2, and add missing versions to type definitions (#510)](https://github.com/PrismarineJS/bedrock-protocol/commit/5d3986924d3f262708d7c7e55a7f410f12c7903c) (thanks @CreeperG16) +* [Fix example in README.md for 1.21 (#506)](https://github.com/PrismarineJS/bedrock-protocol/commit/c4593aa355d6ce9e2ac65cc2102cd9285a6b6449) (thanks @Ant767) +* [Don't send now deprecated tick sync packets on 1.21 and newer (#504)](https://github.com/PrismarineJS/bedrock-protocol/commit/84c5231b92df9f5f1a09b29a05e7abfed62f1c2b) (thanks @w0ahL) + +## 3.37.0 +* [Support 1.21.0](https://github.com/PrismarineJS/bedrock-protocol/commit/5b2d78792c9b4c070d727a9028a6b3a266483e1c) (thanks @CreeperG16) +* [Fix typo in types (#501)](https://github.com/PrismarineJS/bedrock-protocol/commit/16e15d80a5084a19ed2fbabc023789ee38922b3a) (thanks @Kaaaaii) + +## 3.36.0 +* [Support 1.20.80](https://github.com/PrismarineJS/bedrock-protocol/commit/bd32aa8d04555fa2fdc4ecd6abbeb6124e2ae8bb) (thanks @extremeheat) + +## 3.35.0 +* [Support 1.20.71](https://github.com/PrismarineJS/bedrock-protocol/commit/d8e707112acc038b6c9564d9a21b2f977326e47f) (thanks @extremeheat) +* [Note `npm update` command in readme](https://github.com/PrismarineJS/bedrock-protocol/commit/ab93d0d0824bd0ace250fb73f703dc7b60ecd780) (thanks @extremeheat) + +## 3.34.0 +* [1.20.61 support (#480)](https://github.com/PrismarineJS/bedrock-protocol/commit/c278a03f952d23320b80f8c09b6372d41eeff26a) (thanks @extremeheat) +* [Compressor handling update for 1.20.60 (#479)](https://github.com/PrismarineJS/bedrock-protocol/commit/d3161badc65f2eba4b6e7c9e974ca4e3529a7e94) (thanks @extremeheat) +* [Update and rename CONTRIBUTING.md to docs/CONTRIBUTING.md (#475)](https://github.com/PrismarineJS/bedrock-protocol/commit/be6f0cde9f7970a4f9aa376c589c58d8cb4187c3) (thanks @extremeheat) +* [Add flow and deviceType options to relay (#464)](https://github.com/PrismarineJS/bedrock-protocol/commit/842e66266f09e8670a644a618d0ac4157746cd43) (thanks @GameParrot) + ## 3.33.1 * [Fix zigzag type move in prismarine-nbt (#471)](https://github.com/PrismarineJS/bedrock-protocol/commit/7b74cbf7129646adc80d50304afce6240848cfae) (thanks @extremeheat) diff --git a/README.md b/README.md index 0bc55c2..5e3add2 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@ [![Official Discord](https://img.shields.io/static/v1.svg?label=OFFICIAL&message=DISCORD&color=blue&logo=discord&style=for-the-badge)](https://discord.gg/GsEFRM8) -Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication and encryption. Help [contribute](CONTRIBUTING.md). +Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication and encryption. Help [contribute](docs/CONTRIBUTING.md). [Protocol doc](https://prismarinejs.github.io/minecraft-data/?v=bedrock_1.19.10&d=protocol) ## Features - - Supports Minecraft Bedrock version 1.16.201, 1.16.210, 1.16.220, 1.17.0, 1.17.10, 1.17.30, 1.17.40, 1.18.0, 1.18.11, 1.18.30, 1.19.1, 1.19.10, 1.19.20, 1.19.21, 1.19.30, 1.19.40, 1.19.41, 1.19.50, 1.19.60, 1.19.62, 1.19.63, 1.19.70, 1.19.80, 1.20.0, 1.20.10, 1.20.30, 1.20.40, 1.20.50 + - Supports Minecraft Bedrock version 1.16.201, 1.16.210, 1.16.220, 1.17.0, 1.17.10, 1.17.30, 1.17.40, 1.18.0, 1.18.11, 1.18.30, 1.19.1, 1.19.10, 1.19.20, 1.19.21, 1.19.30, 1.19.40, 1.19.41, 1.19.50, 1.19.60, 1.19.62, 1.19.63, 1.19.70, 1.19.80, 1.20.0, 1.20.10, 1.20.30, 1.20.40, 1.20.50, 1.20.61, 1.20.71, 1.20.80, 1.21.0, 1.21.2, 1.21.21, 1.21.30, 1.21.42, 1.21.50, 1.21.60, 1.21.70, 1.21.80, 1.21.90, 1.21.93, 1.21.100, 1.21.111 - Parse and serialize packets as JavaScript objects - Automatically respond to keep-alive packets - [Proxy and mitm connections](docs/API.md#proxy-docs) @@ -34,6 +34,8 @@ Want to contribute on something important for PrismarineJS ? go to https://githu `npm install bedrock-protocol` +To update bedrock-protocol (or any Node.js package) and its dependencies after a previous install, you must run `npm update --depth 9999` + ## Usage ### Client example @@ -52,7 +54,7 @@ const client = bedrock.createClient({ client.on('text', (packet) => { // Listen for chat messages from the server and echo them back. if (packet.source_name != client.username) { client.queue('text', { - type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', + type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '', message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}` }) } @@ -126,7 +128,7 @@ Through node.js, add `process.env.DEBUG = 'minecraft-protocol'` at the top of yo ## Contribute -Please read [CONTRIBUTING.md](CONTRIBUTING.md) and https://github.com/PrismarineJS/prismarine-contribute +Please read [CONTRIBUTING.md](docs/CONTRIBUTING.md) and https://github.com/PrismarineJS/prismarine-contribute ## History diff --git a/docs/API.md b/docs/API.md index b7b5722..d69c385 100644 --- a/docs/API.md +++ b/docs/API.md @@ -142,7 +142,7 @@ client.on('text', (packet) => { // names and as explained in the "Protocol doc" section below, fields are all case sensitive! client.on('add_player', (packet) => { client.queue('text', { - type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', + type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '', message: `Hey, ${packet.username} just joined!` }) }) diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 93% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md index fd23544..56d8f52 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -110,37 +110,39 @@ The above roughly translates to the following JavaScript code to read a packet: ```js function read_position(stream) { const ret = {} - ret.x = stream.readSignedInt32LE() - ret.z = stream.readUnsignedInt32LE() - ret.y = stream.readFloat32LE() + ret.x = stream.readLI32() + ret.z = stream.readLU32() + ret.y = stream.readLF32() return ret } function read_player_position(stream) { const ret = {} ret.on_ground = Boolean(stream.readU8()) - ret.position = read_player_position(stream) + ret.position = read_position(stream) let __movement_reason = stream.readU8() let movement_reason = { 0: 'player_jump', 1: 'player_autojump', 2: 'player_sneak', 3: 'player_sprint', 4: 'player_fall' }[__movement_reason] switch (movement_reason) { case 'player_jump': case 'player_autojump': - ret.original_position = read_player_position(stream) - ret.jump_tick = stream.readInt64LE(stream) + ret.original_position = read_position(stream) + ret.jump_tick = stream.readLI64() break case 'player_fall': - ret.original_position = read_player_position(stream) + ret.original_position = read_position(stream) break default: break } ret.player_hunger = undefined if (movement_reason == 'player_sprint') ret.player_hunger = stream.readU8() ret.last_positions = [] - for (let i = 0; i < stream.readUnsignedVarInt(); i++) { + let __latest_positions_len = stream.readUnsignedVarInt() + for (let i = 0; i < __latest_positions_len; i++) { ret.last_positions.push(read_player_position(stream)) } ret.keys_down = [] - for (let i = 0; i < stream.readZigZagVarInt(); i++) { + let __keys_down_len = stream.readZigZagVarInt() + for (let i = 0; i < __keys_down_len; i++) { const ret1 = {} ret1.up = Boolean(stream.readU8()) ret1.down = Boolean(stream.readU8()) diff --git a/examples/client/client.js b/examples/client/client.js index 7bbd028..9f17e57 100644 --- a/examples/client/client.js +++ b/examples/client/client.js @@ -10,7 +10,7 @@ const client = bedrock.createClient({ client.on('text', (packet) => { // Listen for chat messages and echo them back. if (packet.source_name != client.username) { client.queue('text', { - type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', + type: 'chat', needs_translation: false, source_name: client.username, xuid: '', platform_chat_id: '', filtered_message: '', message: `${packet.source_name} said: ${packet.message} on ${new Date().toLocaleString()}` }) } diff --git a/index.d.ts b/index.d.ts index c93b5a9..3f4a571 100644 --- a/index.d.ts +++ b/index.d.ts @@ -3,7 +3,7 @@ import { Realm } from 'prismarine-realms' import { ServerDeviceCodeResponse } from 'prismarine-auth' declare module 'bedrock-protocol' { - type Version = '1.20.40' | '1.20.30' | '1.20.10' | '1.20.0' | '1.19.80' | '1.19.70' | '1.19.63' | '1.19.62' | '1.19.60' | '1.19.51' | '1.19.50' | '1.19.41' | '1.19.40' | '1.19.31' | '1.19.30' | '1.19.22' | '1.19.21' | '1.19.20' | '1.19.11' | '1.19.10' | '1.19.2' | '1.19.1' | '1.18.31' | '1.18.30' | '1.18.12' | '1.18.11' | '1.18.10' | '1.18.2' | '1.18.1' | '1.18.0' | '1.17.41' | '1.17.40' | '1.17.34' | '1.17.30' | '1.17.11' | '1.17.10' | '1.17.0' | '1.16.220' | '1.16.210' | '1.16.201' + type Version = '1.21.93' | '1.21.90' | '1.21.80' | '1.21.70' | '1.21.60' | '1.21.50' | '1.21.42' | '1.21.30' | '1.21.2' | '1.21.0' | '1.20.80' | '1.20.71' | '1.20.61' | '1.20.50' | '1.20.40' | '1.20.30' | '1.20.10' | '1.20.0' | '1.19.80' | '1.19.70' | '1.19.63' | '1.19.62' | '1.19.60' | '1.19.51' | '1.19.50' | '1.19.41' | '1.19.40' | '1.19.31' | '1.19.30' | '1.19.22' | '1.19.21' | '1.19.20' | '1.19.11' | '1.19.10' | '1.19.2' | '1.19.1' | '1.18.31' | '1.18.30' | '1.18.12' | '1.18.11' | '1.18.10' | '1.18.2' | '1.18.1' | '1.18.0' | '1.17.41' | '1.17.40' | '1.17.34' | '1.17.30' | '1.17.11' | '1.17.10' | '1.17.0' | '1.16.220' | '1.16.210' | '1.16.201' export interface Options { // The string version to start the client or server as @@ -63,7 +63,7 @@ declare module 'bedrock-protocol' { } enum ClientStatus { - Disconected, + Disconnected, Authenticating, Initializing, Initialized @@ -162,7 +162,7 @@ declare module 'bedrock-protocol' { constructor(options: Options) - listen(host?: string, port?: number): Promise + listen(): Promise close(disconnectReason?: string): Promise on(event: 'connect', cb: (client: Player) => void): any diff --git a/package.json b/package.json index f150728..6301be0 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "bedrock-protocol", - "version": "3.33.1", + "version": "3.49.0", "description": "Minecraft Bedrock Edition protocol library", "main": "index.js", "types": "index.d.ts", "scripts": { "build": "cd tools && node compileProtocol.js", - "test": "mocha --bail --exit", + "test": "mocha --retries 2 --bail --exit", "pretest": "npm run lint", "lint": "standard", - "vanillaServer": "node tools/startVanillaServer.js", + "vanillaServer": "minecraft-bedrock-server --root tools --version", "dumpPackets": "node tools/genPacketDumps.js", "fix": "standard --fix" }, @@ -40,7 +40,8 @@ "bedrock-protocol": "file:.", "bedrock-provider": "^2.0.0", "leveldb-zlib": "^1.0.1", - "mocha": "^10.0.0", + "minecraft-bedrock-server": "^1.4.2", + "mocha": "^11.0.1", "protodef-yaml": "^1.1.0", "standard": "^17.0.0-2" }, diff --git a/src/client.js b/src/client.js index b6bb0ac..e3af1d0 100644 --- a/src/client.js +++ b/src/client.js @@ -26,6 +26,7 @@ class Client extends Connection { this.compressionAlgorithm = this.versionGreaterThanOrEqualTo('1.19.30') ? 'none' : 'deflate' this.compressionThreshold = 512 this.compressionLevel = this.options.compressionLevel + this.batchHeader = 0xfe if (isDebug) { this.inLog = (...args) => debug('C ->', ...args) @@ -42,6 +43,7 @@ class Client extends Connection { this.validateOptions() this.serializer = createSerializer(this.options.version) this.deserializer = createDeserializer(this.options.version) + this._loadFeatures() KeyExchange(this, null, this.options) Login(this, null, this.options) @@ -55,6 +57,19 @@ class Client extends Connection { this.emit('connect_allowed') } + _loadFeatures () { + try { + const mcData = require('minecraft-data')('bedrock_' + this.options.version) + this.features = { + compressorInHeader: mcData.supportFeature('compressorInPacketHeader'), + itemRegistryPacket: mcData.supportFeature('itemRegistryPacket'), + newLoginIdentityFields: mcData.supportFeature('newLoginIdentityFields') + } + } catch (e) { + throw new Error(`Unsupported version: '${this.options.version}', no data available`) + } + } + connect () { if (!this.connection) throw new Error('Connect not currently allowed') // must wait for `connect_allowed`, or use `createClient` this.on('session', this._connect) @@ -120,6 +135,7 @@ class Client extends Connection { updateCompressorSettings (packet) { this.compressionAlgorithm = packet.compression_algorithm || 'deflate' this.compressionThreshold = packet.compression_threshold + this.compressionReady = true } sendLogin () { @@ -131,9 +147,18 @@ class Client extends Connection { ...this.accessToken // Mojang + Xbox JWT from auth ] - const encodedChain = JSON.stringify({ chain }) - - debug('Auth chain', chain) + let encodedChain + if (this.features.newLoginIdentityFields) { // 1.21.90+ + encodedChain = JSON.stringify({ + Certificate: JSON.stringify({ chain }), + // 0 = normal, 1 = ss, 2 = offline + AuthenticationType: this.options.offline ? 2 : 0, + Token: '' + }) + } else { + encodedChain = JSON.stringify({ chain }) + } + debug('Auth chain', encodedChain) this.write('login', { protocol_version: this.options.protocolVersion, @@ -170,7 +195,8 @@ class Client extends Connection { if (this.status === ClientStatus.Disconnected) return this.write('disconnect', { hide_disconnect_screen: hide, - message: reason + message: reason, + filtered_message: '' }) this.close(reason) } @@ -226,7 +252,9 @@ class Client extends Connection { break case 'start_game': this.startGameData = pakData.params - this.startGameData.itemstates.forEach(state => { + // fallsthrough + case 'item_registry': // 1.21.60+ send itemstates in item_registry packet + pakData.params.itemstates?.forEach(state => { if (state.name === 'minecraft:shield') { this.serializer.proto.setVariable('ShieldItemID', state.runtime_id) this.deserializer.proto.setVariable('ShieldItemID', state.runtime_id) diff --git a/src/connection.js b/src/connection.js index bc1d56b..f1e9051 100644 --- a/src/connection.js +++ b/src/connection.js @@ -28,17 +28,25 @@ class Connection extends EventEmitter { } versionLessThan (version) { + if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version) return this.options.protocolVersion < (typeof version === 'string' ? Versions[version] : version) } versionGreaterThan (version) { + if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version) return this.options.protocolVersion > (typeof version === 'string' ? Versions[version] : version) } versionGreaterThanOrEqualTo (version) { + if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version) return this.options.protocolVersion >= (typeof version === 'string' ? Versions[version] : version) } + versionLessThanOrEqualTo (version) { + if (typeof version === 'string' && !Versions[version]) throw Error('Unknown version: ' + version) + return this.options.protocolVersion <= (typeof version === 'string' ? Versions[version] : version) + } + startEncryption (iv) { this.encryptionEnabled = true this.inLog?.('Started encryption', this.sharedSecret, iv) @@ -62,10 +70,18 @@ class Connection extends EventEmitter { } } + _processOutbound (name, params) { + if (name === 'item_registry') { + this.updateItemPalette(params.itemstates) + } else if (name === 'start_game' && params.itemstates) { + this.updateItemPalette(params.itemstates) + } + } + write (name, params) { this.outLog?.(name, params) - if (name === 'start_game') this.updateItemPalette(params.itemstates) - const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold) + this._processOutbound(name, params) + const batch = new Framer(this) const packet = this.serializer.createPacketBuffer({ name, params }) batch.addEncodedPacket(packet) @@ -78,7 +94,7 @@ class Connection extends EventEmitter { queue (name, params) { this.outLog?.('Q <- ', name, params) - if (name === 'start_game') this.updateItemPalette(params.itemstates) + this._processOutbound(name, params) const packet = this.serializer.createPacketBuffer({ name, params }) if (name === 'level_chunk') { // Skip queue, send ASAP @@ -91,7 +107,7 @@ class Connection extends EventEmitter { _tick () { if (this.sendQ.length) { - const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold) + const batch = new Framer(this) batch.addEncodedPackets(this.sendQ) this.sendQ = [] this.sendIds = [] @@ -115,7 +131,7 @@ class Connection extends EventEmitter { */ sendBuffer (buffer, immediate = false) { if (immediate) { - const batch = new Framer(this.compressionAlgorithm, this.compressionLevel, this.compressionThreshold) + const batch = new Framer(this) batch.addEncodedPacket(buffer) if (this.encryptionEnabled) { this.sendEncryptedBatch(batch) @@ -149,29 +165,29 @@ class Connection extends EventEmitter { // These are callbacks called from encryption.js onEncryptedPacket = (buf) => { - const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header - + const packet = this.batchHeader ? Buffer.concat([Buffer.from([this.batchHeader]), buf]) : buf this.sendMCPE(packet) } onDecryptedPacket = (buf) => { const packets = Framer.getPackets(buf) - for (const packet of packets) { this.readPacket(packet) } } handle (buffer) { // handle encapsulated - if (buffer[0] === 0xfe) { // wrapper + if (!this.batchHeader || buffer[0] === this.batchHeader) { // wrapper if (this.encryptionEnabled) { this.decrypt(buffer.slice(1)) } else { - const packets = Framer.decode(this.compressionAlgorithm, buffer) + const packets = Framer.decode(this, buffer) for (const packet of packets) { this.readPacket(packet) } } + } else { + throw Error('Bad packet header ' + buffer[0]) } } } diff --git a/src/createClient.js b/src/createClient.js index 154a8ae..9d14134 100644 --- a/src/createClient.js +++ b/src/createClient.js @@ -56,35 +56,41 @@ function connect (client) { }) client.queue('client_cache_status', { enabled: false }) - client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + + if (client.versionLessThanOrEqualTo('1.20.80')) client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + sleep(500).then(() => client.queue('request_chunk_radius', { chunk_radius: client.viewDistance || 10 })) }) - // Send tick sync packets every 10 ticks - const keepAliveInterval = 10 - const keepAliveIntervalBig = BigInt(keepAliveInterval) - let keepalive - client.tick = 0n - client.once('spawn', () => { - keepalive = setInterval(() => { - // Client fills out the request_time and the server does response_time in its reply. - client.queue('tick_sync', { request_time: client.tick, response_time: 0n }) - client.tick += keepAliveIntervalBig - }, 50 * keepAliveInterval) + if (client.versionLessThanOrEqualTo('1.20.80')) { + const keepAliveInterval = 10 + const keepAliveIntervalBig = BigInt(keepAliveInterval) - client.on('tick_sync', async packet => { - client.emit('heartbeat', packet.response_time) - client.tick = packet.response_time + let keepalive + client.tick = 0n + + client.once('spawn', () => { + keepalive = setInterval(() => { + // Client fills out the request_time and the server does response_time in its reply. + client.queue('tick_sync', { request_time: client.tick, response_time: 0n }) + client.tick += keepAliveIntervalBig + }, 50 * keepAliveInterval) + + client.on('tick_sync', async packet => { + client.emit('heartbeat', packet.response_time) + client.tick = packet.response_time + }) }) - }) - client.once('close', () => { - clearInterval(keepalive) - }) + client.once('close', () => { + clearInterval(keepalive) + }) + } } async function ping ({ host, port }) { const con = new RakClient({ host, port }) + try { return advertisement.fromServerName(await con.ping()) } finally { diff --git a/src/datatypes/compiler-minecraft.js b/src/datatypes/compiler-minecraft.js index 3dd9483..75fd0b9 100644 --- a/src/datatypes/compiler-minecraft.js +++ b/src/datatypes/compiler-minecraft.js @@ -1,7 +1,7 @@ /* eslint-disable */ const UUID = require('uuid-1345') const minecraft = require('./minecraft') -const { Read, Write, SizeOf } = require('./varlong') +const [Read, Write, SizeOf] = [{}, {}, {}] /** * UUIDs @@ -116,74 +116,6 @@ Read.lnbt = ['native', minecraft.lnbt[0]] Write.lnbt = ['native', minecraft.lnbt[1]] SizeOf.lnbt = ['native', minecraft.lnbt[2]] -/** - * Bits - */ - -Read.bitflags = ['parametrizable', (compiler, { type, flags, shift, big }) => { - let fstr = JSON.stringify(flags) - if (Array.isArray(flags)) { - fstr = '{' - flags.map((v, k) => fstr += `"${v}": ${big ? 1n << BigInt(k) : 1 << k}` + (big ? 'n,' : ',')) - fstr += '}' - } else if (shift) { - fstr = '{' - for (const key in flags) fstr += `"${key}": ${1 << flags[key]},`; - fstr += '}' - } - return compiler.wrapCode(` - const { value: _value, size } = ${compiler.callType(type, 'offset')} - const value = { _value } - const flags = ${fstr} - for (const key in flags) { - value[key] = (_value & flags[key]) == flags[key] - } - return { value, size } - `.trim()) -}] - -Write.bitflags = ['parametrizable', (compiler, { type, flags, shift, big }) => { - let fstr = JSON.stringify(flags) - if (Array.isArray(flags)) { - fstr = '{' - flags.map((v, k) => fstr += `"${v}": ${big ? 1n << BigInt(k) : 1 << k}` + (big ? 'n,' : ',')) - fstr += '}' - } else if (shift) { - fstr = '{' - for (const key in flags) fstr += `"${key}": ${1 << flags[key]},`; - fstr += '}' - } - return compiler.wrapCode(` - const flags = ${fstr} - let val = value._value ${big ? '|| 0n' : ''} - for (const key in flags) { - if (value[key]) val |= flags[key] - } - return (ctx.${type})(val, buffer, offset) - `.trim()) -}] - -SizeOf.bitflags = ['parametrizable', (compiler, { type, flags, shift, big }) => { - let fstr = JSON.stringify(flags) - if (Array.isArray(flags)) { - fstr = '{' - flags.map((v, k) => fstr += `"${v}": ${big ? 1n << BigInt(k) : 1 << k}` + (big ? 'n,' : ',')) - fstr += '}' - } else if (shift) { - fstr = '{' - for (const key in flags) fstr += `"${key}": ${1 << flags[key]},`; - fstr += '}' - } - return compiler.wrapCode(` - const flags = ${fstr} - let val = value._value ${big ? '|| 0n' : ''} - for (const key in flags) { - if (value[key]) val |= flags[key] - } - return (ctx.${type})(val) - `.trim()) -}] - /** * Command Packet * - used for determining the size of the following enum diff --git a/src/datatypes/minecraft.js b/src/datatypes/minecraft.js index 1a9186d..117e5ff 100644 --- a/src/datatypes/minecraft.js +++ b/src/datatypes/minecraft.js @@ -1,14 +1,11 @@ -/* eslint-disable */ const nbt = require('prismarine-nbt') const UUID = require('uuid-1345') const protoLE = nbt.protos.little const protoLEV = nbt.protos.littleVarint -// TODO: deal with this: -const zigzag = require('prismarine-nbt/zigzag') function readUUID (buffer, offset) { - if (offset + 16 > buffer.length) { throw new PartialReadError() } + if (offset + 16 > buffer.length) { throw new Error('Reached end of buffer') } return { value: UUID.stringify(buffer.slice(offset, 16 + offset)), size: 16 @@ -65,7 +62,7 @@ function readEntityMetadata (buffer, offset, _ref) { const metadata = [] let item while (true) { - if (offset + 1 > buffer.length) throw new PartialReadError() + if (offset + 1 > buffer.length) throw new Error('Reached end of buffer') item = buffer.readUInt8(cursor) if (item === endVal) { return { @@ -159,7 +156,5 @@ module.exports = { lnbt: [readNbtLE, writeNbtLE, sizeOfNbtLE], entityMetadataLoop: [readEntityMetadata, writeEntityMetadata, sizeOfEntityMetadata], ipAddress: [readIpAddress, writeIpAddress, 4], - endOfArray: [readEndOfArray, writeEndOfArray, sizeOfEndOfArray], - zigzag32: zigzag.interpret.zigzag32, - zigzag64: zigzag.interpret.zigzag64 + endOfArray: [readEndOfArray, writeEndOfArray, sizeOfEndOfArray] } diff --git a/src/datatypes/varlong.js b/src/datatypes/varlong.js deleted file mode 100644 index 5e57ad7..0000000 --- a/src/datatypes/varlong.js +++ /dev/null @@ -1,63 +0,0 @@ -function sizeOfVarLong (value) { - if (typeof value.valueOf() === 'object') { - value = (BigInt(value[0]) << 32n) | BigInt(value[1]) - } else if (typeof value !== 'bigint') value = BigInt(value) - - let cursor = 0 - while (value > 127n) { - value >>= 7n - cursor++ - } - return cursor + 1 -} - -/** - * Reads a 64-bit VarInt as a BigInt - */ -function readVarLong (buffer, offset) { - let result = BigInt(0) - let shift = 0n - let cursor = offset - let size = 0 - - while (true) { - if (cursor + 1 > buffer.length) { throw new Error('unexpected buffer end') } - const b = buffer.readUInt8(cursor) - result |= (BigInt(b) & 0x7fn) << shift // Add the bits to our number, except MSB - cursor++ - if (!(b & 0x80)) { // If the MSB is not set, we return the number - size = cursor - offset - break - } - shift += 7n // we only have 7 bits, MSB being the return-trigger - if (shift > 63n) throw new Error(`varint is too big: ${shift}`) - } - - return { value: result, size } -} - -/** - * Writes a zigzag encoded 64-bit VarInt as a BigInt - */ -function writeVarLong (value, buffer, offset) { - // if an array, turn it into a BigInt - if (typeof value.valueOf() === 'object') { - value = BigInt.asIntN(64, (BigInt(value[0]) << 32n)) | BigInt(value[1]) - } else if (typeof value !== 'bigint') value = BigInt(value) - - let cursor = 0 - while (value > 127n) { // keep writing in 7 bit slices - const num = Number(value & 0xFFn) - buffer.writeUInt8(num | 0x80, offset + cursor) - cursor++ - value >>= 7n - } - buffer.writeUInt8(Number(value), offset + cursor) - return offset + cursor + 1 -} - -module.exports = { - Read: { varint64: ['native', readVarLong] }, - Write: { varint64: ['native', writeVarLong] }, - SizeOf: { varint64: ['native', sizeOfVarLong] } -} diff --git a/src/handshake/login.js b/src/handshake/login.js index 0a921fb..8a2830d 100644 --- a/src/handshake/login.js +++ b/src/handshake/login.js @@ -36,7 +36,6 @@ module.exports = (client, server, options) => { client.createClientUserChain = (privateKey) => { let payload = { ...skinData, - SkinGeometryDataEngineVersion: client.versionGreaterThanOrEqualTo('1.17.30') ? '' : undefined, ClientRandomId: Date.now(), CurrentInputMode: 1, @@ -47,25 +46,30 @@ module.exports = (client, server, options) => { GameVersion: options.version || '1.16.201', GuiScale: -1, LanguageCode: 'en_GB', // TODO locale + GraphicsMode: 1, // 1:simple, 2:fancy, 3:advanced, 4:ray_traced PlatformOfflineId: '', PlatformOnlineId: '', // chat // PlayFabID is the PlayFab ID produced for the skin. PlayFab is the company that hosts the Marketplace, // skins and other related features from the game. This ID is the ID of the skin used to store the skin - // inside of PlayFab. - PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16), // 1.16.210 + // inside of PlayFab.The playfab ID is always lowercased. + PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16).toLowerCase(), // 1.16.210 SelfSignedId: nextUUID(), ServerAddress: `${options.host}:${options.port}`, - ThirdPartyName: client.profile.name, - ThirdPartyNameOnly: false, + ThirdPartyName: client.profile.name, // Gamertag + ThirdPartyNameOnly: client.versionGreaterThanOrEqualTo('1.21.90') ? undefined : false, UIProfile: 0, IsEditorMode: false, TrustedSkin: client.versionGreaterThanOrEqualTo('1.19.20') ? false : undefined, OverrideSkin: client.versionGreaterThanOrEqualTo('1.19.62') ? false : undefined, - CompatibleWithClientSideChunkGen: client.versionGreaterThanOrEqualTo('1.19.80') ? false : undefined + CompatibleWithClientSideChunkGen: client.versionGreaterThanOrEqualTo('1.19.80') ? false : undefined, + + MaxViewDistance: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined, + MemoryTier: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined, + PlatformType: client.versionGreaterThanOrEqualTo('1.21.42') ? 0 : undefined } const customPayload = options.skinData || {} payload = { ...payload, ...customPayload } diff --git a/src/options.js b/src/options.js index fb285bd..6e27dbe 100644 --- a/src/options.js +++ b/src/options.js @@ -3,12 +3,12 @@ const mcData = require('minecraft-data') // Minimum supported version (< will be kicked) const MIN_VERSION = '1.16.201' // Currently supported verson. Note, clients with newer versions can still connect as long as data is in minecraft-data -const CURRENT_VERSION = '1.20.50' +const CURRENT_VERSION = '1.21.111' const Versions = Object.fromEntries(mcData.versions.bedrock.filter(e => e.releaseType === 'release').map(e => [e.minecraftVersion, e.version])) // Skip some low priority versions (middle major) on Github Actions to allow faster CI -const skippedVersionsOnGithubCI = ['1.16.210', '1.17.10', '1.17.30', '1.18.11', '1.19.10', '1.19.20', '1.19.30', '1.19.40', '1.19.50', '1.19.60', '1.19.63', '1.19.70', '1.20.10'] +const skippedVersionsOnGithubCI = ['1.16.210', '1.17.10', '1.17.30', '1.18.11', '1.19.10', '1.19.20', '1.19.30', '1.19.40', '1.19.50', '1.19.60', '1.19.63', '1.19.70', '1.20.10', '1.20.15', '1.20.30', '1.20.40', '1.20.50', '1.20.61', '1.20.71', '1.21.2', '1.21.20', '1.21.30', '1.21.42', '1.21.50', '1.21.60', '1.21.70', '1.21.80', '1.21.90'] const testedVersions = process.env.CI ? Object.keys(Versions).filter(v => !skippedVersionsOnGithubCI.includes(v)) : Object.keys(Versions) const defaultOptions = { diff --git a/src/server.js b/src/server.js index 5476455..0b43fd2 100644 --- a/src/server.js +++ b/src/server.js @@ -15,6 +15,7 @@ class Server extends EventEmitter { this.RakServer = require('./rak')(this.options.raknetBackend).RakServer + this._loadFeatures(this.options.version) this.serializer = createSerializer(this.options.version) this.deserializer = createDeserializer(this.options.version) this.advertisement = new ServerAdvertisement(this.options.motd, this.options.port, this.options.version) @@ -23,25 +24,41 @@ class Server extends EventEmitter { this.clients = {} this.clientCount = 0 this.conLog = debug + this.batchHeader = 0xfe this.setCompressor(this.options.compressionAlgorithm, this.options.compressionLevel, this.options.compressionThreshold) } + _loadFeatures (version) { + try { + const mcData = require('minecraft-data')('bedrock_' + version) + this.features = { + compressorInHeader: mcData.supportFeature('compressorInPacketHeader'), + newLoginIdentityFields: mcData.supportFeature('newLoginIdentityFields') + } + } catch (e) { + throw new Error(`Unsupported version: '${version}', no data available`) + } + } + setCompressor (algorithm, level = 1, threshold = 256) { switch (algorithm) { case 'none': this.compressionAlgorithm = 'none' this.compressionLevel = 0 + this.compressionHeader = 255 break case 'deflate': this.compressionAlgorithm = 'deflate' this.compressionLevel = level this.compressionThreshold = threshold + this.compressionHeader = 0 break case 'snappy': this.compressionAlgorithm = 'snappy' this.compressionLevel = level this.compressionThreshold = threshold + this.compressionHeader = 1 break default: throw new Error(`Unknown compression algorithm: ${algorithm}`) @@ -73,12 +90,10 @@ class Server extends EventEmitter { this.emit('connect', player) } - onCloseConnection = (inetAddr, reason) => { - this.conLog('Connection closed: ', inetAddr?.address, reason) - - delete this.clients[inetAddr]?.connection // Prevent close loop - this.clients[inetAddr?.address ?? inetAddr]?.close() - delete this.clients[inetAddr] + onCloseConnection = (conn, reason) => { + this.conLog('Connection closed: ', conn.address, reason) + this.clients[conn.address]?.close() + delete this.clients[conn.address] this.clientCount-- } @@ -102,8 +117,9 @@ class Server extends EventEmitter { return this.advertisement } - async listen (host = this.options.host, port = this.options.port) { - this.raknet = new this.RakServer({ host, port }, this) + async listen () { + const { host, port, maxPlayers } = this.options + this.raknet = new this.RakServer({ host, port, maxPlayers }, this) try { await this.raknet.listen() diff --git a/src/serverPlayer.js b/src/serverPlayer.js index ffb1961..2e8ea77 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -10,6 +10,7 @@ class Player extends Connection { constructor (server, connection) { super() this.server = server + this.features = server.features this.serializer = server.serializer this.deserializer = server.deserializer this.connection = connection @@ -23,14 +24,16 @@ class Player extends Connection { this.status = ClientStatus.Authenticating if (isDebug) { - this.inLog = (...args) => debug('S ->', ...args) - this.outLog = (...args) => debug('S <-', ...args) + this.inLog = (...args) => debug('-> S', ...args) + this.outLog = (...args) => debug('<- S', ...args) } + this.batchHeader = this.server.batchHeader // Compression is server-wide this.compressionAlgorithm = this.server.compressionAlgorithm this.compressionLevel = this.server.compressionLevel this.compressionThreshold = this.server.compressionThreshold + this.compressionHeader = this.server.compressionHeader this._sentNetworkSettings = false // 1.19.30+ } @@ -48,6 +51,7 @@ class Player extends Connection { client_throttle_scalar: 0 }) this._sentNetworkSettings = true + this.compressionReady = true } handleClientProtocolVersion (clientVersion) { @@ -74,11 +78,18 @@ class Player extends Connection { // Parse login data const tokens = body.params.tokens - const authChain = JSON.parse(tokens.identity) - const skinChain = tokens.client - try { - var { key, userData, skinData } = this.decodeLoginJWT(authChain.chain, skinChain) // eslint-disable-line + const skinChain = tokens.client + const authChain = JSON.parse(tokens.identity) + let chain + if (authChain.Certificate) { // 1.21.90+ + chain = JSON.parse(authChain.Certificate).chain + } else if (authChain.chain) { + chain = authChain.chain + } else { + throw new Error('Invalid login packet: missing chain or Certificate') + } + var { key, userData, skinData } = this.decodeLoginJWT(chain, skinChain) // eslint-disable-line } catch (e) { debug(this.address, e) this.disconnect('Server authentication error') @@ -115,7 +126,8 @@ class Player extends Connection { if (this.status === ClientStatus.Disconnected) return this.write('disconnect', { hide_disconnect_screen: hide, - message: reason + message: reason, + filtered_message: '' }) this.server.conLog('Kicked ', this.connection?.address, reason) setTimeout(() => this.close('kick'), 100) // Allow time for message to be recieved. @@ -152,7 +164,7 @@ class Player extends Connection { return } - this.inLog?.(des.data.name, serialize(des.data.params).slice(0, 200)) + this.inLog?.(des.data.name, serialize(des.data.params)) switch (des.data.name) { // This is the first packet on 1.19.30 & above diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index b02351b..ff067cc 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -36,7 +36,10 @@ function createEncryptor (client, iv) { // The send counter is represented as a little-endian 64-bit long and incremented after each packet. function process (chunk) { - const buffer = Zlib.deflateRawSync(chunk, { level: client.compressionLevel }) + const compressed = Zlib.deflateRawSync(chunk, { level: client.compressionLevel }) + const buffer = client.features.compressorInHeader + ? Buffer.concat([Buffer.from([0]), compressed]) + : compressed const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)]) client.sendCounter++ client.cipher.write(packet) @@ -70,7 +73,22 @@ function createDecryptor (client, iv) { return } - const buffer = Zlib.inflateRawSync(chunk, { chunkSize: 512000 }) + let buffer + if (client.features.compressorInHeader) { + switch (packet[0]) { + case 0: + buffer = Zlib.inflateRawSync(packet.slice(1), { chunkSize: 512000 }) + break + case 255: + buffer = packet.slice(1) + break + default: + client.emit('error', Error(`Unsupported compressor: ${packet[0]}`)) + } + } else { + buffer = Zlib.inflateRawSync(packet, { chunkSize: 512000 }) + } + client.onDecryptedPacket(buffer) } diff --git a/src/transforms/framer.js b/src/transforms/framer.js index 2452f01..a2d9c4f 100644 --- a/src/transforms/framer.js +++ b/src/transforms/framer.js @@ -3,12 +3,15 @@ const zlib = require('zlib') // Concatenates packets into one batch packet, and adds length prefixs. class Framer { - constructor (compressor, compressionLevel, compressionThreshold) { + constructor (client) { // Encoding this.packets = [] - this.compressor = compressor || 'none' - this.compressionLevel = compressionLevel - this.compressionThreshold = compressionThreshold + this.batchHeader = client.batchHeader + this.compressor = client.compressionAlgorithm || 'none' + this.compressionLevel = client.compressionLevel + this.compressionThreshold = client.compressionThreshold + this.compressionHeader = client.compressionHeader || 0 + this.writeCompressor = client.features.compressorInHeader && client.compressionReady } // No compression in base class @@ -21,30 +24,46 @@ class Framer { } static decompress (algorithm, buffer) { - try { - switch (algorithm) { - case 'deflate': return zlib.inflateRawSync(buffer, { chunkSize: 512000 }) - case 'snappy': throw Error('Snappy compression not implemented') - case 'none': return buffer - default: throw Error('Unknown compression type ' + this.compressor) - } - } catch { - return buffer + switch (algorithm) { + case 0: + case 'deflate': + return zlib.inflateRawSync(buffer, { chunkSize: 512000 }) + case 1: + case 'snappy': + throw Error('Snappy compression not implemented') + case 'none': + case 255: + return buffer + default: throw Error('Unknown compression type ' + algorithm) } } - static decode (compressor, buf) { + static decode (client, buf) { // Read header - if (buf[0] !== 0xfe) throw Error('bad batch packet header ' + buf[0]) + if (this.batchHeader && buf[0] !== this.batchHeader) throw Error(`bad batch packet header, received: ${buf[0]}, expected: ${this.batchHeader}`) const buffer = buf.slice(1) - const decompressed = this.decompress(compressor, buffer) + // Decompress + let decompressed + if (client.features.compressorInHeader && client.compressionReady) { + decompressed = this.decompress(buffer[0], buffer.slice(1)) + } else { + // On old versions, compressor is session-wide ; failing to decompress + // a packet will assume it's not compressed + try { + decompressed = this.decompress(client.compressionAlgorithm, buffer) + } catch (e) { + decompressed = buffer + } + } return Framer.getPackets(decompressed) } encode () { const buf = Buffer.concat(this.packets) - const compressed = (buf.length > this.compressionThreshold) ? this.compress(buf) : buf - return Buffer.concat([Buffer.from([0xfe]), compressed]) + const shouldCompress = buf.length > this.compressionThreshold + const header = this.batchHeader ? [this.batchHeader] : [] + if (this.writeCompressor) header.push(shouldCompress ? this.compressionHeader : 255) + return Buffer.concat([Buffer.from(header), shouldCompress ? this.compress(buf) : buf]) } addEncodedPacket (chunk) { diff --git a/src/transforms/serializer.js b/src/transforms/serializer.js index 91d4c97..8fffc42 100644 --- a/src/transforms/serializer.js +++ b/src/transforms/serializer.js @@ -37,7 +37,6 @@ function createProtocol (version) { const compiler = new ProtoDefCompiler() compiler.addTypesToCompile(protocol.types) compiler.addTypes(require('../datatypes/compiler-minecraft')) - compiler.addTypes(require('prismarine-nbt/zigzag').compiler) const compiledProto = compiler.compileProtoDefSync() return compiledProto @@ -47,7 +46,6 @@ function createProtocol (version) { function getProtocol (version) { const compiler = new ProtoDefCompiler() compiler.addTypes(require(join(__dirname, '../datatypes/compiler-minecraft'))) - compiler.addTypes(require('prismarine-nbt/zigzag').compiler) global.PartialReadError = require('protodef/src/utils').PartialReadError const compile = (compiler, file) => require(file)(compiler.native) diff --git a/test/internal.js b/test/internal.js index 6b37d95..ffb403e 100644 --- a/test/internal.js +++ b/test/internal.js @@ -14,7 +14,7 @@ function prepare (version) { async function startTest (version = CURRENT_VERSION, ok) { await prepare(version) - const Item = require('../types/Item')(version) + // const Item = require('../types/Item')(version) const port = await getPort() const server = new Server({ host: '0.0.0.0', port, version, offline: true }) @@ -47,6 +47,7 @@ async function startTest (version = CURRENT_VERSION, ok) { must_accept: false, has_scripts: false, behaviour_packs: [], + world_template: { uuid: '550e8400-e29b-41d4-a716-446655440000', version: '' }, // 1.21.50 texture_packs: [], resource_pack_links: [] }) @@ -56,13 +57,17 @@ async function startTest (version = CURRENT_VERSION, ok) { client.write('network_settings', { compression_threshold: 1 }) // Send some inventory slots for (let i = 0; i < 3; i++) { - client.queue('inventory_slot', { window_id: 'armor', slot: 0, item: new Item().toBedrock() }) + // client.queue('inventory_slot', { window_id: 'armor', slot: 0, item: new Item().toBedrock() }) } // client.queue('inventory_transaction', get('packets/inventory_transaction.json')) client.queue('player_list', get('packets/player_list.json')) client.queue('start_game', get('packets/start_game.json')) - client.queue('item_component', { entries: [] }) + if (client.versionLessThan('1.21.60')) { + client.queue('item_component', { entries: [] }) + } else { + client.queue('item_registry', get('packets/item_registry.json')) + } client.queue('set_spawn_position', get('packets/set_spawn_position.json')) client.queue('set_time', { time: 5433771 }) client.queue('set_difficulty', { difficulty: 1 }) @@ -96,11 +101,11 @@ async function startTest (version = CURRENT_VERSION, ok) { loop = setInterval(() => { client.write('network_chunk_publisher_update', { coordinates: { x: 646, y: 130, z: 77 }, radius: 64 }) - }, 9500) + }, 6500) setTimeout(() => { client.write('play_status', { status: 'player_spawn' }) - }, 6000) + }, 3000) // Respond to tick synchronization packets client.on('tick_sync', (packet) => { diff --git a/test/internal.test.js b/test/internal.test.js index 1b6250c..271a7c5 100644 --- a/test/internal.test.js +++ b/test/internal.test.js @@ -3,6 +3,7 @@ const { timedTest } = require('./internal') const { testedVersions } = require('../src/options') const { sleep } = require('../src/datatypes/util') +require('events').captureRejections = true describe('internal client/server test', function () { const vcount = testedVersions.length diff --git a/test/proxy.js b/test/proxy.js index 345e89f..af443d7 100644 --- a/test/proxy.js +++ b/test/proxy.js @@ -22,12 +22,12 @@ function proxyTest (version, raknetBackend = 'raknet-native', timeout = 1000 * 4 console.debug('Client has authenticated') setTimeout(() => { client.disconnect('Hello world !') - }, 1000) // allow some time for client to connect + }, 500) // allow some time for client to connect }) }) console.debug('Server started', server.options.version) - await new Promise(resolve => setTimeout(resolve, 1000)) + await new Promise(resolve => setTimeout(resolve, 500)) const relay = new Relay({ version, @@ -46,7 +46,7 @@ function proxyTest (version, raknetBackend = 'raknet-native', timeout = 1000 * 4 await relay.listen() console.debug('Proxy started', server.options.version) - await new Promise(resolve => setTimeout(resolve, 1000)) + await new Promise(resolve => setTimeout(resolve, 500)) const client = createClient({ host: '127.0.0.1', port: CLIENT_PORT, version, username: 'Boat', offline: true, raknetBackend, skipPing: true }) console.debug('Client started') @@ -58,7 +58,7 @@ function proxyTest (version, raknetBackend = 'raknet-native', timeout = 1000 * 4 server.close() relay.close() console.log('✔ OK') - sleep(500).then(res) + sleep(200).then(res) }) }, timeout, () => { throw Error('timed out') }) } diff --git a/test/proxy.test.js b/test/proxy.test.js index 70ea5f1..27761ae 100644 --- a/test/proxy.test.js +++ b/test/proxy.test.js @@ -11,7 +11,7 @@ describe('proxies client/server', function () { it('proxies ' + version, async () => { console.debug(version) await proxyTest(version) - await sleep(1000) + await sleep(100) console.debug('Done', version) }) } diff --git a/test/util.js b/test/util.js index 2876a67..3325863 100644 --- a/test/util.js +++ b/test/util.js @@ -5,7 +5,12 @@ const getPort = () => new Promise(resolve => { server.listen(0, '127.0.0.1') server.on('listening', () => { const { port } = server.address() - server.close(() => resolve(port)) + server.close(() => { + // Wait a bit for port to free as we try to bind right after freeing it + setTimeout(() => { + resolve(port) + }, 200) + }) }) }) diff --git a/test/vanilla.js b/test/vanilla.js index 7a24008..73a5f44 100644 --- a/test/vanilla.js +++ b/test/vanilla.js @@ -5,11 +5,12 @@ const { waitFor } = require('../src/datatypes/util') const { getPort } = require('./util') async function test (version) { - const ChunkColumn = require('bedrock-provider').chunk('bedrock_' + (version.includes('1.19') ? '1.18.30' : version)) // TODO: Fix prismarine-chunk + // const ChunkColumn = require('bedrock-provider').chunk('bedrock_' + (version.includes('1.19') ? '1.18.30' : version)) // TODO: Fix prismarine-chunk // Start the server, wait for it to accept clients, throws on timeout - const port = await getPort() - const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port }) + const [port, v6] = [await getPort(), await getPort()] + console.log('Starting vanilla server', version, 'on port', port, v6) + const handle = await vanillaServer.startServerAndWait2(version, 1000 * 220, { 'server-port': port, 'server-portv6': v6 }) console.log('Started server') const client = new Client({ @@ -48,10 +49,10 @@ async function test (version) { client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) }) }, 200) - client.on('level_chunk', async packet => { // Chunk read test - const cc = new ChunkColumn(packet.x, packet.z) - await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count) - }) + // client.on('level_chunk', async packet => { // Chunk read test + // const cc = new ChunkColumn(packet.x, packet.z) + // await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count) + // }) console.log('Awaiting join') diff --git a/tools/compileProtocol.js b/tools/compileProtocol.js index dcf6545..a7c3aa0 100644 --- a/tools/compileProtocol.js +++ b/tools/compileProtocol.js @@ -15,7 +15,6 @@ function createProtocol (version) { const compiler = new ProtoDefCompiler() const protocol = mcData('bedrock_' + version).protocol.types compiler.addTypes(require('../src/datatypes/compiler-minecraft')) - compiler.addTypes(require('prismarine-nbt/zigzag').compiler) compiler.addTypesToCompile(protocol) fs.writeFileSync('./read.js', 'module.exports = ' + compiler.readCompiler.generate().replace('() =>', 'native =>')) @@ -39,7 +38,7 @@ require('minecraft-data/bin/generate_data') // If no argument, build everything if (!process.argv[2]) { - convert('latest') + convert('bedrock', 'latest') for (const version of versions) { main(version) } diff --git a/tools/genPacketDumps.js b/tools/genPacketDumps.js index 2e6420f..f0edbfe 100644 --- a/tools/genPacketDumps.js +++ b/tools/genPacketDumps.js @@ -24,7 +24,7 @@ async function dump (version, force = true) { const random = (Math.random() * 1000) | 0 const [port, v6] = [await getPort(), await getPort()] - console.log('Starting dump server', version) + console.log('Starting dump server', version, 'on port', port, v6) const handle = await vanillaServer.startServerAndWait2(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port, 'server-portv6': v6 }) console.log('Started dump server', version) diff --git a/tools/startVanillaServer.js b/tools/startVanillaServer.js index 6d77006..828e332 100644 --- a/tools/startVanillaServer.js +++ b/tools/startVanillaServer.js @@ -1,156 +1,11 @@ -const http = require('https') -const fs = require('fs') -const cp = require('child_process') -const debug = process.env.CI ? console.debug : require('debug')('minecraft-protocol') -const https = require('https') -const { getFiles, waitFor } = require('../src/datatypes/util') +const bedrockServer = require('minecraft-bedrock-server') -function head (url) { - return new Promise((resolve, reject) => { - const req = http.request(url, { method: 'HEAD', timeout: 1000 }, resolve) - req.on('error', reject) - req.on('timeout', () => { req.destroy(); debug('HEAD request timeout'); reject(new Error('timeout')) }) - req.end() - }) -} -function get (url, outPath) { - const file = fs.createWriteStream(outPath) - return new Promise((resolve, reject) => { - https.get(url, { timeout: 1000 * 20 }, response => { - if (response.statusCode !== 200) return reject(new Error('Server returned code ' + response.statusCode)) - response.pipe(file) - file.on('finish', () => { - file.close() - resolve() - }) - }) - }) -} - -// Get the latest versions -// TODO: once we support multi-versions -function fetchLatestStable () { - get('https://raw.githubusercontent.com/minecraft-linux/mcpelauncher-versiondb/master/versions.json', 'versions.json') - const versions = JSON.parse(fs.readFileSync('./versions.json')) - const latest = versions[0] - return latest.version_name -} - -// Download + extract vanilla server and enter the directory -async function download (os, version, path = 'bds-') { - debug('Downloading server', os, version, 'into', path) - process.chdir(__dirname) - const verStr = version.split('.').slice(0, 3).join('.') - const dir = path + version - - if (fs.existsSync(dir) && getFiles(dir).length) { - process.chdir(path + version) // Enter server folder - return verStr - } - try { fs.mkdirSync(dir) } catch { } - - process.chdir(path + version) // Enter server folder - const url = (os, version) => `https://minecraft.azureedge.net/bin-${os}/bedrock-server-${version}.zip` - - let found = false - - for (let i = 0; i < 8; i++) { // Check for the latest server build for version (major.minor.patch.BUILD) - const u = url(os, `${verStr}.${String(i).padStart(2, '0')}`) - debug('Opening', u, Date.now()) - let ret - try { ret = await head(u) } catch (e) { continue } - if (ret.statusCode === 200) { - found = u - debug('Found server', ret.statusCode) - break - } - } - if (!found) throw Error('did not find server bin for ' + os + ' ' + version) - console.info('🔻 Downloading', found) - await get(found, 'bds.zip') - console.info('⚡ Unzipping') - // Unzip server - if (process.platform === 'linux') cp.execSync('unzip -u bds.zip && chmod +777 ./bedrock_server') - else cp.execSync('tar -xf bds.zip') - return verStr -} - -const defaultOptions = { - 'level-generator': '2', - 'server-port': '19130', - 'online-mode': 'false' -} - -// Setup the server -function configure (options = {}) { - const opts = { ...defaultOptions, ...options } - let config = fs.readFileSync('./server.properties', 'utf-8') - config += '\nplayer-idle-timeout=1\nallow-cheats=true\ndefault-player-permission-level=operator' - for (const o in opts) config += `\n${o}=${opts[o]}` - fs.writeFileSync('./server.properties', config) -} - -function run (inheritStdout = true) { - const exe = process.platform === 'win32' ? 'bedrock_server.exe' : './bedrock_server' - return cp.spawn(exe, inheritStdout ? { stdio: 'inherit' } : {}) -} - -let lastHandle - -// Run the server -async function startServer (version, onStart, options = {}) { - const os = process.platform === 'win32' ? 'win' : process.platform - if (os !== 'win' && os !== 'linux') { - throw Error('unsupported os ' + os) - } - await download(os, version, options.path) - configure(options) - const handle = lastHandle = run(!onStart) - handle.on('error', (...a) => { - console.warn('*** THE MINECRAFT PROCESS CRASHED ***', a) - handle.kill('SIGKILL') - }) - if (onStart) { - let stdout = '' - handle.stdout.on('data', data => { - stdout += data - if (stdout.includes('Server started')) onStart() - }) - handle.stdout.pipe(process.stdout) - handle.stderr.pipe(process.stdout) - } - return handle -} - -// Start the server and wait for it to be ready, with a timeout -async function startServerAndWait (version, withTimeout, options) { - let handle - await waitFor(async res => { - handle = await startServer(version, res, options) - }, withTimeout, () => { - handle?.kill() - throw new Error(`Server did not start on time (${withTimeout}ms, now ${Date.now()})`) - }) - return handle -} - -async function startServerAndWait2 (version, withTimeout, options) { - try { - return await startServerAndWait(version, 1000 * 60, options) - } catch (e) { - console.log(e) - console.log('^ Tring once more to start server in 10 seconds...') - lastHandle?.kill() - await new Promise(resolve => setTimeout(resolve, 10000)) - process.chdir(__dirname) - fs.rmSync('bds-' + version, { recursive: true }) - return await startServerAndWait(version, withTimeout, options) +module.exports = { + ...bedrockServer, + startServerAndWait (version, withTimeout, options) { + return bedrockServer.startServerAndWait(version, withTimeout, { ...options, root: __dirname }) + }, + startServerAndWait2 (version, withTimeout, options) { + return bedrockServer.startServerAndWait2(version, withTimeout, { ...options, root: __dirname }) } } - -if (!module.parent) { - // if (process.argv.length < 3) throw Error('Missing version argument') - startServer(process.argv[2] || '1.17.10', null, process.argv[3] ? { 'server-port': process.argv[3], 'online-mode': !!process.argv[4] } : undefined) -} - -module.exports = { fetchLatestStable, startServer, startServerAndWait, startServerAndWait2 }