packet batching + working client/server spawning
This commit is contained in:
parent
f3604fa9b5
commit
4035295cdd
6 changed files with 277 additions and 152 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
112
src/server.js
112
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 }
|
||||
const hash = (inetAddr) => inetAddr.address + '/' + inetAddr.port
|
||||
|
||||
module.exports = { Server }
|
||||
126
src/serverPlayer.js
Normal file
126
src/serverPlayer.js
Normal 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 }
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue