diff --git a/data/new/packet_map.yml b/data/new/packet_map.yml index 45d1f33..3734525 100644 --- a/data/new/packet_map.yml +++ b/data/new/packet_map.yml @@ -150,6 +150,11 @@ mcpe_packet: 0x9a: position_tracking_db_request 0x99: position_tracking_db_broadcast 0x9c: packet_violation_warning + 0x9d: motion_prediction_hints + 0x9e: animate_entity + 0x9f: camera_shake + 0xa0: player_fog + 0xa1: correct_player_move_prediction 0xa2: item_component 0xa3: filter_text_packet @@ -303,5 +308,10 @@ mcpe_packet: if position_tracking_db_request: packet_position_tracking_db_request if position_tracking_db_broadcast: packet_position_tracking_db_broadcast if packet_violation_warning: packet_packet_violation_warning + if motion_prediction_hints: packet_motion_prediction_hints + if animate_entity: packet_animate_entity + if camera_shake: packet_camera_shake + if player_fog: packet_player_fog + if correct_player_move_prediction: packet_correct_player_move_prediction if item_component: packet_item_component if filter_text_packet: packet_filter_text_packet diff --git a/data/new/proto.yml b/data/new/proto.yml index c2c5c1f..0965d3d 100644 --- a/data/new/proto.yml +++ b/data/new/proto.yml @@ -2204,6 +2204,87 @@ packet_packet_violation_warning: # ViolationContext holds a description on the violation of the packet. reason: string + +# MotionPredictionHints is sent by the server to the client. There is a predictive movement component for +# entities. This packet fills the "history" of that component and entity movement is computed based on the +# points. Vanilla sends this packet instead of the SetActorMotion packet when 'spatial optimisations' are +# enabled. +packet_motion_prediction_hints: + !id: 0x9d + !bound: client + # EntityRuntimeID is the runtime ID of the entity whose velocity is sent to the client. + entity_runtime_id: varint64 + # Velocity is the server-calculated velocity of the entity at the point of sending the packet. + velocity: vec3f + # OnGround specifies if the server currently thinks the entity is on the ground. + on_ground: bool + + +# AnimateEntity is sent by the server to animate an entity client-side. It may be used to play a single +# animation, or to activate a controller which can start a sequence of animations based on different +# conditions specified in an animation controller. +# Much of the documentation of this packet can be found at +# https://minecraft.gamepedia.com/Bedrock_Edition_beta_animation_documentation. +packet_animate_entity: + !id: 0x9e + !bound: client + # Animation is the name of a single animation to start playing. + animation: string + # NextState is the first state to start with. These states are declared in animation controllers (which, + # in themselves, are animations too). These states in turn may have animations and transitions to move to + # a next state. + next_state: string + # StopCondition is a MoLang expression that specifies when the animation should be stopped. + stop_condition: string + # Controller is the animation controller that is used to manage animations. These controllers decide when + # to play which animation. + controller: string + # BlendOutTime does not currently seem to be used. + blend_out_time: lf32 + # EntityRuntimeIDs is list of runtime IDs of entities that the animation should be applied to. + runtime_entity_ids: varint64[]varint + +# CameraShake is sent by the server to make the camera shake client-side. This feature was added for map- +# making partners. +packet_camera_shake: + !id: 0x9f + !bound: client + # Intensity is the intensity of the shaking. The client limits this value to 4, so anything higher may + # not work. + intensity: lf32 + # Duration is the number of seconds the camera will shake for. + duration: lf32 + # Type is the type of shake, and is one of the constants listed above. The different type affects how + # the shake looks in game. + type: u8 + +# PlayerFog is sent by the server to render the different fogs in the Stack. The types of fog are controlled +# by resource packs to change how they are rendered, and the ability to create custom fog. +packet_player_fog: + !id: 0xa0 + !bound: client + # Stack is a list of fog identifiers to be sent to the client. Examples of fog identifiers are + # "minecraft:fog_ocean" and "minecraft:fog_hell". + stack: string[]varint + + +# CorrectPlayerMovePrediction is sent by the server if and only if StartGame.ServerAuthoritativeMovementMode +# is set to AuthoritativeMovementModeServerWithRewind. The packet is used to correct movement at a specific +# point in time. +packet_correct_player_move_prediction: + !id: 0xa1 + !bound: client + # Position is the position that the player is supposed to be at at the tick written in the field below. + # The client will change its current position based on movement after that tick starting from the + # Position. + position: vec3f + # Delta is the change in position compared to what the client sent as its position at that specific tick. + delta: vec3f + # OnGround specifies if the player was on the ground at the time of the tick below. + on_ground: bool + # Tick is the tick of the movement which was corrected by this packet. + tick: varint64 + # ItemComponent is sent by the server to attach client-side components to a custom item. packet_item_component: !id: 0xa2 @@ -2221,3 +2302,4 @@ packet_filter_text_packet: text: string # FromServer indicates if the packet was sent by the server or not. from_server: bool + diff --git a/data/newproto.json b/data/newproto.json index 38f4bfd..bbce33d 100644 --- a/data/newproto.json +++ b/data/newproto.json @@ -2694,6 +2694,11 @@ "153": "position_tracking_db_broadcast", "154": "position_tracking_db_request", "156": "packet_violation_warning", + "157": "motion_prediction_hints", + "158": "animate_entity", + "159": "camera_shake", + "160": "player_fog", + "161": "correct_player_move_prediction", "162": "item_component", "163": "filter_text_packet" } @@ -2856,6 +2861,11 @@ "position_tracking_db_request": "packet_position_tracking_db_request", "position_tracking_db_broadcast": "packet_position_tracking_db_broadcast", "packet_violation_warning": "packet_packet_violation_warning", + "motion_prediction_hints": "packet_motion_prediction_hints", + "animate_entity": "packet_animate_entity", + "camera_shake": "packet_camera_shake", + "player_fog": "packet_player_fog", + "correct_player_move_prediction": "packet_correct_player_move_prediction", "item_component": "packet_item_component", "filter_text_packet": "packet_filter_text_packet" }, @@ -6743,6 +6753,111 @@ } ] ], + "packet_motion_prediction_hints": [ + "container", + [ + { + "name": "entity_runtime_id", + "type": "varint64" + }, + { + "name": "velocity", + "type": "vec3f" + }, + { + "name": "on_ground", + "type": "bool" + } + ] + ], + "packet_animate_entity": [ + "container", + [ + { + "name": "animation", + "type": "string" + }, + { + "name": "next_state", + "type": "string" + }, + { + "name": "stop_condition", + "type": "string" + }, + { + "name": "controller", + "type": "string" + }, + { + "name": "blend_out_time", + "type": "lf32" + }, + { + "name": "runtime_entity_ids", + "type": [ + "array", + { + "countType": "varint", + "type": "varint64" + } + ] + } + ] + ], + "packet_camera_shake": [ + "container", + [ + { + "name": "intensity", + "type": "lf32" + }, + { + "name": "duration", + "type": "lf32" + }, + { + "name": "type", + "type": "u8" + } + ] + ], + "packet_player_fog": [ + "container", + [ + { + "name": "stack", + "type": [ + "array", + { + "countType": "varint", + "type": "string" + } + ] + } + ] + ], + "packet_correct_player_move_prediction": [ + "container", + [ + { + "name": "position", + "type": "vec3f" + }, + { + "name": "delta", + "type": "vec3f" + }, + { + "name": "on_ground", + "type": "bool" + }, + { + "name": "tick", + "type": "varint64" + } + ] + ], "packet_item_component": [ "container", [ diff --git a/src/client.js b/src/client.js index 24d0b96..9419202 100644 --- a/src/client.js +++ b/src/client.js @@ -112,7 +112,7 @@ class Client extends Connection { // console.log('packet', 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/*, serialize(pakData.params).slice(0, 100)*/) // No idea what this exotic 0xA0 packet is, it's not implemented anywhere // and seems empty. Possible gibberish from the raknet impl @@ -149,7 +149,7 @@ class Client extends Connection { 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')) + // fs.writeFileSync(`./chunks/chunk-${chunks++}.txt`, packet.toString('hex')) break default: // console.log('Sending to listeners') diff --git a/src/connection.js b/src/connection.js index 8cd8dad..e621446 100644 --- a/src/connection.js +++ b/src/connection.js @@ -19,7 +19,7 @@ class Connection extends EventEmitter { // console.log('Need to encode', name, params) var s = this.connect ? 'C' : 'S' if (this.downQ) s += 'P' - this.outLog('<- ' + s, name) + this.outLog('NB <- ' + s, name,params) const batch = new BatchPacket() const packet = this.serializer.createPacketBuffer({ name, params }) // console.log('Sending buf', packet.toString('hex').) @@ -33,8 +33,13 @@ class Connection extends EventEmitter { } queue(name, params) { - this.outLog('Q <- ', name) + this.outLog('Q <- ', name, params) const packet = this.serializer.createPacketBuffer({ name, params }) + if (name == 'level_chunk') { + // Skip queue + this.sendMCPE(packet) + return + } this.q.push(packet) this.q2.push(name) } @@ -48,16 +53,19 @@ class Connection extends EventEmitter { this.outLog('<- BATCH', this.q2) // 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 < 10 &&*/ i < this.q.length; i++) { + const sending = [] + for (let i = 0; i < 3 && i < this.q.length; i++) { const packet = this.q.shift() + sending.push(this.q2.shift()) batch.addEncodedPacket(packet) } + // console.warn('~~ Sending', sending) if (this.encryptionEnabled) { this.sendEncryptedBatch(batch) } else { this.sendDecryptedBatch(batch) } - this.q2 = [] + // this.q2 = [] } }, 100) } @@ -162,5 +170,7 @@ class Connection extends EventEmitter { // console.log('[client] handled incoming ', buffer) } } - +function serialize(obj = {}, fmt) { + return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt) +} module.exports = { Connection } \ No newline at end of file diff --git a/src/rak.js b/src/rak.js index c9a60a6..5a16182 100644 --- a/src/rak.js +++ b/src/rak.js @@ -57,7 +57,8 @@ class RakNativeServer extends EventEmitter { this.onEncapsulated = () => {} this.raknet = new Server(options.hostname, options.port, { maxConnections: options.maxConnections || 3, - minecraft: { message: new McPingMessage().toString() } + minecraft: { }, + message: new McPingMessage().toBuffer() }) this.raknet.on('openConnection', (client) => { diff --git a/src/relay.js b/src/relay.js new file mode 100644 index 0000000..6fdc890 --- /dev/null +++ b/src/relay.js @@ -0,0 +1,202 @@ +process.env.DEBUG = 'minecraft-protocol raknet' +const { Client } = require("./client") +const { Server } = require("./server") +const { Player } = require("./serverPlayer") +const debug = require('debug')('minecraft-protocol relay') + +/** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */ + +class RelayPlayer extends Player { + constructor(server, conn) { + super(server, conn) + this.server = server + this.conn = conn + + this.startRelaying = false + this.once('join', () => { + this.write('client_cache_status', {enabled:false}) // disable this asap on join + + this.flushDownQueue() + this.startRelaying = true + }) + this.downQ = [] + this.upQ = [] + this.upInLog = (...msg) => console.info('** Backend -> Proxy', ...msg) + this.upOutLog = (...msg) => console.info('** Proxy -> Backend', ...msg) + this.downInLog = (...msg) => console.info('** Client -> Proxy', ...msg) + this.downOutLog = (...msg) => console.info('** Proxy -> Client', ...msg) + + this.outLog = this.downOutLog + this.inLog = this.downInLog + } + + // Called when we get a packet from backend server (Backend -> PROXY -> Client) + readUpstream(packet) { + if (!this.startRelaying) { + console.warn('The downstream client is not ready yet !!') + this.downQ.push(packet) + return + } + this.upInLog('Recv packet', packet) + const des = this.server.deserializer.parsePacketBuffer(packet) + const name = des.data.name + const params = des.data.params + this.upInLog('~~ Bounce B->C', name, serialize(params).slice(0, 100)) + this.upInLog('~~ ', des.buffer) + if (name == 'play_status' && params.status == 'login_success') return + + if (name == 'level_chunk') { //send chunk directly + this.upInLog('Would send chunk', params) + this.sendBuffer(packet) + return + } else this.upInLog('?',name) + + // if (name == 'network_chunk_publisher_update') return + // if (name == 'crafting_data' || name == 'level_chunk') return // Alex breaks + this.queue(name, params) + // this.sendBuffer(packet) + } + + flushDownQueue() { + for (const packet of this.downQ) { + const des = this.server.deserializer.parsePacketBuffer(packet) + this.write(des.data.name, des.data.params) + } + this.downQ = [] + } + + flushUpQueue() { + for (var e of this.upQ) { // Send the queue + const des = this.server.deserializer.parsePacketBuffer(e) + if (des.data.name == 'client_cache_status') { // already disabled on join + // this.upstream.write('client_cache_status', {enabled:false}) + } else { + this.upstream.write(des.data.name, des.data.params) + } + } + this.upQ = [] + } + + // Called when the server gets a packet from the downstream player (Client -> PROXY -> Backend) + readPacket(packet) { + if (this.startRelaying) { // The DS client conn is established & we got a packet to send to US server + if (!this.upstream) { // Upstream is still connecting/handshaking + debug('Got downstream connected packet but upstream is not connected yet, added to q', this.queue.length) + this.upQ.push(packet) // Put into a queue + return + } + this.flushUpQueue() // Send queued packets + this.downInLog('Recv packet', packet) + const des = this.server.deserializer.parsePacketBuffer(packet) + switch (des.data.name) { + case 'client_cache_status': + this.upstream.queue('client_cache_status', {enabled:false}) + break + // case 'request_chunk_radius': + // this.upstream.queue('request_chunk_radius', {chunk_radius: 1}) + // break + default: + // Emit the packet as-is back to the upstream server + this.upstream.queue(des.data.name, des.data.params) + } + } else { + super.readPacket(packet) + } + } +} + +class Relay extends Server { + /** + * Creates a new non-transparent proxy connection to a destination server + * @param {Options} options + */ + constructor(options) { + super(options) + this.RelayPlayer = options.relayPlayer || RelayPlayer + this.forceSingle = true + this.upstreams = new Map() + } + + openUpstreamConnection(ds, clientAddr) { + const client = new Client({ + hostname: this.options.destination.hostname, + port: this.options.destination.port, + encrypt: this.options.encrypt + }) + client.outLog = ds.upOutLog + client.inLog = ds.upInLog + // console.log('Set upstream logs', client.outLog, client.inLog) + client.once('join', () => { // Intercept once handshaking done + ds.upstream = client + ds.flushUpQueue() + console.log('UPSTREAM HAS JOINED') + client.readPacket = (packet) => ds.readUpstream(packet) + }) + this.upstreams.set(clientAddr.hash, client) + } + + closeUpstreamConnection(clientAddr) { + const up = this.upstreams.get(clientAddr.hash) + if (!up) throw Error(`unable to close non-existant connection ${clientAddr.hash}`) + up.close() + this.upstreams.delete(clientAddr.hash) + debug('relay closed connection', clientAddr) + } + + onOpenConnection = (conn) => { + debug('new connection', conn) + if (this.forceSingle && this.clientCount > 0) { + debug('dropping connection as single client relay', conn) + conn.close() + } else { + const player = new this.RelayPlayer(this, conn) + console.log('NEW CONNECTION', conn.address) + this.clients[conn.address] = player + this.emit('connect', { client: player }) + this.openUpstreamConnection(player, conn.address) + } + } +} +console.log = () => {} + + +function serialize(obj = {}, fmt) { + return JSON.stringify(obj, (k, v) => typeof v == 'bigint' ? v.toString() : v, fmt) +} + +function createRelay() { + console.log('Creating relay') + /** + * Example to create a non-transparent proxy (or 'Relay') connection to destination server + * In Relay we de-code and re-encode packets + */ + const relay = new Relay({ + /* Hostname and port for clients to listen to */ + hostname: '0.0.0.0', + port: 19130, + /** + * Who does the authentication + * If set to `client`, all connecting clients will be sent a message with a link to authenticate + * If set to `server`, the server will authenticate and only one client will be able to join + * (Default) If set to `none`, no authentication will be done + */ + auth: 'server', + + /** + * Sets if packets will automatically be forwarded. If set to false, you must listen for on('packet') + * events and + */ + auto: true, + + /* Where to send upstream packets to */ + destination: { + hostname: '127.0.0.1', + port: 19132, + encryption: true + } + }) + + relay.create() +} + +createRelay() \ No newline at end of file diff --git a/src/serverPlayer.js b/src/serverPlayer.js index 20c66ee..8d05c6a 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -90,6 +90,7 @@ class Player extends Connection { // After sending Server to Client Handshake, this handles the client's // Client to Server handshake response. This indicates successful encryption onHandshake() { + // this.outLog('Sending login success!', this.status) // https://wiki.vg/Bedrock_Protocol#Play_Status this.write('play_status', { status: 'login_success' }) this.status = ClientStatus.Initializing diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index 36f7c18..dee6469 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -87,7 +87,7 @@ function createEncryptor(client, iv) { return (blob) => { - client.outLog(client.options ? 'C':'S', '🟡 Encrypting', blob) + // client.outLog(client.options ? 'C':'S', '🟡 Encrypting', client.sendCounter, blob) // stream.write(blob) process(blob) }