packet batching + working client/server spawning

This commit is contained in:
extremeheat 2021-02-21 15:26:34 -05:00
commit 4035295cdd
6 changed files with 277 additions and 152 deletions

View file

@ -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

View file

@ -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 })
})

View file

@ -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)

View file

@ -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 }
const hash = (inetAddr) => inetAddr.address + '/' + inetAddr.port
module.exports = { Server }

126
src/serverPlayer.js Normal file
View file

@ -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 }

View file

@ -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)
})
})
})
})
async function sleep(ms) {
return new Promise(res => {
setTimeout(() => { res() }, ms)
})
}