From cf6471f6eb0db16f097aa1751faba1916f1e9421 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Tue, 23 Mar 2021 03:35:38 -0400 Subject: [PATCH 01/21] Add packet dumper, configuable vanilla server, client events --- src/client.js | 18 ++------ tools/genPacketDumps.js | 83 +++++++++++++++++++++++++++++++++++++ tools/startVanillaServer.js | 30 +++++++++----- 3 files changed, 105 insertions(+), 26 deletions(-) create mode 100644 tools/genPacketDumps.js diff --git a/src/client.js b/src/client.js index 47c5a60..b270c2c 100644 --- a/src/client.js +++ b/src/client.js @@ -108,10 +108,8 @@ class Client extends Connection { } onDisconnectRequest (packet) { - // We're talking over UDP, so there is no connection to close, instead - // we stop communicating with the server console.warn(`Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`) - process.exit(1) // TODO: handle + this.emit('kick', packet) } onPlayStatus (statusPacket) { @@ -125,6 +123,7 @@ class Client extends Connection { } close () { + this.emit('close') clearInterval(this.loop) clearTimeout(this.connectTimeout) this.q = [] @@ -155,22 +154,13 @@ class Client extends Connection { 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.emit('packet', pakData) if (debugging) { // Packet verifying (decode + re-encode + match test) if (pakData.name) { this.tryRencode(pakData.name, pakData.params, packet) } - - // console.info('->', JSON.stringify(pakData, (k,v) => typeof v == 'bigint' ? v.toString() : v)) - // Packet dumping - try { - const root = __dirname + `../data/${this.options.version}/sample/` - if (!fs.existsSync(root + `packets/${pakData.name}.json`)) { - fs.writeFileSync(root + `packets/${pakData.name}.json`, serialize(pakData.params, 2)) - fs.writeFileSync(root + `packets/${pakData.name}.txt`, packet.toString('hex')) - } - } catch { } } // Abstract some boilerplate before sending to listeners @@ -187,8 +177,6 @@ class Client extends Connection { case 'play_status': this.onPlayStatus(pakData.params) break - default: - // console.log('Sending to listeners') } // Emit packet diff --git a/tools/genPacketDumps.js b/tools/genPacketDumps.js new file mode 100644 index 0000000..dd74150 --- /dev/null +++ b/tools/genPacketDumps.js @@ -0,0 +1,83 @@ +/* eslint-disable */ +// Collect sample packets needed for `serverTest.js` +// process.env.DEBUG = 'minecraft-protocol' +const fs = require('fs') +const vanillaServer = require('../tools/startVanillaServer') +const { Client } = require('../src/client') +const { serialize, waitFor } = require('../src/datatypes/util') +const { CURRENT_VERSION } = require('../src/options') +const { join } = require('path') + +let loop + +async function main() { + const random = ((Math.random() * 100) | 0) + const port = 19130 + random + + const handle = await vanillaServer.startServerAndWait(CURRENT_VERSION, 1000 * 120, { 'server-port': port, path: 'bds_' }) + + console.log('Started server') + const client = new Client({ + hostname: '127.0.0.1', + port, + username: 'Boat' + random, + offline: true + }) + + return waitFor(async res => { + const root = join(__dirname, `../data/${client.options.version}/sample/packets/`) + if (!fs.existsSync(root)) { + fs.mkdirSync(root, { recursive: true }) + } + + client.once('resource_packs_info', (packet) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + resourcepackids: [] + }) + + client.once('resource_pack_stack', (stack) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + 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 }) + + clearInterval(loop) + loop = setInterval(() => { + client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) }) + }, 200) + }) + + client.on('packet', pakData => { // Packet dumping + if (pakData.name == 'level_chunk') return + try { + if (!fs.existsSync(root + `${pakData.name}.json`)) { + fs.promises.writeFile(root + `${pakData.name}.json`, serialize(pakData.params, 2)) + } + } catch (e) { console.log(e) } + }) + + console.log('Awaiting join...') + + client.on('spawn', () => { + console.log('Spawned!') + clearInterval(loop) + client.close() + handle.kill() + res() + }) + }, 1000 * 60, () => { + clearInterval(loop) + throw Error('timed out') + }) +} + +main().then(() => { + console.log('Successfully dumped packets') +}) \ No newline at end of file diff --git a/tools/startVanillaServer.js b/tools/startVanillaServer.js index 83856f4..07587ae 100644 --- a/tools/startVanillaServer.js +++ b/tools/startVanillaServer.js @@ -17,18 +17,18 @@ function fetchLatestStable () { } // Download + extract vanilla server and enter the directory -async function download (os, version) { +async function download (os, version, path = 'bds-') { process.chdir(__dirname) const verStr = version.split('.').slice(0, 3).join('.') - const dir = 'bds-' + version + const dir = path + version if (fs.existsSync(dir) && getFiles(dir).length) { - process.chdir('bds-' + version) // Enter server folder + process.chdir(path + version) // Enter server folder return verStr } try { fs.mkdirSync(dir) } catch { } - process.chdir('bds-' + version) // Enter server folder + process.chdir(path + version) // Enter server folder const url = (os, version) => `https://minecraft.azureedge.net/bin-${os}/bedrock-server-${version}.zip` let found = false @@ -53,10 +53,18 @@ async function download (os, version) { return verStr } +const defaultOptions = { + 'level-generator': '2', + 'server-port': '19130', + 'online-mode': 'false' +} + // Setup the server -function configure () { +function configure (options = {}) { + const opts = { ...defaultOptions, ...options } let config = fs.readFileSync('./server.properties', 'utf-8') - config += '\nlevel-generator=2\nserver-port=19130\nplayer-idle-timeout=1\nallow-cheats=true\ndefault-player-permission-level=operator\nonline-mode=false' + config += '\nplayer-idle-timeout=1\nallow-cheats=true\ndefault-player-permission-level=operator' + for (const o in opts) config += `\n${o}=${opts[o]}` fs.writeFileSync('./server.properties', config) } @@ -66,13 +74,13 @@ function run (inheritStdout = true) { } // Run the server -async function startServer (version, onStart) { +async function startServer (version, onStart, options = {}) { const os = process.platform === 'win32' ? 'win' : process.platform if (os !== 'win' && os !== 'linux') { throw Error('unsupported os ' + os) } - await download(os, version) - configure() + await download(os, version, options.path) + configure(options) const handle = run(!onStart) if (onStart) { handle.stdout.on('data', data => data.includes('Server started.') ? onStart() : null) @@ -83,10 +91,10 @@ async function startServer (version, onStart) { } // Start the server and wait for it to be ready, with a timeout -async function startServerAndWait (version, withTimeout) { +async function startServerAndWait (version, withTimeout, options) { let handle await waitFor(async res => { - handle = await startServer(version, res) + handle = await startServer(version, res, options) }, withTimeout, () => { handle?.kill() throw new Error('Server did not start on time ' + withTimeout) From e22dfea5999916d65c86216f02eaf8e4c330e4f7 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 24 Mar 2021 06:10:56 -0400 Subject: [PATCH 02/21] Fix server/client closing --- src/client.js | 11 ++++++++--- src/connection.js | 4 +++- src/server.js | 6 ++++-- src/serverPlayer.js | 8 +++++--- 4 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/client.js b/src/client.js index b270c2c..bd841b3 100644 --- a/src/client.js +++ b/src/client.js @@ -108,7 +108,7 @@ class Client extends Connection { } onDisconnectRequest (packet) { - console.warn(`Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`) + console.warn(`C Server requested ${packet.hide_disconnect_reason ? 'silent disconnect' : 'disconnect'}: ${packet.message}`) this.emit('kick', packet) } @@ -130,7 +130,7 @@ class Client extends Connection { this.q2 = [] this.connection?.close() this.removeAllListeners() - console.log('Closed!') + console.log('Client closed!') } tryRencode (name, params, actual) { @@ -154,7 +154,7 @@ class Client extends Connection { 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.emit('packet', pakData) + this.emit('packet', des) if (debugging) { // Packet verifying (decode + re-encode + match test) @@ -177,6 +177,11 @@ class Client extends Connection { case 'play_status': this.onPlayStatus(pakData.params) break + default: + if (this.status !== ClientStatus.Initializing && this.status !== ClientStatus.Initialized) { + this.inLog(`Can't accept ${des.data.name}, client not yet authenticated : ${this.status}`) + return + } } // Emit packet diff --git a/src/connection.js b/src/connection.js index 220013c..a9061c0 100644 --- a/src/connection.js +++ b/src/connection.js @@ -16,6 +16,8 @@ const ClientStatus = { class Connection extends EventEmitter { status = ClientStatus.Disconnected + q = [] + q2 = [] versionLessThan (version) { if (typeof version === 'string') { @@ -121,7 +123,7 @@ class Connection extends EventEmitter { // TODO: Rename this to sendEncapsulated sendMCPE (buffer, immediate) { - if (this.connection.connected === false) return + if (this.connection.connected === false || this.status === ClientStatus.Disconnected) return this.connection.sendReliable(buffer, immediate) } diff --git a/src/server.js b/src/server.js index 2c5187c..bf440d3 100644 --- a/src/server.js +++ b/src/server.js @@ -15,8 +15,8 @@ class Server extends EventEmitter { /** @type {Object} */ this.clients = {} this.clientCount = 0 - this.inLog = (...args) => debug('C -> S', ...args) - this.outLog = (...args) => debug('S -> C', ...args) + this.inLog = (...args) => debug('S ->', ...args) + this.outLog = (...args) => debug('S <-', ...args) } validateOptions () { @@ -40,6 +40,8 @@ class Server extends EventEmitter { onCloseConnection = (inetAddr, reason) => { console.debug('close connection', inetAddr, reason) + delete this.clients[inetAddr]?.connection // Prevent close loop + this.clients[inetAddr]?.close() delete this.clients[inetAddr] this.clientCount-- } diff --git a/src/serverPlayer.js b/src/serverPlayer.js index 49f7917..6fb03bb 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -21,8 +21,8 @@ class Player extends Connection { this.startQueue() this.status = ClientStatus.Authenticating - this.inLog = (...args) => console.info('S -> C', ...args) - this.outLog = (...args) => console.info('C -> S', ...args) + this.inLog = (...args) => console.info('S ->', ...args) + this.outLog = (...args) => console.info('S <-', ...args) } getData () { @@ -82,7 +82,7 @@ class Player extends Connection { /** * Disconnects a client */ - disconnect (reason, hide = false) { + disconnect (reason = 'Server closed', hide = false) { if ([ClientStatus.Authenticating, ClientStatus.Initializing].includes(this.status)) { this.sendDisconnectStatus('failed_server_full') } else { @@ -110,6 +110,7 @@ class Player extends Connection { clearInterval(this.loop) this.connection?.close() this.removeAllListeners() + this.status = ClientStatus.Disconnected } readPacket (packet) { @@ -136,6 +137,7 @@ class Player extends Connection { break case 'set_local_player_as_initialized': this.status = ClientStatus.Initialized + this.inLog('Server client spawned') // Emit the 'spawn' event this.emit('spawn') break From bb9b94fa0276b506632014d1647b418ee4999a1a Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 24 Mar 2021 08:30:24 -0400 Subject: [PATCH 03/21] Add internal server test --- package.json | 3 +- test/internal.js | 204 ++++++++++++++++++++++++++++++++++++++++ tools/genPacketDumps.js | 46 ++++++--- types/Item.js | 37 ++++++++ 4 files changed, 274 insertions(+), 16 deletions(-) create mode 100644 test/internal.js create mode 100644 types/Item.js diff --git a/package.json b/package.json index aec7b49..9bb997d 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "pretest": "npm run lint", "lint": "standard", "vanillaServer": "node tools/startVanillaServer.js", + "dumpPackets": "node tools/genPacketDumps.js", "fix": "standard --fix" }, "keywords": [ @@ -33,7 +34,7 @@ "node-fetch": "^2.6.1", "prismarine-nbt": "^1.5.0", "protodef": "^1.11.0", - "raknet-native": "^0.1.0", + "raknet-native": "^0.2.0", "uuid-1345": "^1.0.2" }, "devDependencies": { diff --git a/test/internal.js b/test/internal.js new file mode 100644 index 0000000..e01e932 --- /dev/null +++ b/test/internal.js @@ -0,0 +1,204 @@ +process.env.DEBUG = 'minecraft-protocol raknet' +const { Server, Client } = require('../') +const { dumpPackets, hasDumps } = require('../tools/genPacketDumps') +const DataProvider = require('../data/provider') + +// First we need to dump some packets that a vanilla server would send a vanilla +// client. Then we can replay those back in our custom server. +function prepare (version) { + if (!hasDumps(version)) { + return dumpPackets(version) + } +} + +async function startTest (version = '1.16.210', ok) { + await prepare(version) + const Item = require('../types/Item')(version) + const port = 19130 + const server = new Server({ hostname: '0.0.0.0', port, version }) + + function getPath (packetPath) { + return DataProvider(server.options.protocolVersion).getPath(packetPath) + } + + function get (packetPath) { + return require(getPath('sample/' + packetPath)) + } + + server.listen() + console.log('Started server') + + const respawnPacket = get('packets/respawn.json') + const chunks = await requestChunks(respawnPacket.x, respawnPacket.z, 1) + + let loop + + // server logic + server.on('connect', client => { + client.on('join', () => { + console.log('Client joined', client.getData()) + + client.write('resource_packs_info', { + must_accept: false, + has_scripts: false, + behaviour_packs: [], + texture_packs: [] + }) + + client.once('resource_pack_client_response', async rp => { + // Tell the server we will compress everything (>=1 byte) + client.write('network_settings', { compression_threshold: 1 }) + // Send some inventory slots + for (let i = 0; i < 3; i++) { + client.queue('inventory_slot', { window_id: 'armor', slot: 0, item: new Item().toBedrock() }) + } + + // client.queue('inventory_transaction', get('packets/inventory_transaction.json')) + client.queue('player_list', get('packets/player_list.json')) + client.queue('start_game', get('packets/start_game.json')) + client.queue('item_component', { entries: [] }) + client.queue('set_spawn_position', get('packets/set_spawn_position.json')) + client.queue('set_time', { time: 5433771 }) + client.queue('set_difficulty', { difficulty: 1 }) + client.queue('set_commands_enabled', { enabled: true }) + client.queue('adventure_settings', get('packets/adventure_settings.json')) + + client.queue('biome_definition_list', get('packets/biome_definition_list.json')) + client.queue('available_entity_identifiers', get('packets/available_entity_identifiers.json')) + + client.queue('update_attributes', get('packets/update_attributes.json')) + client.queue('creative_content', get('packets/creative_content.json')) + client.queue('inventory_content', get('packets/inventory_content.json')) + + client.queue('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true }) + + client.queue('crafting_data', get('packets/crafting_data.json')) + client.queue('available_commands', get('packets/available_commands.json')) + client.queue('chunk_radius_update', { chunk_radius: 5 }) + + // client.queue('set_entity_data', get('packets/set_entity_data.json')) + + client.queue('game_rules_changed', get('packets/game_rules_changed.json')) + client.queue('respawn', get('packets/game_rules_changed.json')) + + for (const chunk of chunks) { + client.queue('level_chunk', chunk) + } + + loop = 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' }) + }, 6000) + + // Respond to tick synchronization packets + client.on('tick_sync', (packet) => { + client.queue('tick_sync', { + request_time: packet.request_time, + response_time: BigInt(Date.now()) + }) + }) + }) + }) + }) + + // client logic + const client = new Client({ + hostname: '127.0.0.1', + port, + username: 'Notch', + version, + offline: true + }) + + console.log('Started client') + + client.once('resource_packs_info', (packet) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + resourcepackids: [] + }) + + client.once('resource_pack_stack', (stack) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + 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 }) + }) + + client.once('spawn', () => { + console.info('Client spawend!') + setTimeout(() => { + client.close() + + server.close() + ok?.() + }, 500) + clearInterval(loop) + }) +} + +const { ChunkColumn, Version } = require('bedrock-provider') +const { waitFor } = require('../src/datatypes/util') +const mcData = require('minecraft-data')('1.16') + +async function requestChunks (x, z, radius) { + const cxStart = (x >> 4) - radius + const cxEnd = (x >> 4) + radius + const czStart = (z >> 4) - radius + const czEnd = (z >> 4) + radius + + const stone = mcData.blocksByName.stone + const chunks = [] + + for (let cx = cxStart; cx < cxEnd; cx++) { + for (let cz = czStart; cz < czEnd; cz++) { + console.log('reading chunk at ', cx, cz) + const cc = new ChunkColumn(Version.v1_2_0_bis, x, z) + + for (let x = 0; x < 16; x++) { + for (let y = 0; y < 60; y++) { + for (let z = 0; z < 16; z++) { + cc.setBlock(x, y, z, stone) + } + } + } + + if (!cc) { + console.log('no chunk') + continue + } + const cbuf = await cc.networkEncodeNoCache() + chunks.push({ + x: cx, + z: cz, + sub_chunk_count: cc.sectionsLen, + cache_enabled: false, + blobs: [], + payload: cbuf + }) + // console.log('Ht',cc.sectionsLen,cc.sections) + } + } + + return chunks +} + +async function timedTest (version) { + await waitFor((res) => { + startTest(version, res) + }, 1000 * 60, () => { + throw Error('timed out') + }) + console.info('✔ ok') +} + +if (!module.parent) timedTest() +module.exports = { startTest, timedTest, requestChunks } diff --git a/tools/genPacketDumps.js b/tools/genPacketDumps.js index dd74150..3b3de74 100644 --- a/tools/genPacketDumps.js +++ b/tools/genPacketDumps.js @@ -1,20 +1,27 @@ -/* eslint-disable */ // Collect sample packets needed for `serverTest.js` // process.env.DEBUG = 'minecraft-protocol' const fs = require('fs') const vanillaServer = require('../tools/startVanillaServer') const { Client } = require('../src/client') -const { serialize, waitFor } = require('../src/datatypes/util') +const { serialize, waitFor, getFiles } = require('../src/datatypes/util') const { CURRENT_VERSION } = require('../src/options') const { join } = require('path') +function hasDumps (version) { + const root = join(__dirname, `../data/${version}/sample/packets/`) + if (!fs.existsSync(root) || getFiles(root).length < 10) { + return false + } + return true +} + let loop -async function main() { +async function dump (version, force) { const random = ((Math.random() * 100) | 0) const port = 19130 + random - const handle = await vanillaServer.startServerAndWait(CURRENT_VERSION, 1000 * 120, { 'server-port': port, path: 'bds_' }) + const handle = await vanillaServer.startServerAndWait(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port, path: 'bds_' }) console.log('Started server') const client = new Client({ @@ -25,9 +32,10 @@ async function main() { }) return waitFor(async res => { - const root = join(__dirname, `../data/${client.options.version}/sample/packets/`) - if (!fs.existsSync(root)) { - fs.mkdirSync(root, { recursive: true }) + const root = join(__dirname, `../data/${client.options.version}/sample/`) + if (!fs.existsSync(root + 'packets') || !fs.existsSync(root + 'chunks')) { + fs.mkdirSync(root + 'packets', { recursive: true }) + fs.mkdirSync(root + 'chunks', { recursive: true }) } client.once('resource_packs_info', (packet) => { @@ -43,7 +51,6 @@ async function main() { }) }) - 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 }) @@ -54,11 +61,17 @@ async function main() { }, 200) }) - client.on('packet', pakData => { // Packet dumping - if (pakData.name == 'level_chunk') return + let i = 0 + + client.on('packet', async packet => { // Packet dumping + const { name, params } = packet.data + if (name === 'level_chunk') { + fs.writeFileSync(root + `chunks/${name}-${i++}.bin`, packet.buffer) + return + } try { - if (!fs.existsSync(root + `${pakData.name}.json`)) { - fs.promises.writeFile(root + `${pakData.name}.json`, serialize(pakData.params, 2)) + if (!fs.existsSync(root + `packets/${name}.json`) || force) { + fs.writeFileSync(root + `packets/${name}.json`, serialize(params, 2)) } } catch (e) { console.log(e) } }) @@ -78,6 +91,9 @@ async function main() { }) } -main().then(() => { - console.log('Successfully dumped packets') -}) \ No newline at end of file +if (!module.parent) { + dump(null, true).then(() => { + console.log('Successfully dumped packets') + }) +} +module.exports = { dumpPackets: dump, hasDumps } diff --git a/types/Item.js b/types/Item.js new file mode 100644 index 0000000..eb4d804 --- /dev/null +++ b/types/Item.js @@ -0,0 +1,37 @@ +module.exports = (version) => + class Item { + nbt + constructor (obj) { + this.networkId = 0 + this.runtimeId = 0 + this.count = 0 + this.metadata = 0 + Object.assign(this, obj) + this.version = version + } + + static fromBedrock (obj) { + return new Item({ + runtimeId: obj.runtime_id, + networkId: obj.item?.network_id, + count: obj.item?.auxiliary_value & 0xff, + metadata: obj.item?.auxiliary_value >> 8, + nbt: obj.item?.nbt?.nbt + }) + } + + toBedrock () { + return { + runtime_id: this.runtimeId, + item: { + network_id: this.networkId, + auxiliary_value: (this.metadata << 8) | (this.count & 0xff), + has_nbt: !!this.nbt, + nbt: { version: 1, nbt: this.nbt }, + can_place_on: [], + can_destroy: [], + blocking_tick: 0 + } + } + } + } From 3f5b82f0f4af27342bcb0ac31118d81473bfa547 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 24 Mar 2021 08:32:32 -0400 Subject: [PATCH 04/21] protocol: use WindowID types --- data/1.16.201/protocol.json | 10 ++++----- data/1.16.210/protocol.json | 45 ++++++++++++++++++++++++++++++++----- data/latest/proto.yml | 10 ++++----- data/latest/types.yaml | 29 ++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 15 deletions(-) diff --git a/data/1.16.201/protocol.json b/data/1.16.201/protocol.json index 2dd5be8..361d139 100644 --- a/data/1.16.201/protocol.json +++ b/data/1.16.201/protocol.json @@ -4531,7 +4531,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "window_type", @@ -4552,7 +4552,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "server", @@ -4595,7 +4595,7 @@ [ { "name": "window_id", - "type": "varint" + "type": "WindowID" }, { "name": "slot", @@ -5481,7 +5481,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "window_type", @@ -5526,7 +5526,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "window_type", diff --git a/data/1.16.210/protocol.json b/data/1.16.210/protocol.json index fe48767..27a31ae 100644 --- a/data/1.16.210/protocol.json +++ b/data/1.16.210/protocol.json @@ -2546,6 +2546,41 @@ } } ], + "WindowIDVarint": [ + "mapper", + { + "type": "varint", + "mappings": { + "0": "inventory", + "1": "first", + "100": "last", + "119": "offhand", + "120": "armor", + "121": "creative", + "122": "hotbar", + "123": "fixed_inventory", + "124": "ui", + "-100": "drop_contents", + "-24": "beacon", + "-23": "trading_output", + "-22": "trading_use_inputs", + "-21": "trading_input_2", + "-20": "trading_input_1", + "-17": "enchant_output", + "-16": "enchant_material", + "-15": "enchant_input", + "-13": "anvil_output", + "-12": "anvil_result", + "-11": "anvil_material", + "-10": "container_input", + "-5": "crafting_use_ingredient", + "-4": "crafting_result", + "-3": "crafting_remove_ingredient", + "-2": "crafting_add_ingredient", + "-1": "none" + } + } + ], "WindowType": [ "mapper", { @@ -4672,7 +4707,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "window_type", @@ -4693,7 +4728,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "server", @@ -4736,7 +4771,7 @@ [ { "name": "window_id", - "type": "varint" + "type": "WindowIDVarint" }, { "name": "slot", @@ -5653,7 +5688,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "window_type", @@ -5698,7 +5733,7 @@ [ { "name": "window_id", - "type": "u8" + "type": "WindowID" }, { "name": "window_type", diff --git a/data/latest/proto.yml b/data/latest/proto.yml index f24b5fb..713dfff 100644 --- a/data/latest/proto.yml +++ b/data/latest/proto.yml @@ -896,7 +896,7 @@ packet_container_open: !bound: client # WindowID is the ID representing the window that is being opened. It may be used later to close the # container using a ContainerClose packet. - window_id: u8 + window_id: WindowID # ContainerType is the type ID of the container that is being opened when opening the container at the # position of the packet. It depends on the block/entity, and could, for example, be the window type of # a chest or a hopper, but also a horse inventory. @@ -917,7 +917,7 @@ packet_container_close: !bound: both # WindowID is the ID representing the window of the container that should be closed. It must be equal to # the one sent in the ContainerOpen packet to close the designated window. - window_id: u8 + window_id: WindowID # ServerSide determines whether or not the container was force-closed by the server. If this value is # not set correctly, the client may ignore the packet and respond with a PacketViolationWarning. server: bool @@ -954,7 +954,7 @@ packet_inventory_slot: !bound: both # WindowID is the ID of the window that the packet modifies. It must point to one of the windows that the # client currently has opened. - window_id: varint + window_id: WindowIDVarint # Slot is the index of the slot that the packet modifies. The new item will be set to the slot at this # index. slot: varint @@ -1448,7 +1448,7 @@ packet_update_trade: !id: 0x50 !bound: client # WindowID is the ID that identifies the trading window that the client currently has opened. - window_id: u8 + window_id: WindowID # WindowType is an identifier specifying the type of the window opened. In vanilla, it appears this is # always filled out with 15. window_type: WindowType @@ -1486,7 +1486,7 @@ packet_update_equipment: !bound: client # WindowID is the identifier associated with the window that the UpdateEquip packet concerns. It is the # ID sent for the horse inventory that was opened before this packet was sent. - window_id: u8 + window_id: WindowID # WindowType is the type of the window that was opened. Generally, this is the type of a horse inventory, # as the packet is specifically made for that. window_type: WindowType diff --git a/data/latest/types.yaml b/data/latest/types.yaml index 1c613d3..6ba41c8 100644 --- a/data/latest/types.yaml +++ b/data/latest/types.yaml @@ -996,6 +996,35 @@ WindowID: i8 => 123: fixed_inventory 124: ui +WindowIDVarint: varint => + -100: drop_contents + -24: beacon + -23: trading_output + -22: trading_use_inputs + -21: trading_input_2 + -20: trading_input_1 + -17: enchant_output + -16: enchant_material + -15: enchant_input + -13: anvil_output + -12: anvil_result + -11: anvil_material + -10: container_input + -5: crafting_use_ingredient + -4: crafting_result + -3: crafting_remove_ingredient + -2: crafting_add_ingredient + -1: none + 0: inventory + 1: first + 100: last + 119: offhand + 120: armor + 121: creative + 122: hotbar + 123: fixed_inventory + 124: ui + WindowType: u8 => 0: container 1: workbench From 43ef9c9430b657df608d14898cf4e0c06fdd6479 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 25 Mar 2021 02:33:56 -0400 Subject: [PATCH 05/21] Add internal client/server test --- src/auth/loginVerify.js | 5 ++--- src/serverPlayer.js | 10 +++++----- test/internal.js | 2 +- test/internal.test.js | 11 +++++++++++ 4 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 test/internal.test.js diff --git a/src/auth/loginVerify.js b/src/auth/loginVerify.js index 6d21e41..e1c2743 100644 --- a/src/auth/loginVerify.js +++ b/src/auth/loginVerify.js @@ -17,10 +17,10 @@ module.exports = (client, server, options) => { let pubKey = mcPubKeyToPem(getX5U(chain[0])) // the first one is client signed, allow it let finalKey = null - console.log(pubKey) + // console.log(pubKey) for (const token of chain) { const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' }) - console.log('Decoded', decoded) + // console.log('Decoded', decoded) // Check if signed by Mojang key const x5u = getX5U(token) @@ -69,7 +69,6 @@ function getX5U (token) { } function mcPubKeyToPem (mcPubKeyBuffer) { - console.log(mcPubKeyBuffer) if (mcPubKeyBuffer[0] === '-') return mcPubKeyBuffer let pem = '-----BEGIN PUBLIC KEY-----\n' let base64PubKey = mcPubKeyBuffer.toString('base64') diff --git a/src/serverPlayer.js b/src/serverPlayer.js index 6fb03bb..b11d8f4 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -1,6 +1,7 @@ const { ClientStatus, Connection } = require('./connection') const fs = require('fs') const Options = require('./options') +const debug = require('debug')('minecraft-protocol') const { Encrypt } = require('./auth/encryption') const Login = require('./auth/login') @@ -21,8 +22,8 @@ class Player extends Connection { this.startQueue() this.status = ClientStatus.Authenticating - this.inLog = (...args) => console.info('S ->', ...args) - this.outLog = (...args) => console.info('S <-', ...args) + this.inLog = (...args) => debug('S ->', ...args) + this.outLog = (...args) => debug('S <-', ...args) } getData () { @@ -125,10 +126,9 @@ class Player extends Connection { throw e } - console.log('-> S', des) switch (des.data.name) { case 'login': - console.log(des) + // console.log(des) this.onLogin(des) return case 'client_to_server_handshake': @@ -142,7 +142,7 @@ class Player extends Connection { this.emit('spawn') break default: - console.log('ignoring, unhandled') + // console.log('ignoring, unhandled') } this.emit(des.data.name, des.data.params) } diff --git a/test/internal.js b/test/internal.js index e01e932..6e04641 100644 --- a/test/internal.js +++ b/test/internal.js @@ -1,4 +1,4 @@ -process.env.DEBUG = 'minecraft-protocol raknet' +// process.env.DEBUG = 'minecraft-protocol raknet' const { Server, Client } = require('../') const { dumpPackets, hasDumps } = require('../tools/genPacketDumps') const DataProvider = require('../data/provider') diff --git a/test/internal.test.js b/test/internal.test.js new file mode 100644 index 0000000..e3feeac --- /dev/null +++ b/test/internal.test.js @@ -0,0 +1,11 @@ +/* eslint-env jest */ + +const { timedTest } = require('./internal') + +describe('internal client/server test', function () { + this.timeout(120 * 1000) + + it('connects', async () => { + await timedTest() + }) +}) From be98fc6cf8b2cceed1f4a6319c9125cd00d520c0 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 25 Mar 2021 04:38:46 -0400 Subject: [PATCH 06/21] test timeout fixes --- .github/workflows/ci.yml | 2 +- data/provider.js | 6 +++--- package.json | 2 +- test/internal.js | 4 ++-- tools/genPacketDumps.js | 1 + 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c565b62..7803afb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,8 @@ on: jobs: build: - runs-on: ubuntu-latest + timeout-minutes: 10 strategy: matrix: diff --git a/data/provider.js b/data/provider.js index 3f51f02..6ad1af7 100644 --- a/data/provider.js +++ b/data/provider.js @@ -2,7 +2,7 @@ const { Versions } = require('../src/options') const { getFiles } = require('../src/datatypes/util') const { join } = require('path') -const fileMap = {} +let fileMap = {} // Walks all the directories for each of the supported versions in options.js // then builds a file map for each version @@ -23,6 +23,8 @@ function loadVersions () { } module.exports = (protocolVersion) => { + fileMap = {} + loadVersions() return { // Returns the most recent file based on the specified protocolVersion // e.g. if `version` is 1.16 and a file for 1.16 doesn't exist, load from 1.15 file @@ -40,5 +42,3 @@ module.exports = (protocolVersion) => { } } } - -loadVersions() diff --git a/package.json b/package.json index 9bb997d..9be65ba 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "build": "cd tools && node compileProtocol.js", "prepare": "npm run build", - "test": "mocha", + "test": "mocha --bail", "pretest": "npm run lint", "lint": "standard", "vanillaServer": "node tools/startVanillaServer.js", diff --git a/test/internal.js b/test/internal.js index 6e04641..f87d258 100644 --- a/test/internal.js +++ b/test/internal.js @@ -191,10 +191,10 @@ async function requestChunks (x, z, radius) { return chunks } -async function timedTest (version) { +async function timedTest (version, timeout = 1000 * 120) { await waitFor((res) => { startTest(version, res) - }, 1000 * 60, () => { + }, timeout, () => { throw Error('timed out') }) console.info('✔ ok') diff --git a/tools/genPacketDumps.js b/tools/genPacketDumps.js index 3b3de74..33530d8 100644 --- a/tools/genPacketDumps.js +++ b/tools/genPacketDumps.js @@ -87,6 +87,7 @@ async function dump (version, force) { }) }, 1000 * 60, () => { clearInterval(loop) + handle.kill() throw Error('timed out') }) } From 0bdd071876365b0ce7bc17dfe15fee74bb2df46b Mon Sep 17 00:00:00 2001 From: extremeheat Date: Fri, 26 Mar 2021 04:38:25 -0400 Subject: [PATCH 07/21] client example updates --- examples/clientTest.js | 26 ++++++++++++++------------ src/client.js | 7 ++++++- test/vanilla.js | 6 ++++++ tools/genPacketDumps.js | 2 +- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/examples/clientTest.js b/examples/clientTest.js index 157f450..b523d19 100644 --- a/examples/clientTest.js +++ b/examples/clientTest.js @@ -1,10 +1,13 @@ process.env.DEBUG = 'minecraft-protocol raknet' const { Client } = require('bedrock-protocol') +const { ChunkColumn, Version } = require('bedrock-provider') async function test () { const client = new Client({ hostname: '127.0.0.1', - port: 19132 + port: 19130 + // You can specify version by adding : + // version: '1.16.210' }) client.once('resource_packs_info', (packet) => { @@ -20,23 +23,22 @@ async function test () { }) }) - // client.once('resource_packs_info', (packet) => { - // client.write('resource_pack_client_response', { - // response_status: 'completed', - // 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 }) }) - // var read = 0; - // client.on('level_chunk', (packet) => { - // read++ - // fs.writeFileSync(`level_chunk-${read}.json`, JSON.stringify(packet, null, 2)) - // }) + client.on('level_chunk', async packet => { + const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z) + await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count) + let blocks = [] + for (let x = 0; x < 16; x++) { + for (let z = 0; z < 16; z++) { + blocks.push(cc.getBlock(x, 0, z)) // Read some blocks in this chunk + } + } + }) } test() diff --git a/src/client.js b/src/client.js index bd841b3..829eb0d 100644 --- a/src/client.js +++ b/src/client.js @@ -41,6 +41,7 @@ class Client extends Connection { } this.startGameData = {} + this.clientRuntimeId = null this.startQueue() this.inLog = (...args) => debug('C ->', ...args) @@ -60,6 +61,10 @@ class Client extends Connection { } } + get entityId() { + return this.startGameData.runtime_entity_id + } + onEncapsulated = (encapsulated, inetAddr) => { const buffer = Buffer.from(encapsulated.buffer) this.handle(buffer) @@ -116,7 +121,7 @@ class Client extends Connection { if (this.status === ClientStatus.Initializing && this.options.autoInitPlayer === true) { if (statusPacket.status === 'player_spawn') { this.status = ClientStatus.Initialized - this.write('set_local_player_as_initialized', { runtime_entity_id: this.startGameData.runtime_entity_id }) + this.write('set_local_player_as_initialized', { runtime_entity_id: this.entityId }) this.emit('spawn') } } diff --git a/test/vanilla.js b/test/vanilla.js index 1088396..4f37d6b 100644 --- a/test/vanilla.js +++ b/test/vanilla.js @@ -2,6 +2,7 @@ const vanillaServer = require('../tools/startVanillaServer') const { Client } = require('../src/client') const { waitFor } = require('../src/datatypes/util') +const { ChunkColumn, Version } = require('bedrock-provider') async function test (version) { // Start the server, wait for it to accept clients, throws on timeout @@ -42,6 +43,11 @@ async function test (version) { client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: BigInt(Date.now()) }) }, 200) + client.on('level_chunk', async packet => { // Chunk read test + const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z) + await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count) + }) + console.log('Awaiting join') client.on('spawn', () => { diff --git a/tools/genPacketDumps.js b/tools/genPacketDumps.js index 33530d8..5add677 100644 --- a/tools/genPacketDumps.js +++ b/tools/genPacketDumps.js @@ -21,7 +21,7 @@ async function dump (version, force) { const random = ((Math.random() * 100) | 0) const port = 19130 + random - const handle = await vanillaServer.startServerAndWait(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port, path: 'bds_' }) + const handle = await vanillaServer.startServerAndWait(version || CURRENT_VERSION, 1000 * 120, { 'server-port': port }) console.log('Started server') const client = new Client({ From 3369fc279062d2c6095288752dbc81b0d10af32b Mon Sep 17 00:00:00 2001 From: extremeheat Date: Fri, 26 Mar 2021 04:41:42 -0400 Subject: [PATCH 08/21] update server example, use protocol updates Server example with bedrock-provider Use 64-bit varints for entity runtime ids --- data/1.16.210/protocol.json | 36 +++--- data/latest/proto.yml | 36 +++--- examples/clientTest.js | 5 +- examples/serverChunks.js | 48 ++++++++ examples/serverTest.js | 234 +++++++++++++++++------------------- src/client.js | 2 +- 6 files changed, 200 insertions(+), 161 deletions(-) create mode 100644 examples/serverChunks.js diff --git a/data/1.16.210/protocol.json b/data/1.16.210/protocol.json index 27a31ae..8c34b12 100644 --- a/data/1.16.210/protocol.json +++ b/data/1.16.210/protocol.json @@ -3756,7 +3756,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "platform_chat_id", @@ -3853,7 +3853,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "entity_type", @@ -3927,7 +3927,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "item", @@ -3972,7 +3972,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "target", @@ -3985,7 +3985,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "flags", @@ -4129,7 +4129,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "coordinates", @@ -4303,7 +4303,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "event_id", @@ -4381,7 +4381,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "event_id", @@ -4436,7 +4436,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "item", @@ -4461,7 +4461,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "helmet", @@ -4500,7 +4500,7 @@ }, { "name": "target_runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "position", @@ -4561,7 +4561,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "action", @@ -4591,7 +4591,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "metadata", @@ -4608,7 +4608,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "velocity", @@ -4673,7 +4673,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" } ] ], @@ -4698,7 +4698,7 @@ }, { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" } ] ], @@ -5321,7 +5321,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "status", @@ -6100,7 +6100,7 @@ [ { "name": "runtime_entity_id", - "type": "varint" + "type": "varint64" }, { "name": "unknown0", diff --git a/data/latest/proto.yml b/data/latest/proto.yml index 713dfff..68a7bbe 100644 --- a/data/latest/proto.yml +++ b/data/latest/proto.yml @@ -393,7 +393,7 @@ packet_add_player: entity_id_self: zigzag64 # The runtime ID of the player. The runtime ID is unique for each world session, # and entities are generally identified in packets using this runtime ID. - runtime_entity_id: varint + runtime_entity_id: varint64 # An identifier only set for particular platforms when chatting (presumably only for # Nintendo Switch). It is otherwise an empty string, and is used to decide which players # are able to chat with each other. @@ -423,7 +423,7 @@ packet_add_entity: !id: 0x0d !bound: client entity_id_self: zigzag64 - runtime_entity_id: varint + runtime_entity_id: varint64 entity_type: string x: lf32 y: lf32 @@ -447,7 +447,7 @@ packet_add_item_entity: !id: 0x0f !bound: client entity_id_self: zigzag64 - runtime_entity_id: varint + runtime_entity_id: varint64 item: Item x: lf32 y: lf32 @@ -461,13 +461,13 @@ packet_add_item_entity: packet_take_item_entity: !id: 0x11 !bound: client - runtime_entity_id: varint + runtime_entity_id: varint64 target: varint packet_move_entity: !id: 0x12 !bound: both - runtime_entity_id: varint + runtime_entity_id: varint64 flags: u8 position: vec3f rotation: Rotation @@ -559,7 +559,7 @@ packet_add_painting: !id: 0x16 !bound: client entity_id_self: zigzag64 - runtime_entity_id: varint + runtime_entity_id: varint64 coordinates: BlockCoordinates direction: zigzag32 title: string @@ -675,7 +675,7 @@ packet_block_event: packet_entity_event: !id: 0x1b !bound: both - runtime_entity_id: varint + runtime_entity_id: varint64 event_id: u8 => 1: jump 2: hurt_animation @@ -739,7 +739,7 @@ packet_entity_event: packet_mob_effect: !id: 0x1c !bound: client - runtime_entity_id: varint + runtime_entity_id: varint64 event_id: u8 effect_id: zigzag32 amplifier: zigzag32 @@ -765,7 +765,7 @@ packet_inventory_transaction: packet_mob_equipment: !id: 0x1f !bound: both - runtime_entity_id: varint + runtime_entity_id: varint64 item: Item slot: u8 selected_slot: u8 @@ -774,7 +774,7 @@ packet_mob_equipment: packet_mob_armor_equipment: !id: 0x20 !bound: both - runtime_entity_id: varint + runtime_entity_id: varint64 helmet: Item chestplate: Item leggings: Item @@ -793,7 +793,7 @@ packet_interact: 6: open_inventory # TargetEntityRuntimeID is the runtime ID of the entity that the player interacted with. This is empty # for the InteractActionOpenInventory action type. - target_runtime_entity_id: varint + target_runtime_entity_id: varint64 # Position associated with the ActionType above. For the InteractActionMouseOverEntity, this is the # position relative to the entity moused over over which the player hovered with its mouse/touch. For the # InteractActionLeaveVehicle, this is the position that the player spawns at after leaving the vehicle. @@ -822,7 +822,7 @@ packet_player_action: !bound: server # EntityRuntimeID is the runtime ID of the player. The runtime ID is unique for each world session, and # entities are generally identified in packets using this runtime ID. - runtime_entity_id: varint + runtime_entity_id: varint64 # ActionType is the ID of the action that was executed by the player. It is one of the constants that may # be found above. action: Action @@ -841,14 +841,14 @@ packet_hurt_armor: packet_set_entity_data: !id: 0x27 !bound: both - runtime_entity_id: varint + runtime_entity_id: varint64 metadata: MetadataDictionary tick: varint packet_set_entity_motion: !id: 0x28 !bound: both - runtime_entity_id: varint + runtime_entity_id: varint64 velocity: vec3f # SetActorLink is sent by the server to initiate an entity link client-side, meaning one entity will start @@ -877,7 +877,7 @@ packet_animate: !id: 0x2c !bound: both action_id: zigzag32 - runtime_entity_id: varint + runtime_entity_id: varint64 packet_respawn: !id: 0x2d @@ -886,7 +886,7 @@ packet_respawn: y: lf32 z: lf32 state: u8 - runtime_entity_id: varint + runtime_entity_id: varint64 # ContainerOpen is sent by the server to open a container client-side. This container must be physically # present in the world, for the packet to have any effect. Unlike Java Edition, Bedrock Edition requires that @@ -1287,7 +1287,7 @@ packet_boss_event: packet_show_credits: !id: 0x4b !bound: client - runtime_entity_id: varint + runtime_entity_id: varint64 status: zigzag32 # This packet sends a list of commands to the client. Commands can have @@ -1647,7 +1647,7 @@ packet_book_edit: packet_npc_request: !id: 0x62 !bound: both - runtime_entity_id: varint + runtime_entity_id: varint64 unknown0: u8 unknown1: string unknown2: u8 diff --git a/examples/clientTest.js b/examples/clientTest.js index b523d19..bcb644e 100644 --- a/examples/clientTest.js +++ b/examples/clientTest.js @@ -5,7 +5,7 @@ const { ChunkColumn, Version } = require('bedrock-provider') async function test () { const client = new Client({ hostname: '127.0.0.1', - port: 19130 + port: 19132 // You can specify version by adding : // version: '1.16.210' }) @@ -23,7 +23,6 @@ async function test () { }) }) - 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 }) @@ -32,7 +31,7 @@ async function test () { client.on('level_chunk', async packet => { const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z) await cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count) - let blocks = [] + const blocks = [] for (let x = 0; x < 16; x++) { for (let z = 0; z < 16; z++) { blocks.push(cc.getBlock(x, 0, z)) // Read some blocks in this chunk diff --git a/examples/serverChunks.js b/examples/serverChunks.js new file mode 100644 index 0000000..0b711c4 --- /dev/null +++ b/examples/serverChunks.js @@ -0,0 +1,48 @@ +// CHUNKS +const { WorldProvider } = require('bedrock-provider') +const { LevelDB } = require('leveldb-zlib') +const { join } = require('path') + +async function loadWorld (version) { + const path = join(__dirname, `../tools/bds-${version}/worlds/Bedrock level/db`) + console.log('Loading world at path', path) // Load world from testing server + const db = new LevelDB(path, { createIfMissing: false }) + await db.open() + const wp = new WorldProvider(db, { dimension: 0 }) + + async function requestChunks (x, z, radius) { + const chunks = [] + const cxStart = (x >> 4) - radius + const cxEnd = (x >> 4) + radius + const czStart = (z >> 4) - radius + const czEnd = (z >> 4) + radius + + for (let cx = cxStart; cx < cxEnd; cx++) { + for (let cz = czStart; cz < czEnd; cz++) { + // console.log('reading chunk at ', cx, cz) + + const cc = await wp.load(cx, cz, true) + if (!cc) { + // console.log('no chunk') + continue + } + const cbuf = await cc.networkEncodeNoCache() + chunks.push({ + x: cx, + z: cz, + sub_chunk_count: cc.sectionsLen, + cache_enabled: false, + blobs: [], + payload: cbuf + }) + // console.log('Ht',cc.sectionsLen,cc.sections) + } + } + + return chunks + } + + return { requestChunks } +} + +module.exports = { loadWorld } diff --git a/examples/serverTest.js b/examples/serverTest.js index 5006736..58b083e 100644 --- a/examples/serverTest.js +++ b/examples/serverTest.js @@ -1,39 +1,57 @@ -const fs = require('fs') -process.env.DEBUG = 'minecraft-protocol raknet' +/** + * bedrock-protocol server example; to run this example you need to clone this repo from git. + * first need to dump some packets from the vanilla server as there is alot of boilerplate + * to send to clients. + * + * In your server implementation, you need to implement each of the following packets to + * get a client to spawn like vanilla. You can look at the dumped packets in `data/1.16.10/sample` + * + * First, dump packets for version 1.16.210 by running `npm run dumpPackets`. + */ +process.env.DEBUG = 'minecraft-protocol' // packet logging +// const fs = require('fs') const { Server } = require('../src/server') -// const CreativeItems = require('../data/creativeitems.json') +const { hasDumps } = require('../tools/genPacketDumps') const DataProvider = require('../data/provider') +const { waitFor } = require('../src/datatypes/util') +const { loadWorld } = require('./serverChunks') -const server = new Server({ +async function startServer (version = '1.16.210', ok) { + if (!hasDumps(version)) { + throw Error('You need to dump some packets first. Run tools/genPacketDumps.js') + } -}) -server.create('0.0.0.0', 19132) + const Item = require('../types/Item')(version) + const port = 19132 + const server = new Server({ hostname: '0.0.0.0', port, version }) + let loop -function getPath (packetPath) { - return DataProvider(server.options.protocolVersion).getPath(packetPath) -} + const getPath = (packetPath) => DataProvider(server.options.protocolVersion).getPath(packetPath) + const get = (packetPath) => require(getPath('sample/' + packetPath)) -function get (packetPath) { - return require(getPath('sample/' + packetPath)) -} + server.listen() + console.log('Started server') -// const ran = false + // Find the center position from the dumped packets + const respawnPacket = get('packets/respawn.json') + const world = await loadWorld(version) + const chunks = await world.requestChunks(respawnPacket.x, respawnPacket.z, 2) -server.on('connect', ({ client }) => { - /** @type {Player} */ - client.on('join', () => { - console.log('Client joined', client.getData()) + // Connect is emitted when a client first joins our server, before authing them + server.on('connect', client => { + // Join is emitted after the client has been authenticated and encryption has started + client.on('join', () => { + console.log('Client joined', client.getData()) - // ResourcePacksInfo is sent by the server to inform the client on what resource packs the server has. It - // sends a list of the resource packs it has and basic information on them like the version and description. - client.write('resource_packs_info', { - must_accept: false, - has_scripts: false, - behaviour_packs: [], - texture_packs: [] - }) + // ResourcePacksInfo is sent by the server to inform the client on what resource packs the server has. It + // sends a list of the resource packs it has and basic information on them like the version and description. + client.write('resource_packs_info', { + must_accept: false, + has_scripts: false, + behaviour_packs: [], + texture_packs: [] + }) - 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', { @@ -45,109 +63,83 @@ server.on('connect', ({ client }) => { experiments_previously_used: false }) - client.once('resource_pack_client_response', async (packet) => { + client.once('resource_pack_client_response', async rp => { + // Tell the server we will compress everything (>=1 byte) + client.write('network_settings', { compression_threshold: 1 }) + // Send some inventory slots + for (let i = 0; i < 3; i++) { + client.queue('inventory_slot', { window_id: 120, slot: 0, item: new Item().toBedrock() }) + } - }) + client.write('player_list', get('packets/player_list.json')) + client.write('start_game', get('packets/start_game.json')) + client.write('item_component', { entries: [] }) + client.write('set_spawn_position', get('packets/set_spawn_position.json')) + client.write('set_time', { time: 5433771 }) + client.write('set_difficulty', { difficulty: 1 }) + client.write('set_commands_enabled', { enabled: true }) + client.write('adventure_settings', get('packets/adventure_settings.json')) + client.write('biome_definition_list', get('packets/biome_definition_list.json')) + client.write('available_entity_identifiers', get('packets/available_entity_identifiers.json')) + client.write('update_attributes', get('packets/update_attributes.json')) + client.write('creative_content', get('packets/creative_content.json')) + client.write('inventory_content', get('packets/inventory_content.json')) + client.write('player_hotbar', { selected_slot: 3, window_id: 'inventory', select_slot: true }) + client.write('crafting_data', get('packets/crafting_data.json')) + client.write('available_commands', get('packets/available_commands.json')) + client.write('chunk_radius_update', { chunk_radius: 1 }) + client.write('game_rules_changed', get('packets/game_rules_changed.json')) + client.write('respawn', get('packets/respawn.json')) - client.write('network_settings', { - compression_threshold: 1 - }) + for (const chunk of chunks) { + client.queue('level_chunk', chunk) + } - for (let i = 0; i < 3; i++) { - client.queue('inventory_slot', { inventory_id: 120, slot: i, uniqueid: 0, item: { network_id: 0 } }) - } + // Uncomment below and comment above to send dumped chunks. We use bedrock-provider in this example which is still a WIP, some blocks may be broken. + // for (const file of fs.readdirSync(`../data/${server.options.version}/sample/chunks`)) { + // const buffer = fs.readFileSync(`../data/${server.options.version}/sample/chunks/` + file) + // // console.log('Sending chunk', buffer) + // client.sendBuffer(buffer) + // } - client.queue('inventory_transaction', get('packets/inventory_transaction.json')) - client.queue('player_list', get('packets/player_list.json')) - client.queue('start_game', get('packets/start_game.json')) - client.queue('item_component', { entries: [] }) - client.queue('set_spawn_position', get('packets/set_spawn_position.json')) - client.queue('set_time', { time: 5433771 }) - client.queue('set_difficulty', { difficulty: 1 }) - client.queue('set_commands_enabled', { enabled: true }) - client.queue('adventure_settings', get('packets/adventure_settings.json')) + // Constantly send this packet to the client to tell it the center position for chunks. The client should then request these + // missing chunks from the us if it's missing any within the radius. `radius` is in blocks. + loop = setInterval(() => { + client.write('network_chunk_publisher_update', { coordinates: { x: respawnPacket.x, y: 130, z: respawnPacket.z }, radius: 80 }) + }, 4500) - client.queue('biome_definition_list', get('packets/biome_definition_list.json')) - client.queue('available_entity_identifiers', get('packets/available_entity_identifiers.json')) + // Wait some time to allow for the client to recieve and load all the chunks + setTimeout(() => { + // Allow the client to spawn + client.write('play_status', { status: 'player_spawn' }) + }, 6000) - client.queue('update_attributes', get('packets/update_attributes.json')) - client.queue('creative_content', get('packets/creative_content.json')) - client.queue('inventory_content', get('packets/inventory_content.json')) - client.queue('player_hotbar', { selected_slot: 3, window_id: 0, select_slot: true }) - - client.queue('crafting_data', get('packets/crafting_data.json')) - client.queue('available_commands', get('packets/available_commands.json')) - client.queue('chunk_radius_update', { chunk_radius: 5 }) - - client.queue('set_entity_data', get('packets/set_entity_data.json')) - - client.queue('game_rules_changed', get('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(`../data/${server.options.version}/sample/chunks`)) { - const buffer = Buffer.from(fs.readFileSync(`../data/${server.options.version}/sample/chunks/` + file, 'utf8'), 'hex') - // console.log('Sending chunk', chunk) - client.sendBuffer(buffer) - } - - // for (const chunk of chunks) { - // client.queue('level_chunk', chunk) - // } - - 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) - - // Respond to tick synchronization packets - client.on('tick_sync', (packet) => { - client.queue('tick_sync', { - request_time: packet.request_time, - response_time: BigInt(Date.now()) + // Respond to tick synchronization packets + client.on('tick_sync', (packet) => { + client.queue('tick_sync', { + request_time: packet.request_time, + response_time: BigInt(Date.now()) + }) }) }) }) }) + + ok() + + return { + kill: () => { + clearInterval(loop) + server.close() + } + } +} + +let server +waitFor((res) => { + server = startServer(process.argv[2], res) +}, 1000 * 60 /* Wait 60 seconds for the server to start */, function onTimeout () { + console.error('Server did not start in time') + server?.close() + process.exit(1) }) - -// CHUNKS -// const { ChunkColumn, Version } = require('bedrock-provider') -// const mcData = require('minecraft-data')('1.16') -// const chunks = [] -// async function buildChunks () { -// // "x": 40, -// // "z": 4, - -// const stone = mcData.blocksByName.stone - -// for (let cx = 35; cx < 45; cx++) { -// for (let cz = 0; cz < 8; cz++) { -// const column = new ChunkColumn(Version.v1_2_0_bis, x, z) -// for (let x = 0; x < 16; x++) { -// for (let y = 0; y < 60; y++) { -// for (let z = 0; z < 16; z++) { -// column.setBlock(x, y, z, stone) -// } -// } -// } - -// const ser = await column.networkEncodeNoCache() - -// chunks.push({ -// x: cx, -// z: cz, -// sub_chunk_count: column.sectionsLen, -// cache_enabled: false, -// blobs: [], -// payload: ser -// }) -// } -// } - -// // console.log('Chunks',chunks) -// } - -// // buildChunks() diff --git a/src/client.js b/src/client.js index 829eb0d..2fb3ce1 100644 --- a/src/client.js +++ b/src/client.js @@ -61,7 +61,7 @@ class Client extends Connection { } } - get entityId() { + get entityId () { return this.startGameData.runtime_entity_id } From 47c76ee5972df7f39ab8a7390dfa1250ae4366d3 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Fri, 26 Mar 2021 05:13:01 -0400 Subject: [PATCH 09/21] fix internal test packet path --- test/internal.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/internal.js b/test/internal.js index f87d258..40aeb5e 100644 --- a/test/internal.js +++ b/test/internal.js @@ -79,7 +79,7 @@ async function startTest (version = '1.16.210', ok) { // client.queue('set_entity_data', get('packets/set_entity_data.json')) client.queue('game_rules_changed', get('packets/game_rules_changed.json')) - client.queue('respawn', get('packets/game_rules_changed.json')) + client.queue('respawn', get('packets/respawn.json')) for (const chunk of chunks) { client.queue('level_chunk', chunk) From f5321dd277321f6ce8cd897b76b3f94e086e7678 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Tue, 30 Mar 2021 01:39:28 -0400 Subject: [PATCH 10/21] create pviewer exxample --- examples/viewer/client/BotProvider.js | 195 ++++++++++++++++++++++++++ examples/viewer/client/BotViewer.js | 68 +++++++++ examples/viewer/client/Chunk.js | 56 ++++++++ examples/viewer/client/app.css | 22 +++ examples/viewer/client/index.html | 22 +++ examples/viewer/client/index.js | 4 + examples/viewer/client/preload.js | 9 ++ examples/viewer/client/worker.js | 2 + examples/viewer/index.js | 37 +++++ examples/viewer/package.json | 13 ++ 10 files changed, 428 insertions(+) create mode 100644 examples/viewer/client/BotProvider.js create mode 100644 examples/viewer/client/BotViewer.js create mode 100644 examples/viewer/client/Chunk.js create mode 100644 examples/viewer/client/app.css create mode 100644 examples/viewer/client/index.html create mode 100644 examples/viewer/client/index.js create mode 100644 examples/viewer/client/preload.js create mode 100644 examples/viewer/client/worker.js create mode 100644 examples/viewer/index.js create mode 100644 examples/viewer/package.json diff --git a/examples/viewer/client/BotProvider.js b/examples/viewer/client/BotProvider.js new file mode 100644 index 0000000..d6c733b --- /dev/null +++ b/examples/viewer/client/BotProvider.js @@ -0,0 +1,195 @@ +/* eslint-disable */ +const { Client } = require('bedrock-protocol') +const { Version } = require('bedrock-provider') +const { WorldView } = require('prismarine-viewer/viewer') +const vec3, { Vec3 } = require('vec3') +const World = require('prismarine-world')() +const ChunkColumn = require('./Chunk')() +const Physics = require('prismarine-physics') + +class BotProvider extends WorldView { + chunks = {} + lastSentPos + + constructor() { + super() + this.connect() + this.listenToBot() + this.world = new World() + + // Server auth movement : we send inputs, server calculates position & sends back + this.serverMovements = true + + this.tick = 0n + } + + connect() { + const client = new Client({ hostname: '127.0.0.1', version: '1.16.210', port: 19132, connectTimeout: 100000 }) + + client.once('resource_packs_info', (packet) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + resourcepackids: [] + }) + + client.once('resource_pack_stack', (stack) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + 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 }) + }) + + this.client = client + } + + listenToBot() { + this.client.on('connect', () => { + console.log('Bot has connected!') + }) + this.client.on('start_game', packet => { + this.updatePosition(packet.player_position) + }) + + this.client.on('spawn', () => { + // server allows client to render chunks & spawn in world + this.emit('spawn', { position: this.lastPos }) + }) + + this.client.on('level_chunk', packet => { + const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z) + cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count).then(() => { + this.loadedChunks[(packet.x << 4) + ',' + (packet.z << 4)] = true + this.world.setColumn(packet.x, packet.z, cc) + const chunk = cc.serialize() + console.log('Chunk', chunk) + this.emitter.emit('loadChunk', { x: packet.x << 4, z: packet.z << 4, chunk }) + }) + }) + + this.client.on('move_player', packet => { + if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) + }) + + this.client.on('set_entity_motion', packet=>{ + if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) + }) + + this.client.on('tick_sync', (packet) => { + this.lastTick = packet.request_time + }) + + this.tickLoop = setInterval(() => { + client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + }) + } + + stopBot() { + clearInterval(this.tickLoop) + } + + // Server gives us a new position + updatePosition(pos) { + this.lastPos ??= vec3(pos) + super.updatePosition(this.lastPos) + } + + // Ask the server to be in a new position + requestPosition() { + const positionUpdated = !this.lastSentPos || !this.lastPos.equals(this.lastSentPos) + + if (positionUpdated) { + this.client.queue('player_auth_input', { + pitch: 0, + yaw: this.lastYaw, + position: { + x: this.lastPos.x, + y: this.lastPos.y, + z: this.lastPos.z + }, + move_vector: { x: 0, z: 0 }, + head_yaw: 0, + input_data: { + ascend: false, + descend: false, + north_jump: false, + jump_down: false, + sprint_down: false, + change_height: false, + jumping: false, + auto_jumping_in_water: false, + sneaking: false, + sneak_down: false, + up: false, + down: false, + left: false, + right: false, + up_left: false, + up_right: false, + want_up: false, + want_down: false, + want_down_slow: false, + want_up_slow: false, + sprinting: false, + ascend_scaffolding: false, + descend_scaffolding: false, + sneak_toggle_down: false, + persist_sneak: false + }, + input_mode: 'mouse', + play_mode: 'screen', + tick: this.tick, + delta: { x: 0, y: -0.07840000092983246, z: 0 } + }) + } + } + + initPhys() { + this.lastVel = new Vec3(0, 0, 0) + this.lastYaw = 0 + this.player = { + entity: { + position: this.lastPos, + velocity: this.lastVel, + onGround: false, + isInWater: false, + isInLava: false, + isInWeb: false, + isCollidedHorizontally: false, + isCollidedVertically: false, + yaw: this.lastYaw + }, + jumpTicks: 0, + jumpQueued: false + } + + this.physics = Physics(mcData, fakeWorld) + this.controls = { + forward: false, + back: false, + left: false, + right: false, + jump: false, + sprint: false, + sneak: false + } + this.playerState = new PlayerState(this.player, this.controls) + } + + startPhys() { + this.physicsLoop = setInterval(() => { + this.physics.simulatePlayer(this.playerState, this.world).apply(this.player) + this.requestPosition() + }, 50) + } + + stopPhys() { + clearInterval(this.physics) + } +} + +module.exports = { BotProvider } diff --git a/examples/viewer/client/BotViewer.js b/examples/viewer/client/BotViewer.js new file mode 100644 index 0000000..3782824 --- /dev/null +++ b/examples/viewer/client/BotViewer.js @@ -0,0 +1,68 @@ +/* global THREE */ +const { Viewer, MapControls } = require('prismarine-viewer/viewer') +const { Vec3 } = require('vec3') +const { BotProvider } = require('./BotProvider') +global.THREE = require('three') + +const MCVER = '1.16.1' + +class BotViewer { + constructor() { + // Create viewer data provider + this.world = new BotProvider() + } + + start() { + this.worldView = new BotProvider() + + // Create three.js context, add to page + this.renderer = new THREE.WebGLRenderer() + this.renderer.setPixelRatio(window.devicePixelRatio || 1) + this.renderer.setSize(window.innerWidth, window.innerHeight) + document.body.appendChild(this.renderer.domElement) + + // Create viewer + this.viewer = new Viewer(this.renderer) + this.viewer.setVersion(MCVER) + // Attach controls to viewer + this.controls = new MapControls(this.viewer.camera, this.renderer.domElement) + // Enable damping (inertia) on movement + this.controls.enableDamping = true + this.controls.dampingFactor = 0.09 + console.info('Registered handlers') + // Link WorldView and Viewer + this.viewer.listen(this.worldView) + + this.worldView.on('spawn', ({ position }) => { + // Initialize viewer, load chunks + this.worldView.init(position) + }) + + this.controls.update() + + // Browser animation loop + const animate = () => { + window.requestAnimationFrame(animate) + if (this.controls) this.controls.update() + this.viewer.update() + this.renderer.render(this.viewer.scene, this.viewer.camera) + } + animate() + + window.addEventListener('resize', () => { + this.viewer.camera.aspect = window.innerWidth / window.innerHeight + this.viewer.camera.updateProjectionMatrix() + this.renderer.setSize(window.innerWidth, window.innerHeight) + }) + } + + onKeyDown = () => { + + } + + registerBrowserEvents() { + this.renderer.domElement.addEventListener('keydown', this.onKeyDown) + } +} + +module.exports = { BotViewer } diff --git a/examples/viewer/client/Chunk.js b/examples/viewer/client/Chunk.js new file mode 100644 index 0000000..fdfd29f --- /dev/null +++ b/examples/viewer/client/Chunk.js @@ -0,0 +1,56 @@ +const { ChunkColumn, Version } = require('bedrock-provider') +const { SubChunk } = require('bedrock-provider/js/SubChunk') +try { var v8 = require('v8') } catch { } + +const Block = require('prismarine-block')('1.16.1') + +class ChunkColumnWrapped extends ChunkColumn { // pchunk compatiblity wrapper + // Block access + setBlockStateId(pos, stateId) { + super.setBlock(pos.x, pos.y, pos.z, Block.fromStateId(stateId)) + } + + getBlockStateId(pos) { + return super.getBlock(pos.x, pos.y, pos.z)?.stateId + } + + // // Serialization + // serialize() { + // if (typeof v8 === 'undefined') { + // return JSON.stringify(this) + // } else { + // const copy = { ...this, sections: [] } + // for (const section of this.sections) { + // copy.sections.push(v8.serialize(section)) + // } + // return v8.serialize(copy) + // } + // } + + // toJson() { return this.serialize() } + + // static deserialize(obj) { + // if (typeof obj === 'string') { + // Oject.assign(this, JSON.parse(obj)) + // } else { // Buffer + // const chunk = new ChunkColumnWrapped() + // const des = v8.deserialize(obj) + // Object.assign(chunk, des) + // chunk.sections = [] + // for (const section of des.sections) { + // const s = new SubChunk() + // chunk.sections.push(Object.assign(s, v8.deserialize(section))) + // } + // // console.log('Des',obj,chunk) + // return chunk + // } + // } + + // static fromJson(obj) { + // return ChunkColumnWrapped.deserialize(obj) + // } +} + +module.exports = (version) => { + return ChunkColumnWrapped +} \ No newline at end of file diff --git a/examples/viewer/client/app.css b/examples/viewer/client/app.css new file mode 100644 index 0000000..44c6bea --- /dev/null +++ b/examples/viewer/client/app.css @@ -0,0 +1,22 @@ +html { + overflow: hidden; +} + +html, body { + height: 100%; + margin: 0; + padding: 0; + font-family: sans-serif; +} + +a { + text-decoration: none; +} + +canvas { + height: 100%; + width: 100%; + font-size: 0; + margin: 0; + padding: 0; +} diff --git a/examples/viewer/client/index.html b/examples/viewer/client/index.html new file mode 100644 index 0000000..537be49 --- /dev/null +++ b/examples/viewer/client/index.html @@ -0,0 +1,22 @@ + + + + + Prismarine Viewer + + + + + +
+
Prismarine Viewer
+ +
+
Connecting to 127.0.0.1, port 19132...
+
+
+ + + + + \ No newline at end of file diff --git a/examples/viewer/client/index.js b/examples/viewer/client/index.js new file mode 100644 index 0000000..1b0334d --- /dev/null +++ b/examples/viewer/client/index.js @@ -0,0 +1,4 @@ +const { BotViewer } = require('./BotViewer') + +global.viewer = new BotViewer() +global.viewer.start() diff --git a/examples/viewer/client/preload.js b/examples/viewer/client/preload.js new file mode 100644 index 0000000..58594b5 --- /dev/null +++ b/examples/viewer/client/preload.js @@ -0,0 +1,9 @@ +// Required to detect electron in prismarine-viewer +globalThis.isElectron = true + +// If you need to disable node integration: +// * Node.js APIs will only be avaliable in this file +// * Use this file to load a viewer manager class +// based on one of the examples +// * Expose this class to the global window +// * Interact with the class in your code diff --git a/examples/viewer/client/worker.js b/examples/viewer/client/worker.js new file mode 100644 index 0000000..23ff320 --- /dev/null +++ b/examples/viewer/client/worker.js @@ -0,0 +1,2 @@ +// hack for path resolving +require('prismarine-viewer/viewer/lib/worker') diff --git a/examples/viewer/index.js b/examples/viewer/index.js new file mode 100644 index 0000000..f2a942c --- /dev/null +++ b/examples/viewer/index.js @@ -0,0 +1,37 @@ +const path = require('path') +const { app, BrowserWindow } = require('electron') + +function createMainWindow () { + const window = new BrowserWindow({ + webPreferences: { + nodeIntegration: true, + nodeIntegrationInWorker: true, + contextIsolation: false, + preload: path.join(__dirname, './client/preload.js') + } + }) + + // Open dev tools on load + window.webContents.openDevTools() + + window.loadFile(path.join(__dirname, './client/index.html')) + + window.webContents.on('devtools-opened', () => { + window.focus() + setImmediate(() => { + window.focus() + }) + }) + + return window +} + +app.on('ready', () => { + createMainWindow() +}) + +app.on('window-all-closed', function () { + app.quit() +}) + +app.allowRendererProcessReuse = false diff --git a/examples/viewer/package.json b/examples/viewer/package.json new file mode 100644 index 0000000..142f191 --- /dev/null +++ b/examples/viewer/package.json @@ -0,0 +1,13 @@ +{ + "name": "bedrock-protocol-viewer", + "description": "bedrock-protocol prismarine-viewer example", + "scripts": { + "start": "electron ." + }, + "dependencies": { + "bedrock-protocol": "file:../../", + "electron": "^12.0.2", + "patch-package": "^6.4.7", + "prismarine-viewer": "^1.19.1" + } +} From b79e4a65e40d28b315a4c6465a09c261d50b7543 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 3 Apr 2021 20:25:53 -0400 Subject: [PATCH 11/21] packet fixes, login chain update, raknet updates --- data/1.16.210/protocol.json | 34 +++++++++++++++++++--------------- data/latest/proto.yml | 23 +++++++++++++++++++---- data/latest/types.yaml | 2 +- src/auth/login.js | 7 ++++--- src/datatypes/util.js | 6 +++++- src/rak.js | 5 +++-- src/server.js | 3 ++- 7 files changed, 53 insertions(+), 27 deletions(-) diff --git a/data/1.16.210/protocol.json b/data/1.16.210/protocol.json index 8c34b12..9a61484 100644 --- a/data/1.16.210/protocol.json +++ b/data/1.16.210/protocol.json @@ -840,11 +840,11 @@ } ] ], - "creative": [ + "craft": [ "container", [ { - "name": "inventory_id", + "name": "action", "type": "varint" } ] @@ -858,15 +858,6 @@ } ] ], - "craft": [ - "container", - [ - { - "name": "action", - "type": "varint" - } - ] - ], "craft_slot": [ "container", [ @@ -4870,7 +4861,20 @@ ], "packet_gui_data_pick_item": [ "container", - [] + [ + { + "name": "item_name", + "type": "string" + }, + { + "name": "item_effects", + "type": "string" + }, + { + "name": "hotbar_slot", + "type": "li32" + } + ] ], "packet_adventure_settings": [ "container", @@ -6283,14 +6287,14 @@ }, { "name": "entity_unique_id", - "type": "varint" + "type": "zigzag64" }, { "name": "transition_type", "type": [ "mapper", { - "type": "varint", + "type": "varint64", "mappings": { "0": "entity", "1": "create", @@ -7162,7 +7166,7 @@ "type": [ "switch", { - "compareTo": "types.feet", + "compareTo": "type.feet", "fields": { "true": "zigzag32" }, diff --git a/data/latest/proto.yml b/data/latest/proto.yml index 68a7bbe..0d8db35 100644 --- a/data/latest/proto.yml +++ b/data/latest/proto.yml @@ -845,10 +845,16 @@ packet_set_entity_data: metadata: MetadataDictionary tick: varint +# SetActorMotion is sent by the server to change the client-side velocity of an entity. It is usually used +# in combination with server-side movement calculation. packet_set_entity_motion: !id: 0x28 !bound: both + # EntityRuntimeID is the runtime ID of the entity. The runtime ID is unique for each world session, and + # entities are generally identified in packets using this runtime ID. runtime_entity_id: varint64 + # Velocity is the new velocity the entity gets. This velocity will initiate the client-side movement of + # the entity. velocity: vec3f # SetActorLink is sent by the server to initiate an entity link client-side, meaning one entity will start @@ -1007,10 +1013,19 @@ packet_crafting_event: # Output is a list of items that were obtained as a result of crafting the recipe. result: Item[]varint - +# GUIDataPickItem is sent by the server to make the client 'select' a hot bar slot. It currently appears to +# be broken however, and does not actually set the selected slot to the hot bar slot set in the packet. packet_gui_data_pick_item: !id: 0x36 !bound: client + # ItemName is the name of the item that shows up in the top part of the popup that shows up when + # selecting an item. It is shown as if an item was selected by the player itself. + item_name: string + # ItemEffects is the line under the ItemName, where the effects of the item are usually situated. + item_effects: string + # HotBarSlot is the hot bar slot to be selected/picked. This does not currently work, so it does not + # matter what number this is. + hotbar_slot: li32 # AdventureSettings is sent by the server to update game-play related features, in particular permissions to # access these features for the client. It includes allowing the player to fly, build and mine, and attack @@ -1745,11 +1760,11 @@ packet_update_block_synced: # entity transitions from. # Note that for both possible values for TransitionType, the EntityUniqueID should point to the falling # block entity involved. - entity_unique_id: varint + entity_unique_id: zigzag64 # TransitionType is the type of the transition that happened. It is either BlockToEntityTransition, when # a block placed becomes a falling entity, or EntityToBlockTransition, when a falling entity hits the # ground and becomes a solid block again. - transition_type: varint => + transition_type: varint64 => # For falling sand, when a sand turns to an entity 0: entity # When sand turns back to a new block @@ -2231,7 +2246,7 @@ packet_player_armor_damage: if true: zigzag32 leggings_damage: type.legs ? if true: zigzag32 - boots_damage: types.feet ? + boots_damage: type.feet ? if true: zigzag32 ArmorDamageType: [ "bitflags", diff --git a/data/latest/types.yaml b/data/latest/types.yaml index 6ba41c8..b2d8730 100644 --- a/data/latest/types.yaml +++ b/data/latest/types.yaml @@ -448,7 +448,7 @@ TransactionActions: 100: craft_slot 99999: craft _: source_type? - if container or creative: + if container or craft: inventory_id: varint if world_interaction: flags: varint diff --git a/src/auth/login.js b/src/auth/login.js index 55c7ef9..49913b4 100644 --- a/src/auth/login.js +++ b/src/auth/login.js @@ -3,6 +3,7 @@ const JWT = require('jsonwebtoken') const DataProvider = require('../../data/provider') const ecPem = require('ec-pem') const curve = 'secp384r1' +const { nextUUID } = require('../datatypes/util') module.exports = (client, server, options) => { const skinGeom = fs.readFileSync(DataProvider(options.protocolVersion).getPath('skin_geom.txt'), 'utf-8') @@ -45,10 +46,10 @@ module.exports = (client, server, options) => { CapeImageHeight: 0, CapeImageWidth: 0, CapeOnClassicSkin: false, - ClientRandomId: 1, // TODO make biggeer + ClientRandomId: Date.now(), CurrentInputMode: 1, DefaultInputMode: 1, - DeviceId: '2099de18-429a-465a-a49b-fc4710a17bb3', // TODO random + DeviceId: nextUUID(), DeviceModel: '', DeviceOS: client.session?.deviceOS || 7, GameVersion: options.version || '1.16.201', @@ -64,7 +65,7 @@ module.exports = (client, server, options) => { // inside of PlayFab. PlayFabId: '5eb65f73-af11-448e-82aa-1b7b165316ad.persona-e199672a8c1a87e0-0', // 1.16.210 PremiumSkin: false, - SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343701', + SelfSignedId: '78eb38a6-950e-3ab9-b2cf-dd849e343702', ServerAddress: `${options.hostname}:${options.port}`, SkinAnimationData: '', SkinColor: '#ffffcd96', diff --git a/src/datatypes/util.js b/src/datatypes/util.js index 4a14e31..b080577 100644 --- a/src/datatypes/util.js +++ b/src/datatypes/util.js @@ -39,4 +39,8 @@ function uuidFrom (string) { return UUID.v3({ namespace: '6ba7b811-9dad-11d1-80b4-00c04fd430c8', name: string }) } -module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom } +function nextUUID () { + return uuidFrom(Date.now().toString()) +} + +module.exports = { getFiles, sleep, waitFor, serialize, uuidFrom, nextUUID } diff --git a/src/rak.js b/src/rak.js index e4a203d..88682d6 100644 --- a/src/rak.js +++ b/src/rak.js @@ -23,12 +23,13 @@ class RakNativeClient extends EventEmitter { this.raknet.on('encapsulated', ({ buffer, address }) => { this.onEncapsulated(buffer, address) }) - this.raknet.on('connected', () => { + + this.raknet.on('connect', () => { this.connected = true this.onConnected() }) - this.raknet.on('disconnected', ({ reason }) => { + this.raknet.on('disconnect', ({ reason }) => { this.connected = false this.onCloseConnection(reason) }) diff --git a/src/server.js b/src/server.js index bf440d3..c2dcdf7 100644 --- a/src/server.js +++ b/src/server.js @@ -3,7 +3,7 @@ const { createDeserializer, createSerializer } = require('./transforms/serialize const { Player } = require('./serverPlayer') const { RakServer } = require('./rak') const Options = require('./options') -const debug = require('debug')('minecraft-protocol') +const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol') class Server extends EventEmitter { constructor (options) { @@ -62,6 +62,7 @@ class Server extends EventEmitter { this.raknet.onOpenConnection = this.onOpenConnection this.raknet.onCloseConnection = this.onCloseConnection this.raknet.onEncapsulated = this.onEncapsulated + return { hostname, port } } close (disconnectReason) { From 88f88559af5800f171ec8c520061583540223d99 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 3 Apr 2021 20:30:01 -0400 Subject: [PATCH 12/21] switch to browserify-crypto on electron --- package.json | 3 ++- src/relay.js | 23 ++++++++++++++--------- src/transforms/encryption.js | 15 +++++++++------ 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 9be65ba..8090cfa 100644 --- a/package.json +++ b/package.json @@ -25,11 +25,12 @@ "@xboxreplay/xboxlive-auth": "^3.3.3", "aes-js": "^3.1.2", "asn1": "^0.2.4", - "bedrock-provider": "^0.1.1", + "browserify-cipher": "^1.0.1", "debug": "^4.3.1", "ec-pem": "^0.18.0", "jsonwebtoken": "^8.5.1", "jsp-raknet": "github:extremeheat/raknet#client", + "leveldb-zlib": "0.0.26", "minecraft-folder-path": "^1.1.0", "node-fetch": "^2.6.1", "prismarine-nbt": "^1.5.0", diff --git a/src/relay.js b/src/relay.js index ed78a09..8203ad8 100644 --- a/src/relay.js +++ b/src/relay.js @@ -2,7 +2,7 @@ const { Client } = require('./client') const { Server } = require('./server') const { Player } = require('./serverPlayer') -const debug = require('debug')('minecraft-protocol relay') +const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol relay') const { serialize } = require('./datatypes/util') /** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */ @@ -22,10 +22,10 @@ class RelayPlayer extends Player { }) 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.upInLog = (...msg) => console.debug('** Backend -> Proxy', ...msg) + this.upOutLog = (...msg) => console.debug('** Proxy -> Backend', ...msg) + this.downInLog = (...msg) => console.debug('** Client -> Proxy', ...msg) + this.downOutLog = (...msg) => console.debug('** Proxy -> Client', ...msg) if (!server.options.logging) { this.upInLog = () => { } @@ -52,11 +52,11 @@ class RelayPlayer extends Player { this.downQ.push(packet) return } - this.upInLog('Recv packet', packet) + // 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('~ Bounce B->C', name, serialize(params).slice(0, 100)) // this.upInLog('~ ', des.buffer) if (name === 'play_status' && params.status === 'login_success') return // We already sent this, this needs to be sent ASAP or client will disconnect @@ -72,6 +72,8 @@ class RelayPlayer extends Player { this.queue(name, params) // this.sendBuffer(packet) + + this.emit('clientbound', des.data) } // Send queued packets to the connected client @@ -105,7 +107,7 @@ class RelayPlayer extends Player { return } this.flushUpQueue() // Send queued packets - this.downInLog('Recv packet', packet) + // this.downInLog('Recv packet', packet) // TODO: If we fail to parse a packet, proxy it raw and log an error const des = this.server.deserializer.parsePacketBuffer(packet) @@ -129,6 +131,7 @@ class RelayPlayer extends Player { this.downInLog('Relaying', des.data) this.upstream.sendBuffer(packet) } + this.emit('serverbound', des.data) } else { super.readPacket(packet) } @@ -162,6 +165,8 @@ class Relay extends Server { ds.flushUpQueue() console.log('Connected to upstream server') client.readPacket = (packet) => ds.readUpstream(packet) + + this.emit('join', /* client connected to proxy */ ds, /* backend server */ client) }) this.upstreams.set(clientAddr.hash, client) } @@ -183,7 +188,7 @@ class Relay extends Server { const player = new this.RelayPlayer(this, conn) console.debug('New connection from', conn.address) this.clients[conn.address] = player - this.emit('connect', { client: player }) + this.emit('connect', player) this.openUpstreamConnection(player, conn.address) } } diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index 41432c1..81a70af 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -1,5 +1,6 @@ const { Transform } = require('readable-stream') -const crypto = require('crypto') +const crypto = globalThis.isElectron ? require('browserify-cipher/browser') : require('crypto') +const { createHash } = require('crypto') const aesjs = require('aes-js') const Zlib = require('zlib') @@ -52,7 +53,7 @@ class Decipher extends Transform { } function computeCheckSum (packetPlaintext, sendCounter, secretKeyBytes) { - const digest = crypto.createHash('sha256') + const digest = createHash('sha256') const counter = Buffer.alloc(8) counter.writeBigInt64LE(sendCounter, 0) digest.update(counter) @@ -70,10 +71,12 @@ function createEncryptor (client, iv) { // The send counter is represented as a little-endian 64-bit long and incremented after each packet. function process (chunk) { - const buffer = Zlib.deflateRawSync(chunk, { level: 7 }) - const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)]) - client.sendCounter++ - client.cipher.write(packet) + Zlib.deflateRaw(chunk, { level: 7 }, (err, buffer) => { + if (err) throw err + const packet = Buffer.concat([buffer, computeCheckSum(buffer, client.sendCounter, client.secretKeyBytes)]) + client.sendCounter++ + client.cipher.write(packet) + }) } client.cipher.on('data', client.onEncryptedPacket) From d2c0d3c386465d6302893717fbe31aa2e57ba28a Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 3 Apr 2021 20:34:42 -0400 Subject: [PATCH 13/21] viewer: add proxy example --- examples/viewer/client/BotProvider.js | 252 ++++++++++++++++++------ examples/viewer/client/BotViewer.js | 31 +-- examples/viewer/client/Chunk.js | 8 +- examples/viewer/client/ProxyProvider.js | 126 ++++++++++++ examples/viewer/package.json | 2 + src/connection.js | 13 +- 6 files changed, 356 insertions(+), 76 deletions(-) create mode 100644 examples/viewer/client/ProxyProvider.js diff --git a/examples/viewer/client/BotProvider.js b/examples/viewer/client/BotProvider.js index d6c733b..f4d0d37 100644 --- a/examples/viewer/client/BotProvider.js +++ b/examples/viewer/client/BotProvider.js @@ -2,14 +2,19 @@ const { Client } = require('bedrock-protocol') const { Version } = require('bedrock-provider') const { WorldView } = require('prismarine-viewer/viewer') -const vec3, { Vec3 } = require('vec3') +const vec3 = require('vec3') const World = require('prismarine-world')() const ChunkColumn = require('./Chunk')() -const Physics = require('prismarine-physics') +const { Physics, PlayerState } = require('prismarine-physics') +const { performance } = require('perf_hooks') + +const PHYSICS_INTERVAL_MS = 50 +const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000 class BotProvider extends WorldView { chunks = {} lastSentPos + positionUpdated = true constructor() { super() @@ -19,7 +24,6 @@ class BotProvider extends WorldView { // Server auth movement : we send inputs, server calculates position & sends back this.serverMovements = true - this.tick = 0n } @@ -47,6 +51,10 @@ class BotProvider extends WorldView { this.client = client } + close() { + this.client?.close() + } + listenToBot() { this.client.on('connect', () => { console.log('Bot has connected!') @@ -58,33 +66,39 @@ class BotProvider extends WorldView { this.client.on('spawn', () => { // server allows client to render chunks & spawn in world this.emit('spawn', { position: this.lastPos }) + + this.tickLoop = setInterval(() => { + this.client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + }) }) this.client.on('level_chunk', packet => { - const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z) - cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count).then(() => { - this.loadedChunks[(packet.x << 4) + ',' + (packet.z << 4)] = true - this.world.setColumn(packet.x, packet.z, cc) - const chunk = cc.serialize() - console.log('Chunk', chunk) - this.emitter.emit('loadChunk', { x: packet.x << 4, z: packet.z << 4, chunk }) - }) + this.handleChunk(packet) }) this.client.on('move_player', packet => { if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) }) - this.client.on('set_entity_motion', packet=>{ + this.client.on('set_entity_motion', packet => { if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) }) this.client.on('tick_sync', (packet) => { - this.lastTick = packet.request_time + this.lastTick = packet.response_time }) + } - this.tickLoop = setInterval(() => { - client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + handleChunk(packet, render = true) { + const hash = (packet.x << 4) + ',' + (packet.z << 4) + if (this.loadChunk[hash]) return + const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z) + cc.networkDecodeNoCache(packet.payload, packet.sub_chunk_count).then(() => { + this.loadedChunks[hash] = true + this.world.setColumn(packet.x, packet.z, cc) + const chunk = cc.serialize() + // console.log('Chunk', chunk) + if (render) this.emitter.emit('loadChunk', { x: packet.x << 4, z: packet.z << 4, chunk }) }) } @@ -99,60 +113,53 @@ class BotProvider extends WorldView { } // Ask the server to be in a new position - requestPosition() { + requestPosition(time, inputState) { const positionUpdated = !this.lastSentPos || !this.lastPos.equals(this.lastSentPos) + // if (globalThis.logging)console.log('New pos', this.lastSentPos,this.lastPos) if (positionUpdated) { + this.lastSentPos = this.lastPos.clone() + console.log('We computed', this.lastPos) + this.pushCamera({ + position: this.lastSentPos, + input_data: {}, + yaw: this.playerState.yaw, pitch: this.playerState.pitch + }, 2) + return this.client.queue('player_auth_input', { - pitch: 0, - yaw: this.lastYaw, + pitch: this.player.pitch, + yaw: this.player.yaw, position: { x: this.lastPos.x, y: this.lastPos.y, z: this.lastPos.z }, - move_vector: { x: 0, z: 0 }, - head_yaw: 0, - input_data: { - ascend: false, - descend: false, - north_jump: false, - jump_down: false, - sprint_down: false, - change_height: false, - jumping: false, - auto_jumping_in_water: false, - sneaking: false, - sneak_down: false, - up: false, - down: false, - left: false, - right: false, - up_left: false, - up_right: false, - want_up: false, - want_down: false, - want_down_slow: false, - want_up_slow: false, - sprinting: false, - ascend_scaffolding: false, - descend_scaffolding: false, - sneak_toggle_down: false, - persist_sneak: false + move_vector: { // Minecraft coords, N: Z+1, S: Z-1, W: X+1, E: X-1 + x: inputState.left ? 1 : (inputState.right ? -1 : 0), + z: inputState.up ? 1 : (inputState.down ? -1 : 0) }, + head_yaw: this.player.headYaw, + input_data: inputState, input_mode: 'mouse', play_mode: 'screen', tick: this.tick, - delta: { x: 0, y: -0.07840000092983246, z: 0 } + delta: this.lastSentPos?.minus(this.lastPos) ?? { x: 0, y: 0, z: 0 } }) + this.positionUpdated = false + this.lastSentPos = this.lastPos.clone() } } - initPhys() { - this.lastVel = new Vec3(0, 0, 0) - this.lastYaw = 0 + initPhys(position, velocity, yaw = 0, pitch = 0, headYaw = 0) { + this.lastPos = position ? vec3(position) : vec3(0, 0, 0) + this.lastVel = velocity ? vec3(velocity) : vec3(0, 0, 0) this.player = { + version: '1.16.1', + inventory: { + slots: [] + }, entity: { + effects: {}, position: this.lastPos, velocity: this.lastVel, onGround: false, @@ -161,13 +168,23 @@ class BotProvider extends WorldView { isInWeb: false, isCollidedHorizontally: false, isCollidedVertically: false, - yaw: this.lastYaw + yaw, + pitch, + headYaw // bedrock + }, + events: { // Control events to send next tick + startSprint: false, + stopSprint: false, + startSneak: false, + stopSneak: false }, jumpTicks: 0, - jumpQueued: false + jumpQueued: false, + downJump: false } - this.physics = Physics(mcData, fakeWorld) + const mcData = require('minecraft-data')('1.16.1') + this.physics = Physics(mcData, this.world) this.controls = { forward: false, back: false, @@ -180,16 +197,137 @@ class BotProvider extends WorldView { this.playerState = new PlayerState(this.player, this.controls) } + // This function should be executed each tick (every 0.05 seconds) + // How it works: https://gafferongames.com/post/fix_your_timestep/ + timeAccumulator = 0 + lastPhysicsFrameTime = null + inputQueue = [] + doPhysics() { + const now = performance.now() + const deltaSeconds = (now - this.lastPhysicsFrameTime) / 1000 + this.lastPhysicsFrameTime = now + + this.timeAccumulator += deltaSeconds + + while (this.timeAccumulator >= PHYSICS_TIMESTEP) { + let q = this.inputQueue.shift() + if (q) { + Object.assign(this.playerState.control, q) + if (q.yaw) { this.player.entity.yaw = q.yaw; this.playerState.yaw = q.yaw; } + if (q.pitch) this.player.entity.pitch = q.pitch + } + this.physics.simulatePlayer(this.playerState, this.world.sync).apply(this.player) + this.lastPos = this.playerState.pos + this.requestPosition(PHYSICS_TIMESTEP, { + ascend: false, + descend: false, + // Players bob up and down in water, north jump is true when going up. + // In water this is only true after the player has reached max height before bobbing back down. + north_jump: this.player.jumpTicks > 0, // Jump + jump_down: this.controls.jump, // Jump + sprint_down: this.controls.sprint, + change_height: false, + jumping: this.controls.jump, // Jump + auto_jumping_in_water: false, + sneaking: false, + sneak_down: false, + up: this.controls.forward, + down: this.controls.back, + left: this.controls.left, + right: this.controls.right, + up_left: false, + up_right: false, + want_up: this.controls.jump, // Jump + want_down: false, + want_down_slow: false, + want_up_slow: false, + sprinting: false, + ascend_scaffolding: false, + descend_scaffolding: false, + sneak_toggle_down: false, + persist_sneak: false, + start_sprinting: this.player.events.startSprint || false, + stop_sprinting: this.player.events.stopSprint || false, + start_sneaking: this.player.events.startSneak || false, + stop_sneaking: this.player.events.stopSneak || false, + // Player is Update Aqatic swimming + start_swimming: false, + // Player stops Update Aqatic swimming + stop_swimming: false, + start_jumping: this.player.jumpTicks === 1, // Jump + start_gliding: false, + stop_gliding: false, + }) + this.timeAccumulator -= PHYSICS_TIMESTEP + } + } + startPhys() { + console.log('Start phys') this.physicsLoop = setInterval(() => { - this.physics.simulatePlayer(this.playerState, this.world).apply(this.player) - this.requestPosition() - }, 50) + this.doPhysics() + }, PHYSICS_INTERVAL_MS) + } + + setControlState(control, state) { + if (this.controls[control] === state) return + if (control === 'sprint') { + this.player.events.startSprint = state + this.player.events.stopSprint = !state + this.controls.sprint = true + } else if (control === 'sneak') { + this.player.events.startSneak = state + this.player.events.stopSneak = !state + this.controls.sprint = true + } + } + + pushInputState(state, yaw, pitch) { + const yawRad = d2r(yaw) + const pitchRad = d2r(pitch) + this.inputQueue.push({ + forward: state.up, + back: state.down,// TODO: left and right switched ??? + left: state.right, + right: state.left, + jump: state.jump_down, + sneak: state.sprint_down, + yaw: yawRad, pitch: pitchRad, + }) + globalThis.yaw = [yaw, yawRad] + if (global.logYaw) console.log('Pushed', yaw, pitch) + } + + pushCamera(state, id = 1) { + let { x, y, z } = state.position + if (id == 1) y -= 1.62 // account for player bb + const pos = vec3({ x, y, z }) + if (state.position) { + viewer.viewer.entities.update({ + name: 'player', + id, pos, width: 0.6, height: 1.8, + yaw: id == 1 ? d2r(state.yaw) : state.yaw + }) + + //viewer.viewer.camera.position.set(x, y, z) + } + + if (state.input_data.sneak_down) { + this.player.entity.position = pos + this.playerState.pos = this.player.entity.position + } + } + + onCameraMovement(newYaw, newPitch, newHeadYaw) { + this.player.yaw = newYaw + this.player.pitch = newPitch + this.player.headYaw = newHeadYaw } stopPhys() { - clearInterval(this.physics) + clearInterval(this.physicsLoop) } } +const d2r = deg => (180 - (deg < 0 ? (360 + deg) : deg)) * (Math.PI / 180) module.exports = { BotProvider } diff --git a/examples/viewer/client/BotViewer.js b/examples/viewer/client/BotViewer.js index 3782824..7600b0a 100644 --- a/examples/viewer/client/BotViewer.js +++ b/examples/viewer/client/BotViewer.js @@ -2,19 +2,20 @@ const { Viewer, MapControls } = require('prismarine-viewer/viewer') const { Vec3 } = require('vec3') const { BotProvider } = require('./BotProvider') +const { ProxyProvider } = require('./ProxyProvider') global.THREE = require('three') const MCVER = '1.16.1' class BotViewer { - constructor() { - // Create viewer data provider - this.world = new BotProvider() + constructor () { + } - start() { - this.worldView = new BotProvider() - + start () { + // this.bot = new BotProvider() + this.bot = new ProxyProvider() + // return // Create three.js context, add to page this.renderer = new THREE.WebGLRenderer() this.renderer.setPixelRatio(window.devicePixelRatio || 1) @@ -31,11 +32,13 @@ class BotViewer { this.controls.dampingFactor = 0.09 console.info('Registered handlers') // Link WorldView and Viewer - this.viewer.listen(this.worldView) + this.viewer.listen(this.bot) - this.worldView.on('spawn', ({ position }) => { + this.bot.on('spawn', ({ position }) => { // Initialize viewer, load chunks - this.worldView.init(position) + this.bot.init(position) + // Start listening for keys + this.registerBrowserEvents() }) this.controls.update() @@ -56,12 +59,14 @@ class BotViewer { }) } - onKeyDown = () => { - + onKeyDown = (evt) => { + console.log('Key down', evt) + // this.bot.initPhys() + // this.bot.startPhys() } - registerBrowserEvents() { - this.renderer.domElement.addEventListener('keydown', this.onKeyDown) + registerBrowserEvents () { + this.renderer.domElement.parentElement.addEventListener('keydown', this.onKeyDown) } } diff --git a/examples/viewer/client/Chunk.js b/examples/viewer/client/Chunk.js index fdfd29f..b2ecc57 100644 --- a/examples/viewer/client/Chunk.js +++ b/examples/viewer/client/Chunk.js @@ -1,16 +1,16 @@ const { ChunkColumn, Version } = require('bedrock-provider') const { SubChunk } = require('bedrock-provider/js/SubChunk') -try { var v8 = require('v8') } catch { } +try { const v8 = require('v8') } catch { } const Block = require('prismarine-block')('1.16.1') class ChunkColumnWrapped extends ChunkColumn { // pchunk compatiblity wrapper // Block access - setBlockStateId(pos, stateId) { + setBlockStateId (pos, stateId) { super.setBlock(pos.x, pos.y, pos.z, Block.fromStateId(stateId)) } - getBlockStateId(pos) { + getBlockStateId (pos) { return super.getBlock(pos.x, pos.y, pos.z)?.stateId } @@ -53,4 +53,4 @@ class ChunkColumnWrapped extends ChunkColumn { // pchunk compatiblity wrapper module.exports = (version) => { return ChunkColumnWrapped -} \ No newline at end of file +} diff --git a/examples/viewer/client/ProxyProvider.js b/examples/viewer/client/ProxyProvider.js new file mode 100644 index 0000000..0db00df --- /dev/null +++ b/examples/viewer/client/ProxyProvider.js @@ -0,0 +1,126 @@ +const { Relay } = require('bedrock-protocol') +const { BotProvider } = require('./BotProvider') +const vec3 = require('vec3') + +class ProxyProvider extends BotProvider { + lastPlayerMovePacket + + connect () { + const proxy = new Relay({ + hostname: '0.0.0.0', + port: 19130, + // logging: true, + destination: { + hostname: '127.0.0.1', + port: 19132 + } + }) + + proxy.listen() + + console.info('Waiting for connect') + + const maxChunks = 40 + + proxy.on('join', (client, server) => { + client.on('clientbound', ({ name, params }) => { + if (name == 'level_chunk') { + // maxChunks-- + // if (maxChunks >= 0) { + // this.handleChunk(params) + // } + this.handleChunk(params, true) + } else if (name == 'start_game') { + this.initPhys(params.player_position, null, params.rotation.z, params.rotation.x, 0) + } else if (name === 'play_status') { + // this.emit('spawn', { position: server.startGameData.player_position }) + + this.startPhys() + console.info('Started physics!') + } else if (name === 'move_player') { + console.log('move_player', packet) + // if (packet.runtime_id === server.entityId) { + // this.updatePosition(packet.position) + // if (this.lastServerMovement.x == packet.position.x && this.lastServerMovement.y == packet.position.y && this.lastServerMovement.z == packet.position.z) { + + // } else { + // console.log('Server computed', packet.position) + // } + // this.lastServerMovement = { ...packet.position } + // } + } + if (name.includes('entity') || name.includes('network_chunk_publisher_update') || name.includes('tick') || name.includes('level')) return + console.log('CB', name) + }) + + client.on('serverbound', ({ name, params }) => { + // { name, params } + if (name == 'player_auth_input') { + // console.log('player_auth_input', this.lastPlayerMovePacket, params) + + // this.controls.forward = params.input_data.up + // this.controls.back = params.input_data.down + // this.controls.left = params.input_data.left + // this.controls.right = params.input_data.right + // this.player.entity.pitch = params.pitch + // this.player.entity.yaw = params.yaw + this.pushInputState(params.input_data, params.yaw, params.pitch) + this.pushCamera(params) + this.lastMovePacket = params + + // Log Movement deltas + { + if (this.firstPlayerMovePacket) { + const id = diff(this.firstPlayerMovePacket.input_data, params.input_data) + const md = diff(this.firstPlayerMovePacket.move_vector, params.move_vector) + const dd = diff(this.firstPlayerMovePacket.delta, params.delta) + if (id || md) { + if (globalThis.logging) console.log('Move', params.position, id, md, dd) + globalThis.movements ??= [] + globalThis.movements.push(params) + } + } + if (!this.firstPlayerMovePacket) { + this.firstPlayerMovePacket = params + for (const key in params.input_data) { + params.input_data[key] = false + } + params.input_data._value = 0n + params.move_vector = { x: 0, z: 0 } + params.delta = { x: 0, y: 0, z: 0 } + } + } + } else if (!name.includes('tick') && !name.includes('level')) { + console.log('Sending', name) + } + }) + console.info('Client and Server Connected!') + }) + + this.proxy = proxy + } + + listenToBot () { + + } + + close () { + this.proxy?.close() + } +} + +const difference = (o1, o2) => Object.keys(o2).reduce((diff, key) => { + if (o1[key] === o2[key]) return diff + return { + ...diff, + [key]: o2[key] + } +}, {}) + +// console.log = () => {} +// console.debug = () => {} + +const diff = (o1, o2) => { const dif = difference(o1, o2); return Object.keys(dif).length ? dif : null } + +module.exports = { ProxyProvider } +globalThis.logging = true diff --git a/examples/viewer/package.json b/examples/viewer/package.json index 142f191..9895117 100644 --- a/examples/viewer/package.json +++ b/examples/viewer/package.json @@ -6,8 +6,10 @@ }, "dependencies": { "bedrock-protocol": "file:../../", + "browserify-cipher": "^1.0.1", "electron": "^12.0.2", "patch-package": "^6.4.7", + "prismarine-physics": "^1.2.2", "prismarine-viewer": "^1.19.1" } } diff --git a/src/connection.js b/src/connection.js index a9061c0..6a55a1a 100644 --- a/src/connection.js +++ b/src/connection.js @@ -15,10 +15,19 @@ const ClientStatus = { } class Connection extends EventEmitter { - status = ClientStatus.Disconnected + #status = ClientStatus.Disconnected q = [] q2 = [] + get status () { + return this.#status + } + + set status (val) { + this.inLog('* new status', val) + this.#status = val + } + versionLessThan (version) { if (typeof version === 'string') { return Versions[version] < this.options.protocolVersion @@ -132,7 +141,7 @@ class Connection extends EventEmitter { this.outLog('Enc buf', buf) const packet = Buffer.concat([Buffer.from([0xfe]), buf]) // add header - this.outLog('Sending wrapped encrypted batch', packet) + // this.outLog('Sending wrapped encrypted batch', packet) this.sendMCPE(packet) } From 32d52e987852fdf139c6d351b88223845f212fa3 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 3 Apr 2021 22:54:42 -0400 Subject: [PATCH 14/21] viewer: refactor and cleanup --- examples/viewer/client/BotProvider.js | 316 ++--------------------- examples/viewer/client/BotViewer.js | 11 +- examples/viewer/client/Chunk.js | 40 +-- examples/viewer/client/ClientProvider.js | 68 +++++ examples/viewer/client/ProxyProvider.js | 62 +---- examples/viewer/client/movements.js | 244 +++++++++++++++++ examples/viewer/client/util.js | 16 ++ src/connection.js | 4 +- tools/startVanillaServer.js | 2 +- 9 files changed, 366 insertions(+), 397 deletions(-) create mode 100644 examples/viewer/client/ClientProvider.js create mode 100644 examples/viewer/client/movements.js create mode 100644 examples/viewer/client/util.js diff --git a/examples/viewer/client/BotProvider.js b/examples/viewer/client/BotProvider.js index f4d0d37..fe225cf 100644 --- a/examples/viewer/client/BotProvider.js +++ b/examples/viewer/client/BotProvider.js @@ -1,15 +1,9 @@ /* eslint-disable */ -const { Client } = require('bedrock-protocol') const { Version } = require('bedrock-provider') const { WorldView } = require('prismarine-viewer/viewer') -const vec3 = require('vec3') const World = require('prismarine-world')() const ChunkColumn = require('./Chunk')() -const { Physics, PlayerState } = require('prismarine-physics') -const { performance } = require('perf_hooks') - -const PHYSICS_INTERVAL_MS = 50 -const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000 +const { MovementManager } = require('./movements') class BotProvider extends WorldView { chunks = {} @@ -21,72 +15,7 @@ class BotProvider extends WorldView { this.connect() this.listenToBot() this.world = new World() - - // Server auth movement : we send inputs, server calculates position & sends back - this.serverMovements = true - this.tick = 0n - } - - connect() { - const client = new Client({ hostname: '127.0.0.1', version: '1.16.210', port: 19132, connectTimeout: 100000 }) - - client.once('resource_packs_info', (packet) => { - client.write('resource_pack_client_response', { - response_status: 'completed', - resourcepackids: [] - }) - - client.once('resource_pack_stack', (stack) => { - client.write('resource_pack_client_response', { - response_status: 'completed', - 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 }) - }) - - this.client = client - } - - close() { - this.client?.close() - } - - listenToBot() { - this.client.on('connect', () => { - console.log('Bot has connected!') - }) - this.client.on('start_game', packet => { - this.updatePosition(packet.player_position) - }) - - this.client.on('spawn', () => { - // server allows client to render chunks & spawn in world - this.emit('spawn', { position: this.lastPos }) - - this.tickLoop = setInterval(() => { - this.client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) - }) - }) - - this.client.on('level_chunk', packet => { - this.handleChunk(packet) - }) - - this.client.on('move_player', packet => { - if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) - }) - - this.client.on('set_entity_motion', packet => { - if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) - }) - - this.client.on('tick_sync', (packet) => { - this.lastTick = packet.response_time - }) + this.movements = new MovementManager(this) } handleChunk(packet, render = true) { @@ -102,232 +31,27 @@ class BotProvider extends WorldView { }) } + updatePlayerCamera(id, pos, yaw, pitch, updateState) { + // TODO: do this properly + window.viewer.viewer.entities.update({ + name: 'player', + id, + pos, + width: 0.6, + height: 1.8, + yaw, + pitch + }) + + if (updateState) { + this.movements.updatePosition(pos, yaw, pitch) + } + } + stopBot() { clearInterval(this.tickLoop) - } - - // Server gives us a new position - updatePosition(pos) { - this.lastPos ??= vec3(pos) - super.updatePosition(this.lastPos) - } - - // Ask the server to be in a new position - requestPosition(time, inputState) { - const positionUpdated = !this.lastSentPos || !this.lastPos.equals(this.lastSentPos) - // if (globalThis.logging)console.log('New pos', this.lastSentPos,this.lastPos) - - if (positionUpdated) { - this.lastSentPos = this.lastPos.clone() - console.log('We computed', this.lastPos) - this.pushCamera({ - position: this.lastSentPos, - input_data: {}, - yaw: this.playerState.yaw, pitch: this.playerState.pitch - }, 2) - return - this.client.queue('player_auth_input', { - pitch: this.player.pitch, - yaw: this.player.yaw, - position: { - x: this.lastPos.x, - y: this.lastPos.y, - z: this.lastPos.z - }, - move_vector: { // Minecraft coords, N: Z+1, S: Z-1, W: X+1, E: X-1 - x: inputState.left ? 1 : (inputState.right ? -1 : 0), - z: inputState.up ? 1 : (inputState.down ? -1 : 0) - }, - head_yaw: this.player.headYaw, - input_data: inputState, - input_mode: 'mouse', - play_mode: 'screen', - tick: this.tick, - delta: this.lastSentPos?.minus(this.lastPos) ?? { x: 0, y: 0, z: 0 } - }) - this.positionUpdated = false - this.lastSentPos = this.lastPos.clone() - } - } - - initPhys(position, velocity, yaw = 0, pitch = 0, headYaw = 0) { - this.lastPos = position ? vec3(position) : vec3(0, 0, 0) - this.lastVel = velocity ? vec3(velocity) : vec3(0, 0, 0) - this.player = { - version: '1.16.1', - inventory: { - slots: [] - }, - entity: { - effects: {}, - position: this.lastPos, - velocity: this.lastVel, - onGround: false, - isInWater: false, - isInLava: false, - isInWeb: false, - isCollidedHorizontally: false, - isCollidedVertically: false, - yaw, - pitch, - headYaw // bedrock - }, - events: { // Control events to send next tick - startSprint: false, - stopSprint: false, - startSneak: false, - stopSneak: false - }, - jumpTicks: 0, - jumpQueued: false, - downJump: false - } - - const mcData = require('minecraft-data')('1.16.1') - this.physics = Physics(mcData, this.world) - this.controls = { - forward: false, - back: false, - left: false, - right: false, - jump: false, - sprint: false, - sneak: false - } - this.playerState = new PlayerState(this.player, this.controls) - } - - // This function should be executed each tick (every 0.05 seconds) - // How it works: https://gafferongames.com/post/fix_your_timestep/ - timeAccumulator = 0 - lastPhysicsFrameTime = null - inputQueue = [] - doPhysics() { - const now = performance.now() - const deltaSeconds = (now - this.lastPhysicsFrameTime) / 1000 - this.lastPhysicsFrameTime = now - - this.timeAccumulator += deltaSeconds - - while (this.timeAccumulator >= PHYSICS_TIMESTEP) { - let q = this.inputQueue.shift() - if (q) { - Object.assign(this.playerState.control, q) - if (q.yaw) { this.player.entity.yaw = q.yaw; this.playerState.yaw = q.yaw; } - if (q.pitch) this.player.entity.pitch = q.pitch - } - this.physics.simulatePlayer(this.playerState, this.world.sync).apply(this.player) - this.lastPos = this.playerState.pos - this.requestPosition(PHYSICS_TIMESTEP, { - ascend: false, - descend: false, - // Players bob up and down in water, north jump is true when going up. - // In water this is only true after the player has reached max height before bobbing back down. - north_jump: this.player.jumpTicks > 0, // Jump - jump_down: this.controls.jump, // Jump - sprint_down: this.controls.sprint, - change_height: false, - jumping: this.controls.jump, // Jump - auto_jumping_in_water: false, - sneaking: false, - sneak_down: false, - up: this.controls.forward, - down: this.controls.back, - left: this.controls.left, - right: this.controls.right, - up_left: false, - up_right: false, - want_up: this.controls.jump, // Jump - want_down: false, - want_down_slow: false, - want_up_slow: false, - sprinting: false, - ascend_scaffolding: false, - descend_scaffolding: false, - sneak_toggle_down: false, - persist_sneak: false, - start_sprinting: this.player.events.startSprint || false, - stop_sprinting: this.player.events.stopSprint || false, - start_sneaking: this.player.events.startSneak || false, - stop_sneaking: this.player.events.stopSneak || false, - // Player is Update Aqatic swimming - start_swimming: false, - // Player stops Update Aqatic swimming - stop_swimming: false, - start_jumping: this.player.jumpTicks === 1, // Jump - start_gliding: false, - stop_gliding: false, - }) - this.timeAccumulator -= PHYSICS_TIMESTEP - } - } - - startPhys() { - console.log('Start phys') - this.physicsLoop = setInterval(() => { - this.doPhysics() - }, PHYSICS_INTERVAL_MS) - } - - setControlState(control, state) { - if (this.controls[control] === state) return - if (control === 'sprint') { - this.player.events.startSprint = state - this.player.events.stopSprint = !state - this.controls.sprint = true - } else if (control === 'sneak') { - this.player.events.startSneak = state - this.player.events.stopSneak = !state - this.controls.sprint = true - } - } - - pushInputState(state, yaw, pitch) { - const yawRad = d2r(yaw) - const pitchRad = d2r(pitch) - this.inputQueue.push({ - forward: state.up, - back: state.down,// TODO: left and right switched ??? - left: state.right, - right: state.left, - jump: state.jump_down, - sneak: state.sprint_down, - yaw: yawRad, pitch: pitchRad, - }) - globalThis.yaw = [yaw, yawRad] - if (global.logYaw) console.log('Pushed', yaw, pitch) - } - - pushCamera(state, id = 1) { - let { x, y, z } = state.position - if (id == 1) y -= 1.62 // account for player bb - const pos = vec3({ x, y, z }) - if (state.position) { - viewer.viewer.entities.update({ - name: 'player', - id, pos, width: 0.6, height: 1.8, - yaw: id == 1 ? d2r(state.yaw) : state.yaw - }) - - //viewer.viewer.camera.position.set(x, y, z) - } - - if (state.input_data.sneak_down) { - this.player.entity.position = pos - this.playerState.pos = this.player.entity.position - } - } - - onCameraMovement(newYaw, newPitch, newHeadYaw) { - this.player.yaw = newYaw - this.player.pitch = newPitch - this.player.headYaw = newHeadYaw - } - - stopPhys() { - clearInterval(this.physicsLoop) + this.movements.stopPhys() } } -const d2r = deg => (180 - (deg < 0 ? (360 + deg) : deg)) * (Math.PI / 180) module.exports = { BotProvider } diff --git a/examples/viewer/client/BotViewer.js b/examples/viewer/client/BotViewer.js index 7600b0a..a0987e7 100644 --- a/examples/viewer/client/BotViewer.js +++ b/examples/viewer/client/BotViewer.js @@ -1,21 +1,16 @@ /* global THREE */ const { Viewer, MapControls } = require('prismarine-viewer/viewer') -const { Vec3 } = require('vec3') -const { BotProvider } = require('./BotProvider') +// const { Vec3 } = require('vec3') +// const { BotProvider } = require('./BotProvider') const { ProxyProvider } = require('./ProxyProvider') global.THREE = require('three') const MCVER = '1.16.1' class BotViewer { - constructor () { - - } - start () { // this.bot = new BotProvider() this.bot = new ProxyProvider() - // return // Create three.js context, add to page this.renderer = new THREE.WebGLRenderer() this.renderer.setPixelRatio(window.devicePixelRatio || 1) @@ -61,8 +56,6 @@ class BotViewer { onKeyDown = (evt) => { console.log('Key down', evt) - // this.bot.initPhys() - // this.bot.startPhys() } registerBrowserEvents () { diff --git a/examples/viewer/client/Chunk.js b/examples/viewer/client/Chunk.js index b2ecc57..7733cc3 100644 --- a/examples/viewer/client/Chunk.js +++ b/examples/viewer/client/Chunk.js @@ -1,6 +1,4 @@ -const { ChunkColumn, Version } = require('bedrock-provider') -const { SubChunk } = require('bedrock-provider/js/SubChunk') -try { const v8 = require('v8') } catch { } +const { ChunkColumn } = require('bedrock-provider') const Block = require('prismarine-block')('1.16.1') @@ -13,42 +11,6 @@ class ChunkColumnWrapped extends ChunkColumn { // pchunk compatiblity wrapper getBlockStateId (pos) { return super.getBlock(pos.x, pos.y, pos.z)?.stateId } - - // // Serialization - // serialize() { - // if (typeof v8 === 'undefined') { - // return JSON.stringify(this) - // } else { - // const copy = { ...this, sections: [] } - // for (const section of this.sections) { - // copy.sections.push(v8.serialize(section)) - // } - // return v8.serialize(copy) - // } - // } - - // toJson() { return this.serialize() } - - // static deserialize(obj) { - // if (typeof obj === 'string') { - // Oject.assign(this, JSON.parse(obj)) - // } else { // Buffer - // const chunk = new ChunkColumnWrapped() - // const des = v8.deserialize(obj) - // Object.assign(chunk, des) - // chunk.sections = [] - // for (const section of des.sections) { - // const s = new SubChunk() - // chunk.sections.push(Object.assign(s, v8.deserialize(section))) - // } - // // console.log('Des',obj,chunk) - // return chunk - // } - // } - - // static fromJson(obj) { - // return ChunkColumnWrapped.deserialize(obj) - // } } module.exports = (version) => { diff --git a/examples/viewer/client/ClientProvider.js b/examples/viewer/client/ClientProvider.js new file mode 100644 index 0000000..8c13da8 --- /dev/null +++ b/examples/viewer/client/ClientProvider.js @@ -0,0 +1,68 @@ +const { Client } = require('bedrock-protocol') +const { BotProvider } = require('./BotProvider') + +class ClientProvider extends BotProvider { + connect () { + const client = new Client({ hostname: '127.0.0.1', version: '1.16.210', port: 19132, connectTimeout: 100000 }) + + client.once('resource_packs_info', (packet) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + resourcepackids: [] + }) + + client.once('resource_pack_stack', (stack) => { + client.write('resource_pack_client_response', { + response_status: 'completed', + 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 }) + }) + + this.client = client + } + + close () { + this.client?.close() + } + + listenToBot () { + this.client.on('connect', () => { + console.log('Bot has connected!') + }) + this.client.on('start_game', packet => { + this.updatePosition(packet.player_position) + }) + + this.client.on('spawn', () => { + // server allows client to render chunks & spawn in world + this.emit('spawn', { position: this.lastPos }) + + this.tickLoop = setInterval(() => { + this.client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + }) + }) + + this.client.on('level_chunk', packet => { + this.handleChunk(packet) + }) + + this.client.on('move_player', packet => { + if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) + }) + + this.client.on('set_entity_motion', packet => { + if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) + }) + + this.client.on('tick_sync', (packet) => { + this.lastTick = packet.response_time + }) + } +} + +module.exports = { ClientProvider } diff --git a/examples/viewer/client/ProxyProvider.js b/examples/viewer/client/ProxyProvider.js index 0db00df..305b4a4 100644 --- a/examples/viewer/client/ProxyProvider.js +++ b/examples/viewer/client/ProxyProvider.js @@ -1,6 +1,6 @@ const { Relay } = require('bedrock-protocol') const { BotProvider } = require('./BotProvider') -const vec3 = require('vec3') +const { diff } = require('./util') class ProxyProvider extends BotProvider { lastPlayerMovePacket @@ -15,61 +15,36 @@ class ProxyProvider extends BotProvider { port: 19132 } }) - proxy.listen() - console.info('Waiting for connect') - const maxChunks = 40 - proxy.on('join', (client, server) => { client.on('clientbound', ({ name, params }) => { - if (name == 'level_chunk') { - // maxChunks-- - // if (maxChunks >= 0) { - // this.handleChunk(params) - // } + if (name === 'level_chunk') { this.handleChunk(params, true) - } else if (name == 'start_game') { - this.initPhys(params.player_position, null, params.rotation.z, params.rotation.x, 0) + } else if (name === 'start_game') { + this.movements.init('', params.player_position, null, params.rotation.z, params.rotation.x, 0) } else if (name === 'play_status') { - // this.emit('spawn', { position: server.startGameData.player_position }) - - this.startPhys() + this.movements.startPhys() console.info('Started physics!') } else if (name === 'move_player') { - console.log('move_player', packet) - // if (packet.runtime_id === server.entityId) { - // this.updatePosition(packet.position) - // if (this.lastServerMovement.x == packet.position.x && this.lastServerMovement.y == packet.position.y && this.lastServerMovement.z == packet.position.z) { - - // } else { - // console.log('Server computed', packet.position) - // } - // this.lastServerMovement = { ...packet.position } - // } + console.log('move_player', params) + this.movements.updatePosition(params.position, params.yaw, params.pitch, params.head_yaw, params.tick) } + if (name.includes('entity') || name.includes('network_chunk_publisher_update') || name.includes('tick') || name.includes('level')) return console.log('CB', name) }) client.on('serverbound', ({ name, params }) => { // { name, params } - if (name == 'player_auth_input') { - // console.log('player_auth_input', this.lastPlayerMovePacket, params) - - // this.controls.forward = params.input_data.up - // this.controls.back = params.input_data.down - // this.controls.left = params.input_data.left - // this.controls.right = params.input_data.right - // this.player.entity.pitch = params.pitch - // this.player.entity.yaw = params.yaw - this.pushInputState(params.input_data, params.yaw, params.pitch) - this.pushCamera(params) - this.lastMovePacket = params + if (name === 'player_auth_input') { + this.movements.pushInputState(params.input_data, params.yaw, params.pitch) + this.movements.pushCameraControl(params, 1) // Log Movement deltas { + this.lastMovePacket = params if (this.firstPlayerMovePacket) { const id = diff(this.firstPlayerMovePacket.input_data, params.input_data) const md = diff(this.firstPlayerMovePacket.move_vector, params.move_vector) @@ -109,18 +84,5 @@ class ProxyProvider extends BotProvider { } } -const difference = (o1, o2) => Object.keys(o2).reduce((diff, key) => { - if (o1[key] === o2[key]) return diff - return { - ...diff, - [key]: o2[key] - } -}, {}) - -// console.log = () => {} -// console.debug = () => {} - -const diff = (o1, o2) => { const dif = difference(o1, o2); return Object.keys(dif).length ? dif : null } - module.exports = { ProxyProvider } globalThis.logging = true diff --git a/examples/viewer/client/movements.js b/examples/viewer/client/movements.js new file mode 100644 index 0000000..a405722 --- /dev/null +++ b/examples/viewer/client/movements.js @@ -0,0 +1,244 @@ +const { Physics, PlayerState } = require('prismarine-physics') +const { performance } = require('perf_hooks') +const { d2r } = require('./util') +const vec3 = require('vec3') + +const PHYSICS_INTERVAL_MS = 50 +const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000 + +class MovementManager { + // Server auth movement : we send inputs, server calculates position & sends back + serverMovements = false + + constructor (bot) { + this.bot = bot + this.world = bot.world + // Physics tick + this.tick = 0n + } + + get lastPos () { return this.player.entity.position.clone() } + set lastPos (newPos) { this.player.entity.position.set(newPos.x, newPos.y, newPos.z) } + + get lastRot () { return vec3(this.player.entity.yaw, this.player.entity.pitch, this.player.entity.headYaw) } + + set lastRot (rot) { + this.player.entity.yaw = rot.x + this.player.entity.pitch = rot.y + if (rot.z) this.player.entity.headYaw = rot.z + } + + // Ask the server to be in a new position + requestPosition (time, inputState) { + const positionUpdated = !this.lastSentPos || !this.lastPos.equals(this.lastSentPos) + const rotationUpdated = !this.lastSentRot || !this.lastRot.equals(this.lastSentRot) + + if (positionUpdated) { + this.lastSentPos = this.lastPos.clone() + console.log('We computed', this.lastPos) + this.bot.updatePlayerCamera(2, this.lastSentPos, this.playerState.yaw, this.playerState.pitch) + if (this.serverMovements) { + this.client.queue('player_auth_input', { + pitch: this.player.pitch, + yaw: this.player.yaw, + position: { + x: this.lastPos.x, + y: this.lastPos.y, + z: this.lastPos.z + }, + move_vector: { // Minecraft coords, N: Z+1, S: Z-1, W: X+1, E: X-1 + x: inputState.left ? 1 : (inputState.right ? -1 : 0), + z: inputState.up ? 1 : (inputState.down ? -1 : 0) + }, + head_yaw: this.player.headYaw, + input_data: inputState, + input_mode: 'mouse', + play_mode: 'screen', + tick: this.tick, + delta: this.lastSentPos?.minus(this.lastPos) ?? { x: 0, y: 0, z: 0 } + }) + this.positionUpdated = false + } + + this.lastSentPos = this.lastPos + this.lastSentRot = this.lastRot + } + } + + init (movementAuthority, position, velocity, yaw = 0, pitch = 0, headYaw = 0) { + if (movementAuthority.includes('server')) { + this.serverMovements = true + } + this.player = { + version: '1.16.1', + inventory: { + slots: [] + }, + entity: { + effects: {}, + position: vec3(position), + velocity: vec3(velocity), + onGround: false, + isInWater: false, + isInLava: false, + isInWeb: false, + isCollidedHorizontally: false, + isCollidedVertically: false, + yaw, + pitch, + headYaw // bedrock + }, + events: { // Control events to send next tick + startSprint: false, + stopSprint: false, + startSneak: false, + stopSneak: false + }, + jumpTicks: 0, + jumpQueued: false, + downJump: false + } + + const mcData = require('minecraft-data')('1.16.1') + this.physics = Physics(mcData, this.world) + this.controls = { + forward: false, + back: false, + left: false, + right: false, + jump: false, + sprint: false, + sneak: false + } + } + + // This function should be executed each tick (every 0.05 seconds) + // How it works: https://gafferongames.com/post/fix_your_timestep/ + timeAccumulator = 0 + lastPhysicsFrameTime = null + inputQueue = [] + doPhysics () { + const now = performance.now() + const deltaSeconds = (now - this.lastPhysicsFrameTime) / 1000 + this.lastPhysicsFrameTime = now + + this.timeAccumulator += deltaSeconds + + while (this.timeAccumulator >= PHYSICS_TIMESTEP) { + const q = this.inputQueue.shift() + if (q) { + Object.assign(this.playerState.control, q) + if (q.yaw) this.player.entity.yaw = q.yaw + if (q.pitch) this.player.entity.pitch = q.pitch + } + this.playerState = new PlayerState(this.player, this.controls) + this.physics.simulatePlayer(this.playerState, this.world.sync).apply(this.player) + this.lastPos = this.playerState.pos + this.requestPosition(PHYSICS_TIMESTEP, { + ascend: false, + descend: false, + // Players bob up and down in water, north jump is true when going up. + // In water this is only true after the player has reached max height before bobbing back down. + north_jump: this.player.jumpTicks > 0, // Jump + jump_down: this.controls.jump, // Jump + sprint_down: this.controls.sprint, + change_height: false, + jumping: this.controls.jump, // Jump + auto_jumping_in_water: false, + sneaking: false, + sneak_down: false, + up: this.controls.forward, + down: this.controls.back, + left: this.controls.left, + right: this.controls.right, + up_left: false, + up_right: false, + want_up: this.controls.jump, // Jump + want_down: false, + want_down_slow: false, + want_up_slow: false, + sprinting: false, + ascend_scaffolding: false, + descend_scaffolding: false, + sneak_toggle_down: false, + persist_sneak: false, + start_sprinting: this.player.events.startSprint || false, + stop_sprinting: this.player.events.stopSprint || false, + start_sneaking: this.player.events.startSneak || false, + stop_sneaking: this.player.events.stopSneak || false, + // Player is Update Aqatic swimming + start_swimming: false, + // Player stops Update Aqatic swimming + stop_swimming: false, + start_jumping: this.player.jumpTicks === 1, // Jump + start_gliding: false, + stop_gliding: false + }) + this.timeAccumulator -= PHYSICS_TIMESTEP + } + } + + startPhys () { + console.log('Start phys') + this.physicsLoop = setInterval(() => { + this.doPhysics() + }, PHYSICS_INTERVAL_MS) + } + + setControlState (control, state) { + if (this.controls[control] === state) return + if (control === 'sprint') { + this.player.events.startSprint = state + this.player.events.stopSprint = !state + this.controls.sprint = true + } else if (control === 'sneak') { + this.player.events.startSneak = state + this.player.events.stopSneak = !state + this.controls.sprint = true + } + } + + stopPhys () { + clearInterval(this.physicsLoop) + } + + pushInputState (state, yaw, pitch) { + const yawRad = d2r(yaw) + const pitchRad = d2r(pitch) + this.inputQueue.push({ + forward: state.up, + back: state.down, // TODO: left and right switched ??? + left: state.right, + right: state.left, + jump: state.jump_down, + sneak: state.sneak_down, + yaw: yawRad, + pitch: pitchRad + }) + // debug + globalThis.debugYaw = [yaw, yawRad] + } + + pushCameraControl (state, id = 1) { + let { x, y, z } = state.position + if (id === 1) y -= 1.62 // account for player bb + const adjPos = vec3({ x, y, z }) + // Sneak resyncs the position for easy testing + this.bot.updatePlayerCamera(id, adjPos, d2r(state.yaw), d2r(state.pitch), state.input_data.sneak_down) + } + + // Server gives us a new position + updatePosition (pos, yaw, pitch, headYaw, tick) { + this.lastPos = pos + this.lastRot = { x: yaw, y: pitch, z: headYaw } + if (tick) this.tick = tick + } + + onViewerCameraMove (newYaw, newPitch, newHeadYaw) { + this.player.yaw = newYaw + this.player.pitch = newPitch + this.player.headYaw = newHeadYaw + } +} + +module.exports = { MovementManager } diff --git a/examples/viewer/client/util.js b/examples/viewer/client/util.js new file mode 100644 index 0000000..a22a646 --- /dev/null +++ b/examples/viewer/client/util.js @@ -0,0 +1,16 @@ +const difference = (o1, o2) => Object.keys(o2).reduce((diff, key) => { + if (o1[key] === o2[key]) return diff + return { + ...diff, + [key]: o2[key] + } +}, {}) + +const diff = (o1, o2) => { const dif = difference(o1, o2); return Object.keys(dif).length ? dif : null } + +const d2r = deg => (180 - (deg < 0 ? (360 + deg) : deg)) * (Math.PI / 180) + +module.exports = { + diff, + d2r +} diff --git a/src/connection.js b/src/connection.js index 6a55a1a..f4476bc 100644 --- a/src/connection.js +++ b/src/connection.js @@ -24,7 +24,7 @@ class Connection extends EventEmitter { } set status (val) { - this.inLog('* new status', val) + debug('* new status', val) this.#status = val } @@ -126,7 +126,7 @@ class Connection extends EventEmitter { sendEncryptedBatch (batch) { const buf = batch.stream.getBuffer() - debug('Sending encrypted batch', batch) + // debug('Sending encrypted batch', batch) this.encrypt(buf) } diff --git a/tools/startVanillaServer.js b/tools/startVanillaServer.js index 07587ae..a409fca 100644 --- a/tools/startVanillaServer.js +++ b/tools/startVanillaServer.js @@ -104,7 +104,7 @@ async function startServerAndWait (version, withTimeout, options) { if (!module.parent) { // if (process.argv.length < 3) throw Error('Missing version argument') - startServer(process.argv[2] || '1.16.201') + startServer(process.argv[2] || '1.16.201', null, process.argv[3] ? { 'server-port': process.argv[3] } : undefined) } module.exports = { fetchLatestStable, startServer, startServerAndWait } From 7a830317eaf6bd6d7d1f27d4ebcb2d3d8148c3d9 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sun, 4 Apr 2021 03:52:06 -0400 Subject: [PATCH 15/21] viewer: first person views --- examples/viewer/client/BotProvider.js | 22 +++++------------- examples/viewer/client/BotViewer.js | 30 +++++++++++++++++++++++-- examples/viewer/client/ProxyProvider.js | 1 + 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/examples/viewer/client/BotProvider.js b/examples/viewer/client/BotProvider.js index fe225cf..4549959 100644 --- a/examples/viewer/client/BotProvider.js +++ b/examples/viewer/client/BotProvider.js @@ -1,4 +1,3 @@ -/* eslint-disable */ const { Version } = require('bedrock-provider') const { WorldView } = require('prismarine-viewer/viewer') const World = require('prismarine-world')() @@ -10,7 +9,7 @@ class BotProvider extends WorldView { lastSentPos positionUpdated = true - constructor() { + constructor () { super() this.connect() this.listenToBot() @@ -18,7 +17,7 @@ class BotProvider extends WorldView { this.movements = new MovementManager(this) } - handleChunk(packet, render = true) { + handleChunk (packet, render = true) { const hash = (packet.x << 4) + ',' + (packet.z << 4) if (this.loadChunk[hash]) return const cc = new ChunkColumn(Version.v1_4_0, packet.x, packet.z) @@ -31,24 +30,15 @@ class BotProvider extends WorldView { }) } - updatePlayerCamera(id, pos, yaw, pitch, updateState) { - // TODO: do this properly - window.viewer.viewer.entities.update({ - name: 'player', - id, - pos, - width: 0.6, - height: 1.8, - yaw, - pitch - }) + updatePlayerCamera (id, position, yaw, pitch, updateState) { + this.emit('playerMove', id, { position, yaw, pitch }) if (updateState) { - this.movements.updatePosition(pos, yaw, pitch) + this.movements.updatePosition(position, yaw, pitch) } } - stopBot() { + stopBot () { clearInterval(this.tickLoop) this.movements.stopPhys() } diff --git a/examples/viewer/client/BotViewer.js b/examples/viewer/client/BotViewer.js index a0987e7..a29cc6f 100644 --- a/examples/viewer/client/BotViewer.js +++ b/examples/viewer/client/BotViewer.js @@ -29,11 +29,33 @@ class BotViewer { // Link WorldView and Viewer this.viewer.listen(this.bot) - this.bot.on('spawn', ({ position }) => { + this.bot.on('spawn', ({ position, firstPerson }) => { // Initialize viewer, load chunks this.bot.init(position) // Start listening for keys this.registerBrowserEvents() + + if (firstPerson && this.bot.movements) { + this.firstPerson = true + } else { + this.viewer.camera.position.set(position.x, position.y, position.z) + } + }) + + this.bot.on('playerMove', (id, pos) => { + if (this.firstPerson && id === 1) { + this.setFirstPersonCamera(pos) + } + + window.viewer.viewer.entities.update({ + name: 'player', + id, + pos: pos.position, + width: 0.6, + height: 1.8, + yaw: pos.yaw, + pitch: pos.pitch + }) }) this.controls.update() @@ -41,7 +63,7 @@ class BotViewer { // Browser animation loop const animate = () => { window.requestAnimationFrame(animate) - if (this.controls) this.controls.update() + if (this.controls && !this.firstPerson) this.controls.update() this.viewer.update() this.renderer.render(this.viewer.scene, this.viewer.camera) } @@ -61,6 +83,10 @@ class BotViewer { registerBrowserEvents () { this.renderer.domElement.parentElement.addEventListener('keydown', this.onKeyDown) } + + setFirstPersonCamera (entity) { + this.viewer.setFirstPersonCamera(entity.position, entity.yaw, entity.pitch * 2) + } } module.exports = { BotViewer } diff --git a/examples/viewer/client/ProxyProvider.js b/examples/viewer/client/ProxyProvider.js index 305b4a4..de97ee6 100644 --- a/examples/viewer/client/ProxyProvider.js +++ b/examples/viewer/client/ProxyProvider.js @@ -26,6 +26,7 @@ class ProxyProvider extends BotProvider { this.movements.init('', params.player_position, null, params.rotation.z, params.rotation.x, 0) } else if (name === 'play_status') { this.movements.startPhys() + this.emit('spawn', { position: this.movements.lastPos, firstPerson: true }) console.info('Started physics!') } else if (name === 'move_player') { console.log('move_player', params) From 01f19164bab4200896eeb5d8889d5b407f8e921e Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 7 Apr 2021 01:56:32 -0400 Subject: [PATCH 16/21] viewer: key control movement --- examples/viewer/client/BotProvider.js | 11 +++++ examples/viewer/client/BotViewer.js | 55 +++++++++++++++++++++--- examples/viewer/client/ClientProvider.js | 52 ++++++++++++++++++++-- examples/viewer/client/movements.js | 22 +++++++--- examples/viewer/index.js | 6 ++- 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/examples/viewer/client/BotProvider.js b/examples/viewer/client/BotProvider.js index 4549959..4d23377 100644 --- a/examples/viewer/client/BotProvider.js +++ b/examples/viewer/client/BotProvider.js @@ -15,8 +15,19 @@ class BotProvider extends WorldView { this.listenToBot() this.world = new World() this.movements = new MovementManager(this) + + this.onKeyDown = () => {} + this.onKeyUp = () => {} + + this.removeAllListeners('mouseClick') } + raycast () { + // TODO : fix + } + + get entity () { return this.movements.player.entity } + handleChunk (packet, render = true) { const hash = (packet.x << 4) + ',' + (packet.z << 4) if (this.loadChunk[hash]) return diff --git a/examples/viewer/client/BotViewer.js b/examples/viewer/client/BotViewer.js index a29cc6f..384008e 100644 --- a/examples/viewer/client/BotViewer.js +++ b/examples/viewer/client/BotViewer.js @@ -1,7 +1,7 @@ /* global THREE */ const { Viewer, MapControls } = require('prismarine-viewer/viewer') // const { Vec3 } = require('vec3') -// const { BotProvider } = require('./BotProvider') +const { ClientProvider } = require('./ClientProvider') const { ProxyProvider } = require('./ProxyProvider') global.THREE = require('three') @@ -9,8 +9,8 @@ const MCVER = '1.16.1' class BotViewer { start () { - // this.bot = new BotProvider() - this.bot = new ProxyProvider() + this.bot = new ClientProvider() + // this.bot = new ProxyProvider() // Create three.js context, add to page this.renderer = new THREE.WebGLRenderer() this.renderer.setPixelRatio(window.devicePixelRatio || 1) @@ -36,15 +36,18 @@ class BotViewer { this.registerBrowserEvents() if (firstPerson && this.bot.movements) { + this.viewer.camera.position.set(position.x, position.y, position.z) this.firstPerson = true + this.controls.enabled = false } else { this.viewer.camera.position.set(position.x, position.y, position.z) } }) this.bot.on('playerMove', (id, pos) => { - if (this.firstPerson && id === 1) { + if (this.firstPerson && id < 10) { this.setFirstPersonCamera(pos) + return } window.viewer.viewer.entities.update({ @@ -58,6 +61,13 @@ class BotViewer { }) }) + this.bot.on('startSprint', () => { + this.viewer.camera.fov += 20 + }) + this.bot.on('stopSprint', () => { + this.viewer.camera.fov -= 20 + }) + this.controls.update() // Browser animation loop @@ -76,12 +86,43 @@ class BotViewer { }) } - onKeyDown = (evt) => { - console.log('Key down', evt) + onMouseMove = (e) => { + if (this.firstPerson) { + this.bot.entity.pitch -= e.movementY * 0.005 + this.bot.entity.yaw -= e.movementX * 0.004 + } + } + + onPointerLockChange = () => { + const e = this.renderer.domElement + if (document.pointerLockElement === e) { + e.parentElement.addEventListener('mousemove', this.onMouseMove, { passive: true }) + } else { + e.parentElement.removeEventListener('mousemove', this.onMouseMove, false) + } + } + + onMouseDown = () => { + if (this.firstPerson && !document.pointerLockElement) { + this.renderer.domElement.requestPointerLock() + } } registerBrowserEvents () { - this.renderer.domElement.parentElement.addEventListener('keydown', this.onKeyDown) + const e = this.renderer.domElement + e.parentElement.addEventListener('keydown', this.bot.onKeyDown) + e.parentElement.addEventListener('keyup', this.bot.onKeyUp) + e.parentElement.addEventListener('mousedown', this.onMouseDown) + document.addEventListener('pointerlockchange', this.onPointerLockChange, false) + } + + unregisterBrowserEvents () { + const e = this.renderer.domElement + e.parentElement.removeEventListener('keydown', this.bot.onKeyDown) + e.parentElement.removeEventListener('keyup', this.bot.onKeyUp) + e.parentElement.removeEventListener('mousemove', this.onMouseMove) + e.parentElement.removeEventListener('mousedown', this.onMouseDown) + document.removeEventListener('pointerlockchange', this.onPointerLockChange, false) } setFirstPersonCamera (entity) { diff --git a/examples/viewer/client/ClientProvider.js b/examples/viewer/client/ClientProvider.js index 8c13da8..62aae74 100644 --- a/examples/viewer/client/ClientProvider.js +++ b/examples/viewer/client/ClientProvider.js @@ -1,7 +1,18 @@ const { Client } = require('bedrock-protocol') const { BotProvider } = require('./BotProvider') +const controlMap = { + forward: ['KeyW', 'KeyZ'], + back: 'KeyS', + left: ['KeyA', 'KeyQ'], + right: 'KeyD', + sneak: 'ShiftLeft', + jump: 'Space' +} + class ClientProvider extends BotProvider { + downKeys = new Set() + connect () { const client = new Client({ hostname: '127.0.0.1', version: '1.16.210', port: 19132, connectTimeout: 100000 }) @@ -20,7 +31,10 @@ class ClientProvider extends BotProvider { 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 }) + + this.heartbeat = setInterval(() => { + client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) + }) }) this.client = client @@ -36,11 +50,13 @@ class ClientProvider extends BotProvider { }) this.client.on('start_game', packet => { this.updatePosition(packet.player_position) + this.movements.init('', packet.player_position, null, packet.rotation.z, packet.rotation.x, 0) }) this.client.on('spawn', () => { + this.movements.startPhys() // server allows client to render chunks & spawn in world - this.emit('spawn', { position: this.lastPos }) + this.emit('spawn', { position: this.lastPos, firstPerson: true }) this.tickLoop = setInterval(() => { this.client.queue('tick_sync', { request_time: BigInt(Date.now()), response_time: 0n }) @@ -52,17 +68,45 @@ class ClientProvider extends BotProvider { }) this.client.on('move_player', packet => { - if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) + if (packet.runtime_id === this.client.entityId) { this.movements.updatePosition(packet.position, packet.yaw, packet.pitch, packet.head_yaw, packet.tick) } }) this.client.on('set_entity_motion', packet => { - if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) + // if (packet.runtime_id === this.client.entityId) this.updatePosition(packet.position) }) this.client.on('tick_sync', (packet) => { this.lastTick = packet.response_time }) } + + onKeyDown = (evt) => { + const code = evt.code + for (const control in controlMap) { + if (controlMap[control].includes(code)) { + this.movements.setControlState(control, true) + break + } + if (evt.ctrlKey) { + this.movements.setControlState('sprint', true) + } + } + this.downKeys.add(code) + } + + onKeyUp = (evt) => { + const code = evt.code + if (code == 'ControlLeft' && this.downKeys.has('ControlLeft')) { + this.movements.setControlState('sprint', false) + } + for (const control in controlMap) { + if (controlMap[control].includes(code)) { + this.movements.setControlState(control, false) + break + } + } + this.downKeys.delete(code) + } } module.exports = { ClientProvider } diff --git a/examples/viewer/client/movements.js b/examples/viewer/client/movements.js index a405722..d5f52c0 100644 --- a/examples/viewer/client/movements.js +++ b/examples/viewer/client/movements.js @@ -19,9 +19,7 @@ class MovementManager { get lastPos () { return this.player.entity.position.clone() } set lastPos (newPos) { this.player.entity.position.set(newPos.x, newPos.y, newPos.z) } - get lastRot () { return vec3(this.player.entity.yaw, this.player.entity.pitch, this.player.entity.headYaw) } - set lastRot (rot) { this.player.entity.yaw = rot.x this.player.entity.pitch = rot.y @@ -33,10 +31,10 @@ class MovementManager { const positionUpdated = !this.lastSentPos || !this.lastPos.equals(this.lastSentPos) const rotationUpdated = !this.lastSentRot || !this.lastRot.equals(this.lastSentRot) - if (positionUpdated) { + if (positionUpdated || rotationUpdated) { this.lastSentPos = this.lastPos.clone() - console.log('We computed', this.lastPos) - this.bot.updatePlayerCamera(2, this.lastSentPos, this.playerState.yaw, this.playerState.pitch) + // console.log('We computed', this.lastPos) + this.bot.updatePlayerCamera(2, this.lastSentPos, this.playerState.yaw, this.playerState.pitch || this.player.entity.pitch) if (this.serverMovements) { this.client.queue('player_auth_input', { pitch: this.player.pitch, @@ -175,6 +173,7 @@ class MovementManager { stop_gliding: false }) this.timeAccumulator -= PHYSICS_TIMESTEP + this.tick++ } } @@ -185,16 +184,29 @@ class MovementManager { }, PHYSICS_INTERVAL_MS) } + /** + * Sets the active control state and also keeps track of key toggles. + * @param {'forward' | 'back' | 'left' | 'right' | 'jump' | 'sprint' | 'sneak'} control + * @param {boolean} state + */ setControlState (control, state) { + // HACK ! switch left and right, fixes control issue + if (control === 'left') control = 'right' + else if (control === 'right') control = 'left' + if (this.controls[control] === state) return if (control === 'sprint') { this.player.events.startSprint = state this.player.events.stopSprint = !state + if (state) this.bot.emit('startSprint') + else this.bot.emit('stopSprint') this.controls.sprint = true } else if (control === 'sneak') { this.player.events.startSneak = state this.player.events.stopSneak = !state this.controls.sprint = true + } else { + this.controls[control] = state } } diff --git a/examples/viewer/index.js b/examples/viewer/index.js index f2a942c..5d17e38 100644 --- a/examples/viewer/index.js +++ b/examples/viewer/index.js @@ -1,5 +1,5 @@ const path = require('path') -const { app, BrowserWindow } = require('electron') +const { app, BrowserWindow, globalShortcut } = require('electron') function createMainWindow () { const window = new BrowserWindow({ @@ -28,6 +28,10 @@ function createMainWindow () { app.on('ready', () => { createMainWindow() + + globalShortcut.register('CommandOrControl+W', () => { + // no op + }) }) app.on('window-all-closed', function () { From 2aade9403346a18f9d35e8490699ccce4f0ccd5b Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 7 Apr 2021 06:56:24 -0400 Subject: [PATCH 17/21] update raknet-native, remove aes-js --- package.json | 3 +-- src/rak.js | 13 ++++++------ src/server/advertisement.js | 39 ++++++++++++++++++++++++++++++++++++ src/transforms/encryption.js | 26 ++++++++---------------- 4 files changed, 55 insertions(+), 26 deletions(-) create mode 100644 src/server/advertisement.js diff --git a/package.json b/package.json index 8090cfa..51cdac9 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@azure/msal-node": "^1.0.0-beta.6", "@jsprismarine/jsbinaryutils": "^2.1.8", "@xboxreplay/xboxlive-auth": "^3.3.3", - "aes-js": "^3.1.2", "asn1": "^0.2.4", "browserify-cipher": "^1.0.1", "debug": "^4.3.1", @@ -35,7 +34,7 @@ "node-fetch": "^2.6.1", "prismarine-nbt": "^1.5.0", "protodef": "^1.11.0", - "raknet-native": "^0.2.0", + "raknet-native": "^1.0.0", "uuid-1345": "^1.0.2" }, "devDependencies": { diff --git a/src/rak.js b/src/rak.js index 88682d6..912df2d 100644 --- a/src/rak.js +++ b/src/rak.js @@ -5,8 +5,9 @@ const Reliability = require('jsp-raknet/protocol/reliability') const RakClient = require('jsp-raknet/client') const ConnWorker = require('./rakWorker') const { waitFor } = require('./datatypes/util') +const ServerName = require('./server/advertisement') try { - var { Client, Server, PacketPriority, PacketReliability, McPingMessage } = require('raknet-native') // eslint-disable-line + var { Client, Server, PacketPriority, PacketReliability } = require('raknet-native') // eslint-disable-line } catch (e) { console.debug('[raknet] native not found, using js', e) } @@ -19,7 +20,7 @@ class RakNativeClient extends EventEmitter { this.onCloseConnection = () => { } this.onEncapsulated = () => { } - this.raknet = new Client(options.hostname, options.port, 'minecraft') + this.raknet = new Client(options.hostname, options.port, { protocolVersion: 10 }) this.raknet.on('encapsulated', ({ buffer, address }) => { this.onEncapsulated(buffer, address) }) @@ -65,16 +66,17 @@ class RakNativeClient extends EventEmitter { } class RakNativeServer extends EventEmitter { - constructor (options = {}) { + constructor (options = {}, server) { super() this.onOpenConnection = () => { } this.onCloseConnection = () => { } this.onEncapsulated = () => { } this.raknet = new Server(options.hostname, options.port, { maxConnections: options.maxConnections || 3, - minecraft: {}, - message: new McPingMessage().toBuffer() + protocolVersion: 10, + message: ServerName.getServerName(server) }) + // TODO: periodically update the server name until we're closed this.raknet.on('openConnection', (client) => { client.sendReliable = function (buffer, immediate) { @@ -91,7 +93,6 @@ class RakNativeServer extends EventEmitter { }) this.raknet.on('encapsulated', ({ buffer, address }) => { - // console.log('ENCAP',thingy) this.onEncapsulated(buffer, address) }) } diff --git a/src/server/advertisement.js b/src/server/advertisement.js new file mode 100644 index 0000000..0b9a401 --- /dev/null +++ b/src/server/advertisement.js @@ -0,0 +1,39 @@ +class ServerName { + motd = 'Bedrock Protocol Server' + name = 'bedrock-protocol' + protocol = 408 + version = '1.16.20' + players = { + online: 0, + max: 5 + } + + gamemode = 'Creative' + serverId = '0' + + toString (version) { + return [ + 'MCPE', + this.motd, + this.protocol, + this.version, + this.players.online, + this.players.max, + this.serverId, + this.name, + this.gamemode + ].join(';') + ';' + } + + toBuffer (version) { + const str = this.toString(version) + return Buffer.concat([Buffer.from([0, str.length]), Buffer.from(str)]) + } +} + +module.exports = { + ServerName, + getServerName (client) { + return new ServerName().toBuffer() + } +} diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index 81a70af..f06a279 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -1,8 +1,7 @@ const { Transform } = require('readable-stream') -const crypto = globalThis.isElectron ? require('browserify-cipher/browser') : require('crypto') -const { createHash } = require('crypto') -const aesjs = require('aes-js') +const crypto = require('crypto') const Zlib = require('zlib') +if (globalThis.isElectron) var { CipherCFB8 } = require('raknet-native') // eslint-ignore-line const CIPHER_ALG = 'aes-256-cfb8' @@ -23,37 +22,28 @@ function createDecipher (secret, initialValue) { class Cipher extends Transform { constructor (secret, iv) { super() - this.aes = new aesjs.ModeOfOperation.cfb(secret, iv, 1) // eslint-disable-line new-cap + this.aes = new CipherCFB8(secret, iv) } _transform (chunk, enc, cb) { - try { - const res = this.aes.encrypt(chunk) - cb(null, res) - } catch (e) { - cb(e) - } + const ciphered = this.aes.cipher(chunk) + cb(null, ciphered) } } class Decipher extends Transform { constructor (secret, iv) { super() - this.aes = new aesjs.ModeOfOperation.cfb(secret, iv, 1) // eslint-disable-line new-cap + this.aes = new CipherCFB8(secret, iv) } _transform (chunk, enc, cb) { - try { - const res = this.aes.decrypt(chunk) - cb(null, res) - } catch (e) { - cb(e) - } + cb(null, this.aes.decipher(chunk)) } } function computeCheckSum (packetPlaintext, sendCounter, secretKeyBytes) { - const digest = createHash('sha256') + const digest = crypto.createHash('sha256') const counter = Buffer.alloc(8) counter.writeBigInt64LE(sendCounter, 0) digest.update(counter) From 41b9f7b383aba7a7fe409a39ff2f8fc7b7c0d617 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 7 Apr 2021 07:10:32 -0400 Subject: [PATCH 18/21] viewer: working first person --- .eslintignore | 1 + examples/viewer/client/BotViewer.js | 2 +- examples/viewer/client/ClientProvider.js | 10 ++++--- examples/viewer/client/movements.js | 35 ++++++++++++------------ examples/viewer/client/util.js | 8 +++++- src/relay.js | 2 +- src/transforms/encryption.js | 2 +- 7 files changed, 34 insertions(+), 26 deletions(-) diff --git a/.eslintignore b/.eslintignore index e69de29..61743e4 100644 --- a/.eslintignore +++ b/.eslintignore @@ -0,0 +1 @@ +examples/viewer \ No newline at end of file diff --git a/examples/viewer/client/BotViewer.js b/examples/viewer/client/BotViewer.js index 384008e..867fccb 100644 --- a/examples/viewer/client/BotViewer.js +++ b/examples/viewer/client/BotViewer.js @@ -95,7 +95,7 @@ class BotViewer { onPointerLockChange = () => { const e = this.renderer.domElement - if (document.pointerLockElement === e) { + if (document.pointerLockElement === e) { e.parentElement.addEventListener('mousemove', this.onMouseMove, { passive: true }) } else { e.parentElement.removeEventListener('mousemove', this.onMouseMove, false) diff --git a/examples/viewer/client/ClientProvider.js b/examples/viewer/client/ClientProvider.js index 62aae74..e8f3ada 100644 --- a/examples/viewer/client/ClientProvider.js +++ b/examples/viewer/client/ClientProvider.js @@ -14,7 +14,7 @@ class ClientProvider extends BotProvider { downKeys = new Set() connect () { - const client = new Client({ hostname: '127.0.0.1', version: '1.16.210', port: 19132, connectTimeout: 100000 }) + const client = new Client({ hostname: '127.0.0.1', version: '1.16.210', username: 'notch', offline: true, port: 19132, connectTimeout: 100000 }) client.once('resource_packs_info', (packet) => { client.write('resource_pack_client_response', { @@ -50,7 +50,7 @@ class ClientProvider extends BotProvider { }) this.client.on('start_game', packet => { this.updatePosition(packet.player_position) - this.movements.init('', packet.player_position, null, packet.rotation.z, packet.rotation.x, 0) + this.movements.init('server', packet.player_position, /* vel */ null, packet.rotation.z || 0, packet.rotation.x || 0, 0) }) this.client.on('spawn', () => { @@ -68,7 +68,9 @@ class ClientProvider extends BotProvider { }) this.client.on('move_player', packet => { - if (packet.runtime_id === this.client.entityId) { this.movements.updatePosition(packet.position, packet.yaw, packet.pitch, packet.head_yaw, packet.tick) } + if (packet.runtime_id === this.client.entityId) { + this.movements.updatePosition(packet.position, packet.yaw, packet.pitch, packet.head_yaw, packet.tick) + } }) this.client.on('set_entity_motion', packet => { @@ -96,7 +98,7 @@ class ClientProvider extends BotProvider { onKeyUp = (evt) => { const code = evt.code - if (code == 'ControlLeft' && this.downKeys.has('ControlLeft')) { + if (code === 'ControlLeft' && this.downKeys.has('ControlLeft')) { this.movements.setControlState('sprint', false) } for (const control in controlMap) { diff --git a/examples/viewer/client/movements.js b/examples/viewer/client/movements.js index d5f52c0..0fdb76e 100644 --- a/examples/viewer/client/movements.js +++ b/examples/viewer/client/movements.js @@ -1,6 +1,6 @@ const { Physics, PlayerState } = require('prismarine-physics') const { performance } = require('perf_hooks') -const { d2r } = require('./util') +const { d2r, r2d } = require('./util') const vec3 = require('vec3') const PHYSICS_INTERVAL_MS = 50 @@ -21,9 +21,9 @@ class MovementManager { set lastPos (newPos) { this.player.entity.position.set(newPos.x, newPos.y, newPos.z) } get lastRot () { return vec3(this.player.entity.yaw, this.player.entity.pitch, this.player.entity.headYaw) } set lastRot (rot) { - this.player.entity.yaw = rot.x - this.player.entity.pitch = rot.y - if (rot.z) this.player.entity.headYaw = rot.z + if (!isNaN(rot.x)) this.player.entity.yaw = rot.x + if (!isNaN(rot.y)) this.player.entity.pitch = rot.y + if (!isNaN(rot.z)) this.player.entity.headYaw = rot.z } // Ask the server to be in a new position @@ -36,28 +36,29 @@ class MovementManager { // console.log('We computed', this.lastPos) this.bot.updatePlayerCamera(2, this.lastSentPos, this.playerState.yaw, this.playerState.pitch || this.player.entity.pitch) if (this.serverMovements) { - this.client.queue('player_auth_input', { - pitch: this.player.pitch, - yaw: this.player.yaw, + globalThis.movePayload = { + pitch: r2d(this.player.entity.pitch), + yaw: r2d(this.player.entity.yaw), // r2d(this.player.entity.yaw), position: { x: this.lastPos.x, - y: this.lastPos.y, + y: this.lastPos.y + 1.62, z: this.lastPos.z }, move_vector: { // Minecraft coords, N: Z+1, S: Z-1, W: X+1, E: X-1 x: inputState.left ? 1 : (inputState.right ? -1 : 0), z: inputState.up ? 1 : (inputState.down ? -1 : 0) }, - head_yaw: this.player.headYaw, + head_yaw: r2d(this.player.entity.yaw), // r2d(this.player.entity.headYaw), input_data: inputState, input_mode: 'mouse', play_mode: 'screen', tick: this.tick, delta: this.lastSentPos?.minus(this.lastPos) ?? { x: 0, y: 0, z: 0 } - }) - this.positionUpdated = false + } + this.bot.client.queue('player_auth_input', globalThis.movePayload) } + this.positionUpdated = false this.lastSentPos = this.lastPos this.lastSentRot = this.lastRot } @@ -126,8 +127,8 @@ class MovementManager { const q = this.inputQueue.shift() if (q) { Object.assign(this.playerState.control, q) - if (q.yaw) this.player.entity.yaw = q.yaw - if (q.pitch) this.player.entity.pitch = q.pitch + if (!isNaN(q.yaw)) this.player.entity.yaw = q.yaw + if (!isNaN(q.pitch)) this.player.entity.pitch = q.pitch } this.playerState = new PlayerState(this.player, this.controls) this.physics.simulatePlayer(this.playerState, this.world.sync).apply(this.player) @@ -147,8 +148,8 @@ class MovementManager { sneak_down: false, up: this.controls.forward, down: this.controls.back, - left: this.controls.left, - right: this.controls.right, + left: this.controls.right, + right: this.controls.left, up_left: false, up_right: false, want_up: this.controls.jump, // Jump @@ -247,9 +248,7 @@ class MovementManager { } onViewerCameraMove (newYaw, newPitch, newHeadYaw) { - this.player.yaw = newYaw - this.player.pitch = newPitch - this.player.headYaw = newHeadYaw + this.lastRot = { x: newYaw, y: newPitch, z: newHeadYaw } } } diff --git a/examples/viewer/client/util.js b/examples/viewer/client/util.js index a22a646..c947af9 100644 --- a/examples/viewer/client/util.js +++ b/examples/viewer/client/util.js @@ -9,8 +9,14 @@ const difference = (o1, o2) => Object.keys(o2).reduce((diff, key) => { const diff = (o1, o2) => { const dif = difference(o1, o2); return Object.keys(dif).length ? dif : null } const d2r = deg => (180 - (deg < 0 ? (360 + deg) : deg)) * (Math.PI / 180) +const r2d = rad => { + let deg = rad * (180 / Math.PI) + deg = deg % 360 + return 180 - deg +} module.exports = { diff, - d2r + d2r, + r2d } diff --git a/src/relay.js b/src/relay.js index 8203ad8..813ec05 100644 --- a/src/relay.js +++ b/src/relay.js @@ -3,7 +3,7 @@ const { Client } = require('./client') const { Server } = require('./server') const { Player } = require('./serverPlayer') const debug = globalThis.isElectron ? console.debug : require('debug')('minecraft-protocol relay') -const { serialize } = require('./datatypes/util') +// const { serialize } = require('./datatypes/util') /** @typedef {{ hostname: string, port: number, auth: 'client' | 'server' | null, destination?: { hostname: string, port: number } }} Options */ diff --git a/src/transforms/encryption.js b/src/transforms/encryption.js index f06a279..1d4d294 100644 --- a/src/transforms/encryption.js +++ b/src/transforms/encryption.js @@ -1,7 +1,7 @@ const { Transform } = require('readable-stream') const crypto = require('crypto') const Zlib = require('zlib') -if (globalThis.isElectron) var { CipherCFB8 } = require('raknet-native') // eslint-ignore-line +if (globalThis.isElectron) var { CipherCFB8 } = require('raknet-native') // eslint-disable-line const CIPHER_ALG = 'aes-256-cfb8' From a9ad14008e5dd0f3b647b404d6846833ed3c4fd3 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Thu, 8 Apr 2021 01:32:20 -0400 Subject: [PATCH 19/21] viewer: add sprint, sneaking --- examples/viewer/client/BotViewer.js | 31 ++++++++--- examples/viewer/client/movements.js | 80 +++++++++++++++++++++++------ examples/viewer/index.js | 9 ++-- package.json | 1 + 4 files changed, 96 insertions(+), 25 deletions(-) diff --git a/examples/viewer/client/BotViewer.js b/examples/viewer/client/BotViewer.js index 867fccb..57f9d16 100644 --- a/examples/viewer/client/BotViewer.js +++ b/examples/viewer/client/BotViewer.js @@ -2,7 +2,7 @@ const { Viewer, MapControls } = require('prismarine-viewer/viewer') // const { Vec3 } = require('vec3') const { ClientProvider } = require('./ClientProvider') -const { ProxyProvider } = require('./ProxyProvider') +// const { ProxyProvider } = require('./ProxyProvider') global.THREE = require('three') const MCVER = '1.16.1' @@ -61,12 +61,29 @@ class BotViewer { }) }) - this.bot.on('startSprint', () => { - this.viewer.camera.fov += 20 - }) - this.bot.on('stopSprint', () => { - this.viewer.camera.fov -= 20 - }) + const oldFov = this.viewer.camera.fov + const sprintFov = this.viewer.camera.fov + 20 + const sneakFov = this.viewer.camera.fov - 10 + + const onSprint = () => { + this.viewer.camera.fov = sprintFov + this.viewer.camera.updateProjectionMatrix() + } + + const onSneak = () => { + this.viewer.camera.fov = sneakFov + this.viewer.camera.updateProjectionMatrix() + } + + const onRelease = () => { + this.viewer.camera.fov = oldFov + this.viewer.camera.updateProjectionMatrix() + } + + this.bot.on('startSprint', onSprint) + this.bot.on('startSneak', onSneak) + this.bot.on('stopSprint', onRelease) + this.bot.on('stopSneak', onRelease) this.controls.update() diff --git a/examples/viewer/client/movements.js b/examples/viewer/client/movements.js index 0fdb76e..d359bea 100644 --- a/examples/viewer/client/movements.js +++ b/examples/viewer/client/movements.js @@ -5,6 +5,7 @@ const vec3 = require('vec3') const PHYSICS_INTERVAL_MS = 50 const PHYSICS_TIMESTEP = PHYSICS_INTERVAL_MS / 1000 +const AXES = ['forward', 'back', 'left', 'right'] class MovementManager { // Server auth movement : we send inputs, server calculates position & sends back @@ -38,7 +39,7 @@ class MovementManager { if (this.serverMovements) { globalThis.movePayload = { pitch: r2d(this.player.entity.pitch), - yaw: r2d(this.player.entity.yaw), // r2d(this.player.entity.yaw), + yaw: r2d(this.player.entity.yaw), position: { x: this.lastPos.x, y: this.lastPos.y + 1.62, @@ -48,7 +49,7 @@ class MovementManager { x: inputState.left ? 1 : (inputState.right ? -1 : 0), z: inputState.up ? 1 : (inputState.down ? -1 : 0) }, - head_yaw: r2d(this.player.entity.yaw), // r2d(this.player.entity.headYaw), + head_yaw: r2d(this.player.entity.yaw), input_data: inputState, input_mode: 'mouse', play_mode: 'screen', @@ -93,6 +94,7 @@ class MovementManager { startSneak: false, stopSneak: false }, + sprinting: false, jumpTicks: 0, jumpQueued: false, downJump: false @@ -185,36 +187,80 @@ class MovementManager { }, PHYSICS_INTERVAL_MS) } + get sprinting() { + return this.player.sprinting + } + + set sprinting(val) { + this.player.events.startSprint = val + this.player.events.stopSprint = !val + if (val && !this.player.sprinting) { + this.bot.emit('startSprint') + } else { + this.bot.emit('stopSprint') + } + this.player.sprinting = val + } + + _lastInput = { control: '', time: 0 } + /** * Sets the active control state and also keeps track of key toggles. * @param {'forward' | 'back' | 'left' | 'right' | 'jump' | 'sprint' | 'sneak'} control * @param {boolean} state */ - setControlState (control, state) { + setControlState (control, state, time = Date.now()) { // HACK ! switch left and right, fixes control issue if (control === 'left') control = 'right' else if (control === 'right') control = 'left' if (this.controls[control] === state) return - if (control === 'sprint') { - this.player.events.startSprint = state - this.player.events.stopSprint = !state - if (state) this.bot.emit('startSprint') - else this.bot.emit('stopSprint') - this.controls.sprint = true - } else if (control === 'sneak') { - this.player.events.startSneak = state - this.player.events.stopSneak = !state - this.controls.sprint = true - } else { - this.controls[control] = state + + const isAxis = AXES.includes(control) + let hasOtherAxisKeyDown = false + for (const c of AXES) { + if (this.controls[c] && c != control) { + hasOtherAxisKeyDown = true + } } + + if (control === 'sprint') { + if (state && hasOtherAxisKeyDown) { // sprint down + a axis movement key + this.sprinting = true + } else if ((!state || !hasOtherAxisKeyDown) && this.sprinting) { // sprint up or movement key up & current sprinting + this.bot.emit('stopSprint') + this.sprinting = false + } + } else if (isAxis && this.controls.sprint) { + if (!state && !hasOtherAxisKeyDown) { + this.sprinting = false + } else if (state && !hasOtherAxisKeyDown) { + this.sprinting = true + } + } else if (control === 'sneak') { + if (state) { + this.player.events.startSneak = true + this.bot.emit('startSneak') + } else { + this.player.events.stopSneak = true + this.bot.emit('stopSneak') + } + } else if (control === 'forward' && this._lastInput.control === 'forward' && (Date.now() - this._lastInput.time) < 100 && !this.controls.sprint) { + // double tap forward within 0.5 seconds, toggle sprint + // this.controls.sprint = true + // this.sprinting = true + } + + this._lastInput = { control, time } + this.controls[control] = state } stopPhys () { clearInterval(this.physicsLoop) } + // Called when a proxy player sends a PlayerInputPacket. We need to apply these inputs tick-by-tick + // as these packets are sent by the client every tick. pushInputState (state, yaw, pitch) { const yawRad = d2r(yaw) const pitchRad = d2r(pitch) @@ -232,6 +278,9 @@ class MovementManager { globalThis.debugYaw = [yaw, yawRad] } + + // Called when a proxy player sends a PlayerInputPacket. We need to apply these inputs tick-by-tick + // as these packets are sent by the client every tick. pushCameraControl (state, id = 1) { let { x, y, z } = state.position if (id === 1) y -= 1.62 // account for player bb @@ -247,6 +296,7 @@ class MovementManager { if (tick) this.tick = tick } + // User has moved the camera. Update the movements stored. onViewerCameraMove (newYaw, newPitch, newHeadYaw) { this.lastRot = { x: newYaw, y: newPitch, z: newHeadYaw } } diff --git a/examples/viewer/index.js b/examples/viewer/index.js index 5d17e38..954d4b6 100644 --- a/examples/viewer/index.js +++ b/examples/viewer/index.js @@ -1,7 +1,7 @@ const path = require('path') const { app, BrowserWindow, globalShortcut } = require('electron') -function createMainWindow () { +function createMainWindow() { const window = new BrowserWindow({ webPreferences: { nodeIntegration: true, @@ -27,10 +27,13 @@ function createMainWindow () { } app.on('ready', () => { - createMainWindow() + const win = createMainWindow() globalShortcut.register('CommandOrControl+W', () => { - // no op + win.webContents.sendInputEvent({ + type: 'keyDown', + keyCode: 'W' + }) }) }) diff --git a/package.json b/package.json index 51cdac9..e6371bd 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@xboxreplay/xboxlive-auth": "^3.3.3", "asn1": "^0.2.4", "browserify-cipher": "^1.0.1", + "bedrock-provider": "^1.0.0", "debug": "^4.3.1", "ec-pem": "^0.18.0", "jsonwebtoken": "^8.5.1", From fc39d697989461cd65036917cdec9656fb07d950 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 14 Apr 2021 07:23:42 -0400 Subject: [PATCH 20/21] protocol: inventory, recipe updates --- data/1.16.210/protocol.json | 155 ++++++++++++++++++++---------------- data/latest/proto.yml | 6 +- data/latest/types.yaml | 100 +++++++++++++---------- examples/createRelay.js | 2 +- 4 files changed, 147 insertions(+), 116 deletions(-) diff --git a/data/1.16.210/protocol.json b/data/1.16.210/protocol.json index 9a61484..47043ad 100644 --- a/data/1.16.210/protocol.json +++ b/data/1.16.210/protocol.json @@ -1074,8 +1074,8 @@ "container", [ { - "name": "runtime_id", - "type": "zigzag32" + "name": "stack_id", + "type": "varint" }, { "name": "item", @@ -1256,7 +1256,7 @@ }, { "name": "network_id", - "type": "zigzag32" + "type": "varint" } ] ], @@ -1301,7 +1301,7 @@ }, { "name": "network_id", - "type": "zigzag32" + "type": "varint" } ] ], @@ -1346,7 +1346,7 @@ }, { "name": "network_id", - "type": "zigzag32" + "type": "varint" } ] ], @@ -1405,7 +1405,7 @@ }, { "name": "network_id", - "type": "zigzag32" + "type": "varint" } ] ], @@ -1464,7 +1464,7 @@ }, { "name": "network_id", - "type": "zigzag32" + "type": "varint" } ] ], @@ -1515,7 +1515,7 @@ }, { "name": "network_id", - "type": "zigzag32" + "type": "varint" } ] ] @@ -2049,7 +2049,7 @@ "22": "stop_swimming", "23": "start_spin_attack", "24": "stop_spin_attack", - "25": "ineract_block", + "25": "interact_block", "26": "predict_break", "27": "continue_break" } @@ -2059,11 +2059,11 @@ "container", [ { - "name": "container_id", - "type": "u8" + "name": "slot_type", + "type": "ContainerSlotType" }, { - "name": "slot_id", + "name": "slot", "type": "u8" }, { @@ -2077,7 +2077,7 @@ [ { "name": "request_id", - "type": "zigzag32" + "type": "varint" }, { "name": "actions", @@ -2273,7 +2273,7 @@ "container", [ { - "name": "creative_item_network_id", + "name": "item_id", "type": "varint32" } ] @@ -2358,58 +2358,75 @@ "type": "varint32" }, { - "name": "containers", + "anon": true, "type": [ - "array", + "switch", { - "countType": "varint", - "type": [ - "container", - [ - { - "name": "slot_type", - "type": "ContainerSlotType" - }, - { - "name": "slots", - "type": [ - "array", - { - "countType": "varint", - "type": [ - "container", - [ - { - "name": "slot", - "type": "u8" - }, - { - "name": "hotbar_slot", - "type": "u8" - }, - { - "name": "count", - "type": "u8" - }, - { - "name": "item_stack_id", - "type": "varint32" - }, - { - "name": "custom_name", - "type": "string" - }, - { - "name": "durability_correction", - "type": "zigzag32" - } + "compareTo": "status", + "fields": { + "ok": [ + "container", + [ + { + "name": "containers", + "type": [ + "array", + { + "countType": "varint", + "type": [ + "container", + [ + { + "name": "slot_type", + "type": "ContainerSlotType" + }, + { + "name": "slots", + "type": [ + "array", + { + "countType": "varint", + "type": [ + "container", + [ + { + "name": "slot", + "type": "u8" + }, + { + "name": "hotbar_slot", + "type": "u8" + }, + { + "name": "count", + "type": "u8" + }, + { + "name": "item_stack_id", + "type": "varint32" + }, + { + "name": "custom_name", + "type": "string" + }, + { + "name": "durability_correction", + "type": "zigzag32" + } + ] + ] + } + ] + } + ] ] - ] - } - ] - } + } + ] + } + ] ] - ] + }, + "default": "void" } ] } @@ -2575,7 +2592,7 @@ "WindowType": [ "mapper", { - "type": "u8", + "type": "i8", "mappings": { "0": "container", "1": "workbench", @@ -2610,7 +2627,9 @@ "30": "cartography", "31": "hud", "32": "jigsaw_editor", - "33": "smithing_table" + "33": "smithing_table", + "-9": "none", + "-1": "inventory" } } ], @@ -4442,7 +4461,7 @@ "type": "u8" }, { - "name": "windows_id", + "name": "window_id", "type": "WindowID" } ] @@ -4490,7 +4509,7 @@ ] }, { - "name": "target_runtime_entity_id", + "name": "target_entity_id", "type": "varint64" }, { @@ -4748,8 +4767,8 @@ "container", [ { - "name": "inventory_id", - "type": "varint" + "name": "window_id", + "type": "WindowIDVarint" }, { "name": "input", diff --git a/data/latest/proto.yml b/data/latest/proto.yml index 0d8db35..e46463d 100644 --- a/data/latest/proto.yml +++ b/data/latest/proto.yml @@ -769,7 +769,7 @@ packet_mob_equipment: item: Item slot: u8 selected_slot: u8 - windows_id: WindowID + window_id: WindowID packet_mob_armor_equipment: !id: 0x20 @@ -793,7 +793,7 @@ packet_interact: 6: open_inventory # TargetEntityRuntimeID is the runtime ID of the entity that the player interacted with. This is empty # for the InteractActionOpenInventory action type. - target_runtime_entity_id: varint64 + target_entity_id: varint64 # Position associated with the ActionType above. For the InteractActionMouseOverEntity, this is the # position relative to the entity moused over over which the player hovered with its mouse/touch. For the # InteractActionLeaveVehicle, this is the position that the player spawns at after leaving the vehicle. @@ -947,7 +947,7 @@ packet_inventory_content: !bound: both # WindowID is the ID that identifies one of the windows that the client currently has opened, or one of # the consistent windows such as the main inventory. - inventory_id: varint + window_id: WindowIDVarint # Content is the new content of the inventory. The length of this slice must be equal to the full size of # the inventory window updated. input: ItemStacks diff --git a/data/latest/types.yaml b/data/latest/types.yaml index b2d8730..7d85243 100644 --- a/data/latest/types.yaml +++ b/data/latest/types.yaml @@ -548,7 +548,7 @@ ItemStack: # StackNetworkID is the network ID of the item stack. If the stack is empty, 0 is always written for this # field. If not, the field should be set to 1 if the server authoritative inventories are disabled in the # StartGame packet, or to a unique stack ID if it is enabled. - runtime_id: zigzag32 + stack_id: varint # Stack is the actual item stack of the item instance. item: Item @@ -577,16 +577,16 @@ PotionContainerChangeRecipes: []varint Recipes: []varint type: zigzag32 => - '0': 'shapeless' #'ENTRY_SHAPELESS', - '1': 'shaped' #'ENTRY_SHAPED', - '2': 'furnace' # 'ENTRY_FURNACE', + 0: shapeless #'ENTRY_SHAPELESS', + 1: shaped #'ENTRY_SHAPED', + 2: furnace # 'ENTRY_FURNACE', # `furnace_with_metadata` is a recipe specifically used for furnace-type crafting stations. It is equal to # `furnace`, except it has an input item with a specific metadata value, instead of any metadata value. - '3': 'furnace_with_metadata' # 'ENTRY_FURNACE_DATA', // has metadata - '4': 'multi' #'ENTRY_MULTI', //TODO - '5': 'shulker_box' #'ENTRY_SHULKER_BOX', //TODO - '6': 'shapeless_chemistry' #'ENTRY_SHAPELESS_CHEMISTRY', //TODO - '7': 'shaped_chemistry' #'ENTRY_SHAPED_CHEMISTRY', //TODO + 3: furnace_with_metadata # 'ENTRY_FURNACE_DATA', // has metadata + 4: multi #'ENTRY_MULTI', //TODO + 5: shulker_box #'ENTRY_SHULKER_BOX', //TODO + 6: shapeless_chemistry #'ENTRY_SHAPELESS_CHEMISTRY', //TODO + 7: shaped_chemistry #'ENTRY_SHAPED_CHEMISTRY', //TODO recipe: type? if shapeless or shulker_box or shapeless_chemistry: recipe_id: string @@ -595,7 +595,7 @@ Recipes: []varint uuid: uuid block: string priority: zigzag32 - network_id: zigzag32 + network_id: varint if shaped or shaped_chemistry: recipe_id: string width: zigzag32 @@ -608,7 +608,7 @@ Recipes: []varint uuid: uuid block: string priority: zigzag32 - network_id: zigzag32 + network_id: varint if furnace: input_id: zigzag32 output: Item @@ -620,7 +620,7 @@ Recipes: []varint block: string if multi: uuid: uuid - network_id: zigzag32 + network_id: varint SkinImage: width: li32 @@ -748,13 +748,20 @@ Action: zigzag32 => 22: stop_swimming 23: start_spin_attack 24: stop_spin_attack - 25: ineract_block + 25: interact_block 26: predict_break 27: continue_break +# Source and Destination point to the source slot from which Count of the item stack were taken and the +# destination slot to which this item was moved. StackRequestSlotInfo: - container_id: u8 - slot_id: u8 + # ContainerID is the ID of the container that the slot was in. + slot_type: ContainerSlotType + # Slot is the index of the slot within the container with the ContainerID above. + slot: u8 + # StackNetworkID is the unique stack ID that the client assumes to be present in this slot. The server + # must check if these IDs match. If they do not match, servers should reject the stack request that the + # action holding this info was in. stack_id: zigzag32 # ItemStackRequest is sent by the client to change item stacks in an inventory. It is essentially a @@ -764,7 +771,7 @@ StackRequestSlotInfo: ItemStackRequest: # RequestID is a unique ID for the request. This ID is used by the server to send a response for this # specific request in the ItemStackResponse packet. - request_id: zigzag32 + request_id: varint actions: []varint type_id: u8 => # TakeStackRequestAction is sent by the client to the server to take x amount of items from one slot in a @@ -872,9 +879,9 @@ ItemStackRequest: # of 1.16. recipe_network_id: varint if craft_creative: - # CreativeItemNetworkID is the network ID of the creative item that is being created. This is one of the - # creative item network IDs sent in the CreativeContent packet. - creative_item_network_id: varint32 + # The stack ID of the creative item that is being created. This is one of the + # creative item stack IDs sent in the CreativeContent packet. + item_id: varint32 if optional: # For the cartography table, if a certain MULTI recipe is being called, this points to the network ID that was assigned. recipe_network_id: varint @@ -901,30 +908,32 @@ ItemStackResponses: []varint # RequestID is the unique ID of the request that this response is in reaction to. If rejected, the client # will undo the actions from the request with this ID. request_id: varint32 - # ContainerInfo holds information on the containers that had their contents changed as a result of the - # request. - containers: []varint - # ContainerID is the container ID of the container that the slots that follow are in. For the main - # inventory, this value seems to be 0x1b. For the cursor, this value seems to be 0x3a. For the crafting - # grid, this value seems to be 0x0d. - # * actually, this is ContainerSlotType - used by the inventory system that specifies the type of slot - slot_type: ContainerSlotType - # SlotInfo holds information on what item stack should be present in specific slots in the container. - slots: []varint - # Slot and HotbarSlot seem to be the same value every time: The slot that was actually changed. I'm not - # sure if these slots ever differ. - slot: u8 - hotbar_slot: u8 - # Count is the total count of the item stack. This count will be shown client-side after the response is - # sent to the client. - count: u8 - # StackNetworkID is the network ID of the new stack at a specific slot. - item_stack_id: varint32 - # CustomName is the custom name of the item stack. It is used in relation to text filtering. - custom_name: string - # DurabilityCorrection is the current durability of the item stack. This durability will be shown - # client-side after the response is sent to the client. - durability_correction: zigzag32 + _: status ? + if ok: + # ContainerInfo holds information on the containers that had their contents changed as a result of the + # request. + containers: []varint + # ContainerID is the container ID of the container that the slots that follow are in. For the main + # inventory, this value seems to be 0x1b. For the cursor, this value seems to be 0x3a. For the crafting + # grid, this value seems to be 0x0d. + # * actually, this is ContainerSlotType - used by the inventory system that specifies the type of slot + slot_type: ContainerSlotType + # SlotInfo holds information on what item stack should be present in specific slots in the container. + slots: []varint + # Slot and HotbarSlot seem to be the same value every time: The slot that was actually changed. I'm not + # sure if these slots ever differ. + slot: u8 + hotbar_slot: u8 + # Count is the total count of the item stack. This count will be shown client-side after the response is + # sent to the client. + count: u8 + # StackNetworkID is the network ID of the new stack at a specific slot. + item_stack_id: varint32 + # CustomName is the custom name of the item stack. It is used in relation to text filtering. + custom_name: string + # DurabilityCorrection is the current durability of the item stack. This durability will be shown + # client-side after the response is sent to the client. + durability_correction: zigzag32 ItemComponentList: []varint @@ -1025,7 +1034,9 @@ WindowIDVarint: varint => 123: fixed_inventory 124: ui -WindowType: u8 => +WindowType: i8 => + -9: none + -1: inventory 0: container 1: workbench 2: furnace @@ -1061,6 +1072,7 @@ WindowType: u8 => 32: jigsaw_editor 33: smithing_table +# Used in inventory transactions. ContainerSlotType: u8 => - anvil_input - anvil_material diff --git a/examples/createRelay.js b/examples/createRelay.js index 31d8553..d3b76ad 100644 --- a/examples/createRelay.js +++ b/examples/createRelay.js @@ -32,7 +32,7 @@ function createRelay () { } }) - relay.create() + relay.listen() } createRelay() From 8663247a2aa77bd9f9648715a35acfef922fd139 Mon Sep 17 00:00:00 2001 From: extremeheat Date: Wed, 14 Apr 2021 07:42:36 -0400 Subject: [PATCH 21/21] Lessen logging, fix JWT param --- src/auth/loginVerify.js | 7 ++++--- src/client/tokens.js | 2 +- src/datatypes/BatchPacket.js | 2 -- src/serverPlayer.js | 2 +- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/auth/loginVerify.js b/src/auth/loginVerify.js index e1c2743..0809269 100644 --- a/src/auth/loginVerify.js +++ b/src/auth/loginVerify.js @@ -1,5 +1,6 @@ const JWT = require('jsonwebtoken') const constants = require('./constants') +const debug = require('debug')('minecraft-protocol') module.exports = (client, server, options) => { // Refer to the docs: @@ -19,14 +20,14 @@ module.exports = (client, server, options) => { let finalKey = null // console.log(pubKey) for (const token of chain) { - const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' }) + const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] }) // console.log('Decoded', decoded) // Check if signed by Mojang key const x5u = getX5U(token) if (x5u === constants.PUBLIC_KEY && !data.extraData?.XUID) { // didVerify = true - console.log('verified with mojang key!', x5u) + debug('Verified client with mojang key', x5u) } // TODO: Handle `didVerify` = false @@ -41,7 +42,7 @@ module.exports = (client, server, options) => { function verifySkin (publicKey, token) { const pubKey = mcPubKeyToPem(publicKey) - const decoded = JWT.verify(token, pubKey, { algorithms: 'ES384' }) + const decoded = JWT.verify(token, pubKey, { algorithms: ['ES384'] }) return decoded } diff --git a/src/client/tokens.js b/src/client/tokens.js index af82f45..6603df9 100644 --- a/src/client/tokens.js +++ b/src/client/tokens.js @@ -244,7 +244,7 @@ class MinecraftTokenManager { const token = this.cache.mca debug('[mc] token cache', this.cache) if (!token) return - console.log('TOKEN', token) + debug('Auth token', token) const jwt = token.chain[0] const [header, payload, signature] = jwt.split('.').map(k => Buffer.from(k, 'base64')) // eslint-disable-line diff --git a/src/datatypes/BatchPacket.js b/src/datatypes/BatchPacket.js index cdfb292..8c1094c 100644 --- a/src/datatypes/BatchPacket.js +++ b/src/datatypes/BatchPacket.js @@ -38,10 +38,8 @@ class BatchPacket { encode () { const buf = this.stream.getBuffer() - console.log('Encoding payload', buf) const def = Zlib.deflateRawSync(buf, { level: this.compressionLevel }) const ret = Buffer.concat([Buffer.from([0xfe]), def]) - console.log('Compressed', ret) return ret } diff --git a/src/serverPlayer.js b/src/serverPlayer.js index b11d8f4..d2a6d93 100644 --- a/src/serverPlayer.js +++ b/src/serverPlayer.js @@ -57,7 +57,7 @@ class Player extends Connection { // TODO: disconnect user throw new Error('Failed to verify user') } - console.log('Verified user', 'got pub key', key, userData) + debug('Verified user 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