From d2c0d3c386465d6302893717fbe31aa2e57ba28a Mon Sep 17 00:00:00 2001 From: extremeheat Date: Sat, 3 Apr 2021 20:34:42 -0400 Subject: [PATCH] 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) }