Send skin data, protocol updates (#88)
* Add skin data * Serialization updates * Dynamic shield item id * NBT reading/writing on void type uses 0 length, fix some third party servers * Fix proxy empty chunk issue * Fix scoreboards compiler needs ../ * fix indentation * Fix set_score packet * Fix readme title auth doc * Implement new compiler vars
This commit is contained in:
parent
76febb29f1
commit
f0fbf4f859
19 changed files with 5557 additions and 5164 deletions
|
|
@ -2,7 +2,6 @@ const { ClientStatus, Connection } = require('./connection')
|
|||
const { createDeserializer, createSerializer } = require('./transforms/serializer')
|
||||
const { RakClient } = require('./rak')
|
||||
const { serialize } = require('./datatypes/util')
|
||||
const fs = require('fs')
|
||||
const debug = require('debug')('minecraft-protocol')
|
||||
const Options = require('./options')
|
||||
const auth = require('./client/auth')
|
||||
|
|
@ -152,33 +151,16 @@ class Client extends Connection {
|
|||
this.status = ClientStatus.Disconnected
|
||||
}
|
||||
|
||||
tryRencode (name, params, actual) {
|
||||
const packet = this.serializer.createPacketBuffer({ name, params })
|
||||
|
||||
console.assert(packet.equals(actual))
|
||||
if (!packet.equals(actual)) {
|
||||
const ours = packet.toString('hex').match(/.{1,16}/g).join('\n')
|
||||
const theirs = actual.toString('hex').match(/.{1,16}/g).join('\n')
|
||||
|
||||
fs.writeFileSync('ours.txt', ours)
|
||||
fs.writeFileSync('theirs.txt', theirs)
|
||||
fs.writeFileSync('ours.json', serialize(params))
|
||||
fs.writeFileSync('theirs.json', serialize(this.deserializer.parsePacketBuffer(packet).data.params))
|
||||
|
||||
throw new Error(name + ' Packet comparison failed!')
|
||||
}
|
||||
}
|
||||
|
||||
readPacket (packet) {
|
||||
const des = this.deserializer.parsePacketBuffer(packet)
|
||||
const pakData = { name: des.data.name, params: des.data.params }
|
||||
this.inLog('-> C', pakData.name/*, serialize(pakData.params).slice(0, 100) */)
|
||||
this.inLog('-> C', pakData.name, this.options.loggging ? serialize(pakData.params) : '')
|
||||
this.emit('packet', des)
|
||||
|
||||
if (debugging) {
|
||||
// Packet verifying (decode + re-encode + match test)
|
||||
if (pakData.name) {
|
||||
this.tryRencode(pakData.name, pakData.params, packet)
|
||||
this.deserializer.verify(packet, this.serializer)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,6 +175,12 @@ class Client extends Connection {
|
|||
break
|
||||
case 'start_game':
|
||||
this.startGameData = pakData.params
|
||||
this.startGameData.itemstates.forEach(state => {
|
||||
if (state.name === 'minecraft:shield') {
|
||||
this.serializer.proto.setVariable('ShieldItemID', state.runtime_id)
|
||||
this.deserializer.proto.setVariable('ShieldItemID', state.runtime_id)
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'play_status':
|
||||
if (this.status === ClientStatus.Authenticating) {
|
||||
|
|
|
|||
|
|
@ -40,8 +40,25 @@ class Connection extends EventEmitter {
|
|||
this.encrypt = cipher.createEncryptor(this, iv)
|
||||
}
|
||||
|
||||
updateItemPalette (palette) {
|
||||
// In the future, we can send down the whole item palette if we need
|
||||
// but since it's only one item, we can just make a single variable.
|
||||
let shieldItemID
|
||||
for (const state of palette) {
|
||||
if (state.name === 'minecraft:shield') {
|
||||
shieldItemID = state.runtime_id
|
||||
break
|
||||
}
|
||||
}
|
||||
if (shieldItemID) {
|
||||
this.serializer.proto.setVariable('ShieldItemID', shieldItemID)
|
||||
this.deserializer.proto.setVariable('ShieldItemID', shieldItemID)
|
||||
}
|
||||
}
|
||||
|
||||
write (name, params) {
|
||||
this.outLog('sending', name, params)
|
||||
if (name === 'start_game') this.updateItemPalette(params.itemstates)
|
||||
const batch = new Framer()
|
||||
const packet = this.serializer.createPacketBuffer({ name, params })
|
||||
batch.addEncodedPacket(packet)
|
||||
|
|
@ -55,6 +72,7 @@ class Connection extends EventEmitter {
|
|||
|
||||
queue (name, params) {
|
||||
this.outLog('Q <- ', name, params)
|
||||
if (name === 'start_game') this.updateItemPalette(params.itemstates)
|
||||
const packet = this.serializer.createPacketBuffer({ name, params })
|
||||
if (name === 'level_chunk') {
|
||||
// Skip queue, send ASAP
|
||||
|
|
@ -113,7 +131,11 @@ class Connection extends EventEmitter {
|
|||
|
||||
sendMCPE (buffer, immediate) {
|
||||
if (this.connection.connected === false || this.status === ClientStatus.Disconnected) return
|
||||
this.connection.sendReliable(buffer, immediate)
|
||||
try {
|
||||
this.connection.sendReliable(buffer, immediate)
|
||||
} catch (e) {
|
||||
debug('while sending to', this.connection, e)
|
||||
}
|
||||
}
|
||||
|
||||
// These are callbacks called from encryption.js
|
||||
|
|
|
|||
|
|
@ -37,10 +37,10 @@ function connect (client) {
|
|||
response_status: 'completed',
|
||||
resourcepackids: []
|
||||
})
|
||||
client.queue('request_chunk_radius', { chunk_radius: client.renderDistance || 10 })
|
||||
})
|
||||
|
||||
client.queue('client_cache_status', { enabled: false })
|
||||
client.queue('request_chunk_radius', { chunk_radius: client.renderDistance || 1 })
|
||||
client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n })
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -38,14 +38,18 @@ function sizeOfNbt (value) {
|
|||
// Little Endian
|
||||
|
||||
function readNbtLE (buffer, offset) {
|
||||
return protoLE.read(buffer, offset, 'nbt')
|
||||
const r = protoLE.read(buffer, offset, 'nbt')
|
||||
if (r.value.type === 'end') return { value: r.value, size: 0 }
|
||||
return r
|
||||
}
|
||||
|
||||
function writeNbtLE (value, buffer, offset) {
|
||||
if (value.type === 'end') return offset
|
||||
return protoLE.write(value, buffer, offset, 'nbt')
|
||||
}
|
||||
|
||||
function sizeOfNbtLE (value) {
|
||||
if (value.type === 'end') return 0
|
||||
return protoLE.sizeOf(value, 'nbt')
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ const { PUBLIC_KEY } = require('./constants')
|
|||
const algorithm = 'ES384'
|
||||
|
||||
module.exports = (client, server, options) => {
|
||||
const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8')
|
||||
const dp = DataProvider(options.protocolVersion)
|
||||
const skinTex = fs.readFileSync(dp.getPath('steveSkin.bin')).toString('base64')
|
||||
const skinGeom = fs.readFileSync(dp.getPath('steveGeometry.json')).toString('base64')
|
||||
const skinData = JSON.parse(fs.readFileSync(dp.getPath('steve.json'), 'utf-8'))
|
||||
|
||||
client.createClientChain = (mojangKey, offline) => {
|
||||
const privateKey = client.ecdhKeyPair.privateKey
|
||||
|
|
@ -22,12 +25,12 @@ module.exports = (client, server, options) => {
|
|||
certificateAuthority: true,
|
||||
identityPublicKey: client.clientX509
|
||||
}
|
||||
token = JWT.sign(payload, privateKey, { algorithm, notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509 } })
|
||||
token = JWT.sign(payload, privateKey, { algorithm, notBefore: 0, issuer: 'self', expiresIn: 60 * 60, header: { x5u: client.clientX509, typ: undefined } })
|
||||
} else {
|
||||
token = JWT.sign({
|
||||
identityPublicKey: mojangKey || PUBLIC_KEY,
|
||||
certificateAuthority: true
|
||||
}, privateKey, { algorithm, header: { x5u: client.clientX509 } })
|
||||
}, privateKey, { algorithm, header: { x5u: client.clientX509, typ: undefined } })
|
||||
}
|
||||
|
||||
client.clientIdentityChain = token
|
||||
|
|
@ -36,49 +39,38 @@ module.exports = (client, server, options) => {
|
|||
|
||||
client.createClientUserChain = (privateKey) => {
|
||||
let payload = {
|
||||
AnimatedImageData: [],
|
||||
ArmSize: 'wide',
|
||||
CapeData: '',
|
||||
CapeId: '',
|
||||
CapeImageHeight: 0,
|
||||
CapeImageWidth: 0,
|
||||
CapeOnClassicSkin: false,
|
||||
...skinData,
|
||||
|
||||
ClientRandomId: Date.now(),
|
||||
CurrentInputMode: 1,
|
||||
DefaultInputMode: 1,
|
||||
DeviceId: nextUUID(),
|
||||
DeviceModel: '',
|
||||
DeviceModel: 'PrismarineJS',
|
||||
DeviceOS: client.session?.deviceOS || 7,
|
||||
GameVersion: options.version || '1.16.201',
|
||||
GuiScale: -1,
|
||||
LanguageCode: 'en_GB', // TODO locale
|
||||
PersonaPieces: [],
|
||||
PersonaSkin: true,
|
||||
PieceTintColors: [],
|
||||
|
||||
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: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0', // 1.16.210
|
||||
PremiumSkin: false,
|
||||
PlayFabId: nextUUID().replace(/-/g, '').slice(0, 16), // 1.16.210
|
||||
|
||||
SelfSignedId: nextUUID(),
|
||||
ServerAddress: `${options.host}:${options.port}`,
|
||||
SkinAnimationData: '',
|
||||
SkinColor: '#ffffcd96',
|
||||
SkinData: 'AAAAAA==',
|
||||
SkinData: skinTex,
|
||||
SkinGeometryData: skinGeom,
|
||||
SkinId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0',
|
||||
SkinImageHeight: 1,
|
||||
SkinImageWidth: 1,
|
||||
SkinResourcePatch: '',
|
||||
|
||||
ThirdPartyName: client.profile.name,
|
||||
ThirdPartyNameOnly: false,
|
||||
UIProfile: 0
|
||||
}
|
||||
const customPayload = options.skinData || {}
|
||||
payload = { ...payload, ...customPayload }
|
||||
payload.ServerAddress = `${options.host}:${options.port}`
|
||||
|
||||
client.clientUserChain = JWT.sign(payload, privateKey, { algorithm, header: { x5u: client.clientX509 } })
|
||||
client.clientUserChain = JWT.sign(payload, privateKey, { algorithm, header: { x5u: client.clientX509, typ: undefined }, noTimestamp: true /* pocketmine.. */ })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
49
src/relay.js
49
src/relay.js
|
|
@ -30,6 +30,7 @@ class RelayPlayer extends Player {
|
|||
|
||||
this.outLog = this.downOutLog
|
||||
this.inLog = this.downInLog
|
||||
this.chunkSendCache = []
|
||||
}
|
||||
|
||||
// Called when we get a packet from backend server (Backend -> PROXY -> Client)
|
||||
|
|
@ -45,16 +46,23 @@ class RelayPlayer extends Player {
|
|||
if (name === 'play_status' && params.status === 'login_success') return // We already sent this, this needs to be sent ASAP or client will disconnect
|
||||
|
||||
if (debugging) { // some packet encode/decode testing stuff
|
||||
const rpacket = this.server.serializer.createPacketBuffer({ name, params })
|
||||
if (!rpacket.equals(packet)) {
|
||||
console.warn('New', rpacket.toString('hex'))
|
||||
console.warn('Old', packet.toString('hex'))
|
||||
console.log('Failed to re-encode', name, params)
|
||||
process.exit(1)
|
||||
}
|
||||
this.server.deserializer.verify(des, this.server.serializer)
|
||||
}
|
||||
|
||||
this.emit('clientbound', des.data)
|
||||
|
||||
// If we're sending a chunk, but player isn't yet initialized, wait until it is.
|
||||
// This is wrong and should not be an issue to send chunks before the client
|
||||
// is in the world; need to investigate further, but for now it's fine.
|
||||
if (name === 'level_chunk' && this.status !== 3) {
|
||||
this.chunkSendCache.push([name, params])
|
||||
return
|
||||
} else if (this.status === 3 && this.chunkSendCache.length) {
|
||||
for (const chunk of this.chunkSendCache) {
|
||||
this.queue(...chunk)
|
||||
}
|
||||
this.chunkSendCache = []
|
||||
}
|
||||
this.queue(name, params)
|
||||
}
|
||||
|
||||
|
|
@ -82,33 +90,36 @@ class RelayPlayer extends Player {
|
|||
|
||||
// Called when the server gets a packet from the downstream player (Client -> PROXY -> Backend)
|
||||
readPacket (packet) {
|
||||
if (this.startRelaying) { // The downstream client conn is established & we got a packet to send to upstream server
|
||||
if (!this.upstream) { // Upstream is still connecting/handshaking
|
||||
// The downstream client conn is established & we got a packet to send to upstream server
|
||||
if (this.startRelaying) {
|
||||
// Upstream is still connecting/handshaking
|
||||
if (!this.upstream) {
|
||||
this.downInLog('Got downstream connected packet but upstream is not connected yet, added to q', this.upQ.length)
|
||||
this.upQ.push(packet) // Put into a queue
|
||||
return
|
||||
}
|
||||
this.flushUpQueue() // Send queued packets
|
||||
|
||||
// Send queued packets
|
||||
this.flushUpQueue()
|
||||
this.downInLog('recv', 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
|
||||
const rpacket = this.server.serializer.createPacketBuffer(des.data)
|
||||
if (!rpacket.equals(packet)) {
|
||||
console.warn('New', rpacket.toString('hex'))
|
||||
console.warn('Old', packet.toString('hex'))
|
||||
console.log('Failed to re-encode', des.data)
|
||||
process.exit(1)
|
||||
}
|
||||
this.server.deserializer.verify(des, this.server.serializer)
|
||||
}
|
||||
|
||||
this.emit('serverbound', des.data)
|
||||
|
||||
switch (des.data.name) {
|
||||
case 'client_cache_status':
|
||||
// Force the chunk cache off.
|
||||
this.upstream.queue('client_cache_status', { enabled: false })
|
||||
break
|
||||
case 'set_local_player_as_initialized':
|
||||
this.status = 3
|
||||
break
|
||||
default:
|
||||
// Emit the packet as-is back to the upstream server
|
||||
this.downInLog('Relaying', des.data)
|
||||
|
|
@ -149,6 +160,10 @@ class Relay extends Server {
|
|||
client.outLog = ds.upOutLog
|
||||
client.inLog = ds.upInLog
|
||||
client.once('join', () => { // Intercept once handshaking done
|
||||
// Tell the server to disable chunk cache for this connection as a client.
|
||||
// Wait a bit for the server to ack and process, the continue with proxying
|
||||
// otherwise the player can get stuck in an empty world.
|
||||
client.write('client_cache_status', { enabled: false })
|
||||
ds.upstream = client
|
||||
ds.flushUpQueue()
|
||||
this.conLog('Connected to upstream server')
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@ class Parser extends FullPacketParser {
|
|||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
verify (deserialized, serializer) {
|
||||
const { name, params } = deserialized.data
|
||||
const oldBuffer = deserialized.fullBuffer
|
||||
const newBuffer = serializer.createPacketBuffer({ name, params })
|
||||
if (!newBuffer.equals(oldBuffer)) {
|
||||
console.warn('New', newBuffer.toString('hex'))
|
||||
console.warn('Old', oldBuffer.toString('hex'))
|
||||
console.log('Failed to re-encode', name, params)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compiles the ProtoDef schema at runtime
|
||||
|
|
@ -31,11 +43,8 @@ function getProtocol (version) {
|
|||
compiler.addTypes(require(join(__dirname, '../datatypes/compiler-minecraft')))
|
||||
compiler.addTypes(require('prismarine-nbt/compiler-zigzag'))
|
||||
|
||||
const compile = (compiler, file) => {
|
||||
global.native = compiler.native // eslint-disable-line
|
||||
const { PartialReadError } = require('protodef/src/utils') // eslint-disable-line
|
||||
return require(file)() // eslint-disable-line
|
||||
}
|
||||
global.PartialReadError = require('protodef/src/utils').PartialReadError
|
||||
const compile = (compiler, file) => require(file)(compiler.native)
|
||||
|
||||
return new CompiledProtodef(
|
||||
compile(compiler.sizeOfCompiler, join(__dirname, `../../data/${version}/size.js`)),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue