diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 150bd8a..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: diff --git a/.github/workflows/update-helper.yml b/.github/workflows/update-helper.yml index 168dd04..d9380da 100644 --- a/.github/workflows/update-helper.yml +++ b/.github/workflows/update-helper.yml @@ -14,7 +14,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@master with: - node-version: 18.0.0 + 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 diff --git a/HISTORY.md b/HISTORY.md index 7182963..799d0a9 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,3 +1,35 @@ +## 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 diff --git a/README.md b/README.md index 25971a1..5e3add2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Minecraft Bedrock Edition (aka MCPE) protocol library, supporting authentication ## 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, 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 + - 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) 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/index.d.ts b/index.d.ts index cda2551..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.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' + 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 diff --git a/package.json b/package.json index 18573d6..6301be0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bedrock-protocol", - "version": "3.42.2", + "version": "3.49.0", "description": "Minecraft Bedrock Edition protocol library", "main": "index.js", "types": "index.d.ts", @@ -41,7 +41,7 @@ "bedrock-provider": "^2.0.0", "leveldb-zlib": "^1.0.1", "minecraft-bedrock-server": "^1.4.2", - "mocha": "^10.0.0", + "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 2d798e6..e3af1d0 100644 --- a/src/client.js +++ b/src/client.js @@ -61,7 +61,9 @@ class Client extends Connection { try { const mcData = require('minecraft-data')('bedrock_' + this.options.version) this.features = { - compressorInHeader: mcData.supportFeature('compressorInPacketHeader') + 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`) @@ -145,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, @@ -241,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 eb01f7f..f1e9051 100644 --- a/src/connection.js +++ b/src/connection.js @@ -28,18 +28,22 @@ 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) } @@ -66,9 +70,17 @@ 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) + this._processOutbound(name, params) const batch = new Framer(this) const packet = this.serializer.createPacketBuffer({ name, params }) batch.addEncodedPacket(packet) @@ -82,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 diff --git a/src/handshake/login.js b/src/handshake/login.js index e25b15f..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,19 +46,20 @@ 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, @@ -67,9 +67,9 @@ module.exports = (client, server, options) => { OverrideSkin: client.versionGreaterThanOrEqualTo('1.19.62') ? false : undefined, CompatibleWithClientSideChunkGen: client.versionGreaterThanOrEqualTo('1.19.80') ? false : undefined, - MaxViewDistance: client.versionGreaterThanOrEqualTo('1.21.40') ? 0 : undefined, - MemoryTier: client.versionGreaterThanOrEqualTo('1.21.40') ? 0 : undefined, - PlatformType: client.versionGreaterThanOrEqualTo('1.21.40') ? 0 : 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 dd42cdf..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.21.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', '1.20.15', '1.20.30', '1.20.40', '1.20.50', '1.20.61', '1.20.71'] +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 4dc9852..0b43fd2 100644 --- a/src/server.js +++ b/src/server.js @@ -33,7 +33,8 @@ class Server extends EventEmitter { try { const mcData = require('minecraft-data')('bedrock_' + version) this.features = { - compressorInHeader: mcData.supportFeature('compressorInPacketHeader') + compressorInHeader: mcData.supportFeature('compressorInPacketHeader'), + newLoginIdentityFields: mcData.supportFeature('newLoginIdentityFields') } } catch (e) { throw new Error(`Unsupported version: '${version}', no data available`) @@ -89,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-- } diff --git a/src/serverPlayer.js b/src/serverPlayer.js index b7ef02d..2e8ea77 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -78,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') diff --git a/test/internal.js b/test/internal.js index 1003a87..ffb403e 100644 --- a/test/internal.js +++ b/test/internal.js @@ -63,7 +63,11 @@ async function startTest (version = CURRENT_VERSION, ok) { // 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 }) @@ -97,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/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) }