diff --git a/data/1.16.201/protocol.json b/data/1.16.201/protocol.json index 4c90341..2dd5be8 100644 --- a/data/1.16.201/protocol.json +++ b/data/1.16.201/protocol.json @@ -1048,23 +1048,24 @@ } ] ], + "ItemStack": [ + "container", + [ + { + "name": "runtime_id", + "type": "zigzag32" + }, + { + "name": "item", + "type": "Item" + } + ] + ], "ItemStacks": [ "array", { "countType": "varint", - "type": [ - "container", - [ - { - "name": "runtime_id", - "type": "zigzag32" - }, - { - "name": "item", - "type": "Item" - } - ] - ] + "type": "ItemStack" } ], "RecipeIngredient": [ @@ -4600,13 +4601,9 @@ "name": "slot", "type": "varint" }, - { - "name": "uniqueid", - "type": "zigzag32" - }, { "name": "item", - "type": "Item" + "type": "ItemStack" } ] ], diff --git a/package.json b/package.json index 9de80f9..9984792 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Parse and serialize Minecraft Bedrock Edition packets", "main": "index.js", "scripts": { - "build": "cd data/new && node compile.js", + "build": "cd tools && node compileProtocol.js", "prepare": "npm run build", "test": "mocha", "pretest": "npm run lint", diff --git a/src/auth/encryption.js b/src/auth/encryption.js index 99d8d34..95d87b1 100644 --- a/src/auth/encryption.js +++ b/src/auth/encryption.js @@ -1,4 +1,5 @@ const { Ber } = require('asn1') +const { ClientStatus } = require('../connection') const JWT = require('jsonwebtoken') const crypto = require('crypto') const ecPem = require('ec-pem') @@ -82,6 +83,7 @@ function Encrypt (client, server, options) { // It works! First encrypted packet :) client.write('client_to_server_handshake', {}) this.emit('join') + client.status = ClientStatus.Initializing } client.on('server.client_handshake', startClientboundEncryption) diff --git a/src/client.js b/src/client.js index 5b6c960..c9d94b4 100644 --- a/src/client.js +++ b/src/client.js @@ -1,4 +1,4 @@ -const { Connection } = require('./connection') +const { ClientStatus, Connection } = require('./connection') const { createDeserializer, createSerializer } = require('./transforms/serializer') const { RakClient } = require('./Rak') const { serialize } = require('./datatypes/util') @@ -32,10 +32,12 @@ class Client extends Connection { auth.authenticateDeviceCode(this, options) } + this.startGameData = {} + this.on('session', this.connect) this.startQueue() - this.inLog = (...args) => console.info('C ->', ...args) - this.outLog = (...args) => console.info('C <-', ...args) + this.inLog = (...args) => debug('C ->', ...args) + this.outLog = (...args) => debug('C <-', ...args) } validateOptions () { @@ -62,11 +64,13 @@ class Client extends Connection { this.connection = new RakClient({ useWorkers: true, hostname, port }) this.connection.onConnected = () => this.sendLogin() + this.connection.onCloseConnection = () => this._close() this.connection.onEncapsulated = this.onEncapsulated this.connection.connect() } sendLogin () { + this.status = ClientStatus.Authenticating this.createClientChain() const chain = [ @@ -96,8 +100,25 @@ class Client extends Connection { process.exit(1) // TODO: handle } + onPlayStatus(statusPacket) { + if (this.status == ClientStatus.Initializing && this.options.autoInitPlayer === true) { + if (statusPacket.status === 'player_spawn') { + this.status = ClientStatus.Initialized + this.write('set_local_player_as_initialized', { runtime_entity_id: this.startGameData.runtime_entity_id }) + this.emit('spawn') + } + } + } + + _close() { + this.q = [] + this.q2 = [] + } + close () { - console.warn('Close not implemented!!') + this._close() + this.connection.close() + console.log('Closed!') } tryRencode (name, params, actual) { @@ -147,15 +168,12 @@ class Client extends Connection { case 'disconnect': // Client kicked this.onDisconnectRequest(des.data.params) break - // case 'crafting_data': - // fs.writeFileSync('crafting.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v)) - // break - // case 'start_game': - // fs.writeFileSync('start_game.json', JSON.stringify(des.data.params, (k, v) => typeof v == 'bigint' ? v.toString() : v)) - // break - // case 'level_chunk': - // // fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex')) - // break + case 'start_game': + this.startGameData = pakData.params + break + case 'play_status': + this.onPlayStatus(pakData.params) + break default: // console.log('Sending to listeners') } diff --git a/src/connection.js b/src/connection.js index ba03549..7b39dad 100644 --- a/src/connection.js +++ b/src/connection.js @@ -7,7 +7,16 @@ const debug = require('debug')('minecraft-protocol') const SKIP_BATCH = ['level_chunk', 'client_cache_blob_status', 'client_cache_miss_response'] +const ClientStatus = { + Disconnected: 0, + Authenticating: 1, // Handshaking + Initializing: 2, // Authed, need to spawn + Initialized: 3 // play_status spawn sent by server, client responded with SetPlayerInit packet +} + class Connection extends EventEmitter { + state = ClientStatus.Disconnected + versionLessThan (version) { if (typeof version === 'string') { return Versions[version] < this.options.version @@ -112,6 +121,7 @@ class Connection extends EventEmitter { // TODO: Rename this to sendEncapsulated sendMCPE (buffer, immediate) { + if (this.connection.connected === false) return this.connection.sendReliable(buffer, immediate) } @@ -151,4 +161,4 @@ class Connection extends EventEmitter { } } -module.exports = { Connection } +module.exports = { ClientStatus, Connection } diff --git a/src/options.js b/src/options.js index 9d204e1..ec1127d 100644 --- a/src/options.js +++ b/src/options.js @@ -5,11 +5,16 @@ const CURRENT_VERSION = '1.16.201' const defaultOptions = { // https://minecraft.gamepedia.com/Protocol_version#Bedrock_Edition_2 - version: CURRENT_VERSION + version: CURRENT_VERSION, + // client: If we should send SetPlayerInitialized to the server after getting play_status spawn. + // if this is disabled, no 'spawn' event will be emitted, you should manually set + // client.status to ClientStatus.Initialized after sending the init packet. + autoInitPlayer: true } const Versions = { - '1.16.210': 428, + // TODO + // '1.16.210': 428, '1.16.201': 422 } diff --git a/src/rak.js b/src/rak.js index c08fe83..0f44fac 100644 --- a/src/rak.js +++ b/src/rak.js @@ -14,6 +14,7 @@ try { class RakNativeClient extends EventEmitter { constructor (options) { super() + this.connected = false this.onConnected = () => { } this.onCloseConnection = () => { } this.onEncapsulated = () => { } @@ -23,8 +24,14 @@ class RakNativeClient extends EventEmitter { this.onEncapsulated(buffer, address) }) this.raknet.on('connected', () => { + this.connected = true this.onConnected() }) + + this.raknet.on('disconnected', ({ reason }) => { + this.connected = false + this.onCloseConnection(reason) + }) } async ping () { @@ -42,7 +49,15 @@ class RakNativeClient extends EventEmitter { this.raknet.connect() } + close() { + this.connected = false + setTimeout(() => { + this.raknet.close() + }, 40) + } + sendReliable (buffer, immediate) { + if (!this.connected) return const priority = immediate ? PacketPriority.IMMEDIATE_PRIORITY : PacketPriority.MEDIUM_PRIORITY return this.raknet.send(buffer, priority, PacketReliability.RELIABLE_ORDERED, 0) } @@ -83,6 +98,10 @@ class RakNativeServer extends EventEmitter { listen () { this.raknet.listen() } + + close() { + this.raknet.close() + } } class RakJsClient extends EventEmitter { diff --git a/src/relay.js b/src/relay.js index dbb15c5..ed78a09 100644 --- a/src/relay.js +++ b/src/relay.js @@ -106,6 +106,7 @@ class RelayPlayer extends Player { } this.flushUpQueue() // Send queued packets this.downInLog('Recv packet', packet) + // TODO: If we fail to parse a packet, proxy it raw and log an error const des = this.server.deserializer.parsePacketBuffer(packet) if (debugging) { // some packet encode/decode testing stuff @@ -150,7 +151,8 @@ class Relay extends Server { const client = new Client({ hostname: this.options.destination.hostname, port: this.options.destination.port, - encrypt: this.options.encrypt + encrypt: this.options.encrypt, + autoInitPlayer: false }) client.outLog = ds.upOutLog client.inLog = ds.upInLog diff --git a/src/serverPlayer.js b/src/serverPlayer.js index a699ec0..7455b35 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -1,4 +1,4 @@ -const { Connection } = require('./connection') +const { ClientStatus, Connection } = require('./connection') const fs = require('fs') const Options = require('./options') @@ -6,12 +6,6 @@ const { Encrypt } = require('./auth/encryption') const Login = require('./auth/login') const LoginVerify = require('./auth/loginVerify') -const ClientStatus = { - Authenticating: 0, - Initializing: 1, - Initialized: 2 -} - class Player extends Connection { constructor (server, connection) { super() diff --git a/tools/compileProtocol.js b/tools/compileProtocol.js index 5c67af1..d787cb9 100644 --- a/tools/compileProtocol.js +++ b/tools/compileProtocol.js @@ -7,6 +7,11 @@ */ const fs = require('fs') const { ProtoDefCompiler } = require('protodef').Compiler +const { join } = require('path') + +function getJSON (path) { + return JSON.parse(fs.readFileSync(path, 'utf-8')) +} // Parse the YML files and turn to JSON function genProtoSchema () { @@ -37,15 +42,15 @@ function genProtoSchema () { const t = `#Auto-generated from proto.yml, do not modify\n!import: types.yaml\nmcpe_packet:\n name: varint =>\n${l1}\n params: name ?\n${l2}` fs.writeFileSync('./packet_map.yml', t) - compile('./proto.yml', 'protocol.json') + compile('./proto.yml', 'proto.json') return version } // Compile the ProtoDef JSON into JS function createProtocol (version) { const compiler = new ProtoDefCompiler() - const protocol = require(`../${version}/protocol.json`).types - compiler.addTypes(require('../../src/datatypes/compiler-minecraft')) + const protocol = getJSON(`../${version}/protocol.json`).types + compiler.addTypes(require('../src/datatypes/compiler-minecraft')) compiler.addTypes(require('prismarine-nbt/compiler-zigzag')) compiler.addTypesToCompile(protocol) @@ -57,11 +62,12 @@ function createProtocol (version) { return compiledProto } -function main () { +function main (ver = 'latest') { + process.chdir(join(__dirname, '/../data/', ver)) const version = genProtoSchema() - fs.writeFileSync(`../${version}/protocol.json`, JSON.stringify({ types: require('./protocol.json') }, null, 2)) - fs.unlinkSync('./protocol.json') // remove temp file + fs.writeFileSync(`../${version}/protocol.json`, JSON.stringify({ types: getJSON('./proto.json') }, null, 2)) + fs.unlinkSync('./proto.json') // remove temp file fs.unlinkSync('./packet_map.yml') // remove temp file console.log('Generating JS...')