From 4035295cdd2855b1105758de36738e2ea3d9c6ca Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sun, 21 Feb 2021 15:26:34 -0500 Subject: [PATCH] packet batching + working client/server spawning --- src/client.js | 9 +--- src/clientTest.js | 5 ++ src/connection.js | 48 +++++++++++++---- src/server.js | 112 ++++++--------------------------------- src/serverPlayer.js | 126 ++++++++++++++++++++++++++++++++++++++++++++ src/serverTest.js | 125 ++++++++++++++++++++++++++++++------------- 6 files changed, 275 insertions(+), 150 deletions(-) create mode 100644 src/serverPlayer.js diff --git a/src/client.js b/src/client.js index e83d8db..6ceca76 100644 --- a/src/client.js +++ b/src/client.js @@ -28,6 +28,7 @@ class Client extends Connection { } this.on('session', this.connect) + this.startQueue() // this.on('decrypted', this.onDecryptedPacket) } @@ -104,14 +105,6 @@ class Client extends Connection { }) } - // After sending Server to Client Handshake, this handles the client's - // Client to Server handshake response. This indicates successful encryption - onHandshake() { - // https://wiki.vg/Bedrock_Protocol#Play_Status - this.write('play_status', { status: PLAY_STATUS.LoginSuccess }) - this.emit('join') - } - onDisconnectRequest(packet) { // We're talking over UDP, so there is no connection to close, instead // we stop communicating with the server diff --git a/src/clientTest.js b/src/clientTest.js index 43af281..75a621d 100644 --- a/src/clientTest.js +++ b/src/clientTest.js @@ -28,6 +28,11 @@ async function test() { // resourcepackids: [] // }) // }) + + client.queue('client_cache_status', { enabled: false }) + client.queue('request_chunk_radius', { chunk_radius: 1 }) + client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + }) diff --git a/src/connection.js b/src/connection.js index 3fa2475..44f3891 100644 --- a/src/connection.js +++ b/src/connection.js @@ -18,7 +18,7 @@ class Connection extends EventEmitter { // console.log('<-', name) const batch = new BatchPacket() const packet = this.serializer.createPacketBuffer({ name, params }) - console.log('Sending buf', packet.toString('hex')) + // console.log('Sending buf', packet.toString('hex').) batch.addEncodedPacket(packet) if (this.encryptionEnabled) { @@ -28,6 +28,33 @@ class Connection extends EventEmitter { } } + queue(name, params) { + console.log('<- ', name) + const packet = this.serializer.createPacketBuffer({ name, params }) + this.q.push(packet) + } + + startQueue() { + this.q = [] + this.loop = setInterval(() => { + if (this.q.length) { + //TODO: can we just build Batch before the queue loop? + const batch = new BatchPacket() + // For now, we're over conservative so send max 3 packets + // per batch and hold the rest for the next tick + for (let i = 0; i < 3 && i < this.q.length; i++) { + const packet = this.q.shift() + batch.addEncodedPacket(packet) + } + if (this.encryptionEnabled) { + this.sendEncryptedBatch(batch) + } else { + this.sendDecryptedBatch(batch) + } + } + }, 100) + } + writeRaw(name, buffer) { // skip protodef serializaion // temporary hard coded stuff const batch = new BatchPacket() @@ -37,7 +64,6 @@ class Connection extends EventEmitter { stream.writeUnsignedVarInt(0x7a) stream.append(buffer) batch.addEncodedPacket(stream.getBuffer()) - // console.log('----- SENDING BIOME DEFINITIONS') } if (this.encryptionEnabled) { @@ -50,13 +76,17 @@ class Connection extends EventEmitter { /** * Sends a MCPE packet buffer */ - sendBuffer(buffer) { - const batch = new BatchPacket() - batch.addEncodedPacket(buffer) - if (this.encryptionEnabled) { - this.sendEncryptedBatch(batch) + sendBuffer(buffer, immediate = false) { + if (immediate) { + const batch = new BatchPacket() + batch.addEncodedPacket(buffer) + if (this.encryptionEnabled) { + this.sendEncryptedBatch(batch) + } else { + this.sendDecryptedBatch(batch) + } } else { - this.sendDecryptedBatch(batch) + this.q.push(buffer) } } @@ -78,7 +108,7 @@ class Connection extends EventEmitter { console.log('-> buf', buffer) this.worker.postMessage({ type: 'queueEncapsulated', packet: buffer, immediate }) } else { - const sendPacket = new EncapsulatedPacket(); + const sendPacket = new EncapsulatedPacket() sendPacket.reliability = 0 sendPacket.buffer = buffer this.connection.addEncapsulatedToQueue(sendPacket) diff --git a/src/server.js b/src/server.js index 7db207d..f94b573 100644 --- a/src/server.js +++ b/src/server.js @@ -1,90 +1,10 @@ -const Listener = require('@jsprismarine/raknet/listener') +const Listener = require('jsp-raknet/listener') const { EventEmitter } = require('events') const { createDeserializer, createSerializer } = require('./transforms/serializer') -const { Encrypt } = require('./auth/encryption') -const { decodeLoginJWT } = require('./auth/chains') -const { Connection } = require('./connection') +const { Player } = require('./serverPlayer') + const Options = require('./options') - -const log = (...args) => console.log(...args) - -class Player extends Connection { - constructor(server, connection, options) { - super() - this.server = server - this.serializer = server.serializer - this.connection = connection - Encrypt(this, server, options) - } - - getData() { - return this.userData - } - - onLogin(packet) { - let body = packet.data - console.log('Body', body) - - const clientVer = body.protocol_version - if (this.server.options.version) { - if (this.server.options.version < clientVer) { - this.sendDisconnectStatus(failed_client) - return - } - } else if (clientVer < MIN_VERSION) { - this.sendDisconnectStatus(failed_client) - return - } - - // Parse login data - const authChain = JSON.parse(body.params.chain) - const skinChain = body.params.client_data - - try { - var { key, userData, chain } = decodeLoginJWT(authChain.chain, skinChain) - } catch (e) { - console.error(e) - throw new Error('Failed to verify user') - } - console.log('Verified user', 'got pub key', key, userData) - - this.emit('login', { user: userData.extraData }) // emit events for user - this.emit('server.client_handshake', { key }) // internal so we start encryption - - this.userData = userData.extraData - this.version = clientVer - } - - sendDisconnectStatus(play_status) { - this.write('play_status', { status: play_status }) - this.connection.close() - } - - // After sending Server to Client Handshake, this handles the client's - // Client to Server handshake response. This indicates successful encryption - onHandshake() { - // https://wiki.vg/Bedrock_Protocol#Play_Status - this.write('play_status', { status: 'login_success' }) - this.emit('join') - } - - readPacket(packet) { - console.log('packet', packet) - const des = this.server.deserializer.parsePacketBuffer(packet) - console.log('->', des) - switch (des.data.name) { - case 'login': - console.log(des) - this.onLogin(des) - return - case 'client_to_server_handshake': - this.onHandshake() - default: - console.log('ignoring, unhandled') - } - this.emit(des.data.name, des.data.params) - } -} +const debug = require('debug')('minecraft-protocol') class Server extends EventEmitter { constructor(options) { @@ -102,27 +22,23 @@ class Server extends EventEmitter { } } - getAddrHash(inetAddr) { - return inetAddr.address + '/' + inetAddr.port - } - onOpenConnection = (conn) => { - log('new connection', conn) + debug('new connection', conn) const player = new Player(this, conn) - this.clients[this.getAddrHash(conn.address)] = player + this.clients[hash(conn.address)] = player this.emit('connect', { client: player }) } onCloseConnection = (inetAddr, reason) => { - log('close connection', inetAddr, reason) - delete this.clients[this.getAddrHash(inetAddr)] + debug('close connection', inetAddr, reason) + delete this.clients[hash(inetAddr)] } onEncapsulated = (encapsulated, inetAddr) => { - log(inetAddr.address, ': Encapsulated', encapsulated) + debug(inetAddr.address, 'Encapsulated', encapsulated) const buffer = encapsulated.buffer - const client = this.clients[this.getAddrHash(inetAddr)] + const client = this.clients[hash(inetAddr)] if (!client) { throw new Error(`packet from unknown inet addr: ${inetAddr.address}/${inetAddr.port}`) } @@ -132,16 +48,18 @@ class Server extends EventEmitter { async create(serverIp, port) { this.listener = new Listener(this) this.raknet = await this.listener.listen(serverIp, port) - log('Listening on', serverIp, port) + console.debug('Listening on', serverIp, port) this.raknet.on('openConnection', this.onOpenConnection) this.raknet.on('closeConnection', this.onCloseConnection) this.raknet.on('encapsulated', this.onEncapsulated) this.raknet.on('raw', (buffer, inetAddr) => { - console.log('Raw packet', buffer, inetAddr) + debug('Raw packet', buffer, inetAddr) }) } } -module.exports = { Server, Player } \ No newline at end of file +const hash = (inetAddr) => inetAddr.address + '/' + inetAddr.port + +module.exports = { Server } \ No newline at end of file diff --git a/src/serverPlayer.js b/src/serverPlayer.js new file mode 100644 index 0000000..86d10bc --- /dev/null +++ b/src/serverPlayer.js @@ -0,0 +1,126 @@ +const { Encrypt } = require('./auth/encryption') +const { decodeLoginJWT } = require('./auth/chains') +const { Connection } = require('./connection') +const fs = require('fs') +const debug = require('debug')('minecraft-protocol') + +const ClientStatus = { + Authenticating: 0, + Initializing: 1, + Initialized: 2 +} + +class Player extends Connection { + constructor(server, connection, options) { + super() + this.server = server + this.serializer = server.serializer + this.connection = connection + Encrypt(this, server, options) + + this.startQueue() + this.status = ClientStatus.Authenticating + } + + getData() { + return this.userData + } + + onLogin(packet) { + let body = packet.data + debug('Body', body) + this.emit('loggingIn', body) + + const clientVer = body.protocol_version + if (this.server.options.version) { + if (this.server.options.version < clientVer) { + this.sendDisconnectStatus(failed_client) + return + } + } else if (clientVer < MIN_VERSION) { + this.sendDisconnectStatus(failed_client) + return + } + + // Parse login data + const authChain = JSON.parse(body.params.chain) + const skinChain = body.params.client_data + + try { + var { key, userData, chain } = decodeLoginJWT(authChain.chain, skinChain) + } catch (e) { + console.error(e) + throw new Error('Failed to verify user') + } + console.log('Verified user', 'got pub key', key, userData) + + this.emit('login', { user: userData.extraData }) // emit events for user + this.emit('server.client_handshake', { key }) // internal so we start encryption + + this.userData = userData.extraData + this.version = clientVer + } + + + /** + * Disconnects a client before it has joined + * @param {string} play_status + */ + sendDisconnectStatus(play_status) { + this.write('play_status', { status: play_status }) + this.connection.close() + } + + /** + * Disconnects a client after it has joined + */ + disconnect(reason, hide = false) { + this.write('disconnect', { + hide_disconnect_screen: hide, + message: reason + }) + this.connection.close() + } + + // After sending Server to Client Handshake, this handles the client's + // Client to Server handshake response. This indicates successful encryption + onHandshake() { + // https://wiki.vg/Bedrock_Protocol#Play_Status + this.write('play_status', { status: 'login_success' }) + this.status = ClientStatus.Initializing + this.emit('join') + } + + readPacket(packet) { + // console.log('packet', packet) + try { + var des = this.server.deserializer.parsePacketBuffer(packet) + } catch (e) { + this.disconnect('Server error') + console.warn('Packet parsing failed! Writing dump to ./packetdump.bin') + fs.writeFileSync('packetdump.bin', packet) + fs.writeFileSync('packetdump.txt', packet.toString('hex')) + throw e + } + + console.log('->', des) + switch (des.data.name) { + case 'login': + console.log(des) + this.onLogin(des) + return + case 'client_to_server_handshake': + // Emit the 'join' event + this.onHandshake() + case 'set_local_player_as_initialized': + this.state = ClientStatus.Initialized + // Emit the 'spawn' event + this.emit('spawn') + default: + console.log('ignoring, unhandled') + } + this.emit(des.data.name, des.data.params) + } +} + +module.exports = { Player, ClientStatus } \ No newline at end of file diff --git a/src/serverTest.js b/src/serverTest.js index 0d733f6..ad54097 100644 --- a/src/serverTest.js +++ b/src/serverTest.js @@ -1,3 +1,4 @@ +// process.env.DEBUG = 'minecraft-protocol raknet' const { Server } = require('./server') const CreativeItems = require('../data/creativeitems.json') const NBT = require('prismarine-nbt') @@ -24,7 +25,7 @@ server.on('connect', ({ client }) => { 'texture_packs': [] }) - client.once('resource_pack_client_response', (packet) => { + client.once('resource_pack_client_response', async (packet) => { // ResourcePackStack is sent by the server to send the order in which resource packs and behaviour packs // should be applied (and downloaded) by the client. client.write('resource_pack_stack', { @@ -37,45 +38,97 @@ server.on('connect', ({ client }) => { }) client.once('resource_pack_client_response', async (packet) => { - ran = true - let items = [] - let ids = 0 - for (var item of CreativeItems) { - let creativeitem = { runtime_id: items.length } - const has_nbt = !!item.nbt_b64 - if (item.id != 0) { - creativeitem.item = { - network_id: item.id, - auxiliary_value: item.damage || 0, - has_nbt, - nbt: { - version: 1, - }, - blocking_tick: 0, - can_destroy: [], - can_place_on: [] - } - if (has_nbt) { - let nbtBuf = Buffer.from(item.nbt_b64, 'base64') - let { parsed } = await NBT.parse(nbtBuf, 'little') - creativeitem.item.nbt.nbt = parsed - } - } - items.push(creativeitem) - // console.log(creativeitem) - } + // ran = true + // let items = [] + // let ids = 0 + // for (var item of CreativeItems) { + // let creativeitem = { runtime_id: items.length } + // const has_nbt = !!item.nbt_b64 + // if (item.id != 0) { + // creativeitem.item = { + // network_id: item.id, + // auxiliary_value: item.damage || 0, + // has_nbt, + // nbt: { + // version: 1, + // }, + // blocking_tick: 0, + // can_destroy: [], + // can_place_on: [] + // } + // if (has_nbt) { + // let nbtBuf = Buffer.from(item.nbt_b64, 'base64') + // let { parsed } = await NBT.parse(nbtBuf, 'little') + // creativeitem.item.nbt.nbt = parsed + // } + // } + // items.push(creativeitem) + // // console.log(creativeitem) + // } - console.log(items, ids) + // console.log(items, ids) - client.write('creative_content', { items }) + // client.write('creative_content', { items }) // wait a bit just for easier debugging - setTimeout(() => { - const biomeDefs = fs.readFileSync('../data/biome_definitions.nbt') - client.writeRaw('biome_definition_list', biomeDefs) + // setTimeout(() => { + // const biomeDefs = fs.readFileSync('../data/biome_definitions.nbt') + // client.writeRaw('biome_definition_list', biomeDefs) + + // // TODO: send chunks so we can spawn player + // }, 1000) - // TODO: send chunks so we can spawn player - }, 1000) }) + + client.write('network_settings', { + compression_threshold: 1 + }) + + for (let i = 0; i < 3; i++) { + client.queue('inventory_slot', {"inventory_id":120,"slot":i,"uniqueid":0,"item":{"network_id":0}}) + } + + client.queue('inventory_transaction', require('./packets/inventory_transaction.json')) + client.queue('player_list', require('./packets/player_list.json')) + client.queue('start_game', require('./packets/start_game.json')) + client.queue('item_component', {"entries":[]}) + client.queue('set_time', { time: 5433771 }) + client.queue('set_difficulty', { difficulty: 1 }) + client.queue('set_commands_enabled', { enabled: true }) + client.queue('adventure_settings', require('./packets/adventure_settings.json')) + + client.queue('biome_definition_list', require('./packets/biome_definition_list.json')) + client.queue('available_entity_identifiers', require('./packets/available_entity_identifiers.json')) + + client.queue('update_attributes', require('./packets/update_attributes.json')) + client.queue('creative_content', require('./packets/creative_content.json')) + client.queue('player_hotbar', {"selected_slot":3,"window_id":0,"select_slot":true}) + + client.queue('crafting_data', require('./packets/crafting_data.json')) + client.queue('available_commands', require('./packets/available_commands.json')) + + client.queue('game_rules_changed', require('./packets/game_rules_changed.json')) + client.queue('respawn', {"x":646.9405517578125,"y":65.62001037597656,"z":77.86255645751953,"state":0,"runtime_entity_id":0}) + + for (const file of fs.readdirSync('chunks')) { + const buffer = Buffer.from(fs.readFileSync('./chunks/' + file, 'utf8'), 'hex') + // console.log('Sending chunk', chunk) + client.sendBuffer(buffer) + } + + setInterval(() => { + client.write('network_chunk_publisher_update', {"coordinates":{"x":646,"y":130,"z":77},"radius":64}) + }, 9500) + + + setTimeout(() => { + client.write('play_status', { status: 'player_spawn' }) + }, 8000) }) }) -}) \ No newline at end of file +}) + +async function sleep(ms) { + return new Promise(res => { + setTimeout(() => { res() }, ms) + }) +} \ No newline at end of file