diff --git a/.gitignore b/.gitignore index fe44a853..f03da047 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules package-lock.json +yarn.lock /test/*.png versions/ public/index.js* diff --git a/README.md b/README.md index 7f52d92d..223b41df 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,14 @@ Remove the primitive with the given id from the display. Stop the server and disconnect users. +#### bot.viewer.on('onRender', (fps) => void) + +Provides an approximation of the current highest fps of any client connected to the server. + +Use this event for events that should be fired at least once per render cycle. + + + ## Tests `npm run jestTest -- -t "1.9.4"` diff --git a/examples/bot.js b/examples/bot.js index 6fa505a9..6b030b11 100644 --- a/examples/bot.js +++ b/examples/bot.js @@ -2,7 +2,10 @@ const mineflayer = require('mineflayer') const mineflayerViewer = require('prismarine-viewer').mineflayer const bot = mineflayer.createBot({ - username: 'Bot' + username: 'Bot', + host: process.argv[2], + port: isNaN(parseInt(process.argv[3])) ? 25565 : parseInt(process.argv[3]), + version: process.argv[4] ?? '1.16.5' }) bot.once('spawn', () => { diff --git a/examples/render_bot.js b/examples/render_bot.js new file mode 100644 index 00000000..e81e4699 --- /dev/null +++ b/examples/render_bot.js @@ -0,0 +1,32 @@ +const mineflayer = require('mineflayer') +const mineflayerViewer = require('prismarine-viewer').mineflayer + +const bot = mineflayer.createBot({ + username: 'Bot', + host: process.argv[2], + port: isNaN(parseInt(process.argv[3])) ? 25565 : parseInt(process.argv[3]), + version: process.argv[4] ?? '1.16.5' +}) + +bot.once('spawn', () => { + mineflayerViewer(bot, { port: 3000 }) + + // approximate maximum FPS of all clients connected to our viewer + bot.viewer.on('onRender', (fps) => { + // do something every frame + + // random offset of bot position, +/5 in x and z, +5 in y + const pos = bot.entity.position.offset(Math.random() * 10 - 5, 5, Math.random() * 10 - 5) + + bot.viewer.erase('posExample') + bot.viewer.drawBoxGrid('posExample', pos, pos.offset(1, 1, 1), 'aqua') + }) + + const path = [bot.entity.position.clone()] + bot.on('move', () => { + if (path[path.length - 1].distanceTo(bot.entity.position) > 1) { + path.push(bot.entity.position.clone()) + bot.viewer.drawLine('path', path) + } + }) +}) diff --git a/examples/typescript/bot.ts b/examples/typescript/bot.ts new file mode 100644 index 00000000..7ee2d2e4 --- /dev/null +++ b/examples/typescript/bot.ts @@ -0,0 +1,18 @@ +import * as mineflayer from "mineflayer"; +import { mineflayer as mineflayerLoader } from "prismarine-viewer"; + +import type { Vec3 } from "vec3"; + +const bot = mineflayer.createBot({ + username: "Bot", +}); + +bot.once("spawn", async () => { + mineflayerLoader(bot, { port: 3000 }); + + const path: Vec3[] = [bot.entity.position.clone()]; + if (path[path.length - 1].distanceTo(bot.entity.position) > 1) { + path.push(bot.entity.position.clone()); + bot.viewer.drawLine("path", path); + } +}); diff --git a/index.d.ts b/index.d.ts index 4a39dc75..ea10e63b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,4 +1,32 @@ -import {Bot} from "mineflayer"; +import type {Bot} from "mineflayer"; + +// TODO: BlockFace should be from here (0 to 5) +import { Block } from "prismarine-block"; +import type { Vec3 } from "vec3"; + +import type {PointsMaterialParameters} from 'three' + + +interface BotViewerEvents { + blockClicked: (block: Block, face: number, button: number) => void; + onRender: (fps: number) => void; +} + +interface BotViewer { + erase: (id: string) => void; + drawBoxGrid: (id: string, start: Vec3, end: Vec3, color: PointsMaterialParameters) => void; + drawLine: (id: string, points: Vec3[], color?: PointsMaterialParameters) => void; + drawPoints: (id: string, points: Vec3[], color?: PointsMaterialParameters, size?: number) => void; + close: () => void; +} + + +declare module 'mineflayer' { + interface Bot { + viewer: BotViewer; + } +} + export function mineflayer(bot: Bot, settings: { viewDistance?: number; diff --git a/lib/index.js b/lib/index.js index 706772dd..da5ae9ae 100644 --- a/lib/index.js +++ b/lib/index.js @@ -20,10 +20,28 @@ const viewer = new Viewer(renderer) let controls = new THREE.OrbitControls(viewer.camera, renderer.domElement) +const fpsCheck = 1000 // ms +let lastTime = performance.now() +let frameCount = 0 + +function handleUpdate () { + frameCount++ + const currentTime = performance.now() + + if (currentTime - lastTime > fpsCheck) { + const fps = Math.ceil((frameCount / ((currentTime - lastTime) / fpsCheck))) + lastTime = currentTime + frameCount = 0 + if (fps != null) socket.emit('renderFPS', { id: socket.id, fps }) + } + + viewer.update() +} + function animate () { window.requestAnimationFrame(animate) if (controls) controls.update() - viewer.update() + handleUpdate() renderer.render(viewer.scene, viewer.camera) } animate() diff --git a/lib/mineflayer.js b/lib/mineflayer.js index 06634bf6..50828ecf 100644 --- a/lib/mineflayer.js +++ b/lib/mineflayer.js @@ -45,7 +45,83 @@ module.exports = (bot, { viewDistance = 6, firstPerson = false, port = 3000, pre } } + let renderInterval = null + + let maxFPS = 0 + let maxFPSId = null + + const fpsMap = new Map() + io.on('connection', (socket) => { + const getRenderInterval = (fps) => setInterval(() => bot.viewer.emit('onRender', fps), 1000 / fps) + + const updateListener = ({ id, fps }) => { + if (id == null || fps == null) return + + fpsMap.set(id, fps) + + if (fps > maxFPS) { + maxFPS = fps + if (renderInterval) clearInterval(renderInterval) + + renderInterval = getRenderInterval(maxFPS) + } else if (id === maxFPSId && fps < maxFPS) { + // handle where maxFPSId's fps decreases + + let secondHighest = 0 + let secondHighestId = null + + // check for alternative highest fps + for (const [id1, fps1] of fpsMap) { + if (fps1 > secondHighest && id1 !== id) { + secondHighest = fps1 + secondHighestId = id1 + } + } + + // if there is no alternative highest fps, set maxFPS to current fps + // note: if secondHighest is 0, then there is no alternative highest fps + if (fps > secondHighest) { + maxFPS = fps + maxFPSId = id + if (renderInterval) clearInterval(renderInterval) + + renderInterval = getRenderInterval(maxFPS) + } else { + // if there is an alternative highest fps that is higher than current FPS, + // set maxFPS to the alternative highest fps + maxFPS = secondHighest + maxFPSId = secondHighestId + if (renderInterval) clearInterval(renderInterval) + + renderInterval = getRenderInterval(maxFPS) + } + } + } + + const onSocketRemoval = () => { + if (fpsMap.has(socket.id)) { + fpsMap.delete(socket.id) + } + + if (fpsMap.size === 0) { + maxFPS = 0 + if (renderInterval) clearInterval(renderInterval) + return + } + + for (const [, fps] of fpsMap) { + if (fps > maxFPS) { + maxFPS = fps + } + } + + if (renderInterval) clearInterval(renderInterval) + + renderInterval = getRenderInterval(maxFPS) + } + + socket.on('renderFPS', updateListener) socket.emit('version', bot.version) sockets.push(socket) @@ -75,6 +151,7 @@ module.exports = (bot, { viewDistance = 6, firstPerson = false, port = 3000, pre bot.removeListener('move', botPosition) worldView.removeListenersFromBot(bot) sockets.splice(sockets.indexOf(socket), 1) + onSocketRemoval() }) }) @@ -87,5 +164,9 @@ module.exports = (bot, { viewDistance = 6, firstPerson = false, port = 3000, pre for (const socket of sockets) { socket.disconnect() } + + // should already always be handled, but hey you never know. + if (renderInterval) clearInterval(renderInterval) + fpsMap.clear() } } diff --git a/package.json b/package.json index 4a7696a0..c83d01b5 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "vec3": "^0.1.7" }, "devDependencies": { + "@types/three": "^0.161.2", "assert": "^2.0.0", "buffer": "^6.0.3", "canvas": "^2.6.1", diff --git a/viewer/lib/viewer.js b/viewer/lib/viewer.js index db02d904..6aeafd10 100644 --- a/viewer/lib/viewer.js +++ b/viewer/lib/viewer.js @@ -27,7 +27,7 @@ class Viewer { this.primitives = new Primitives(this.scene, this.camera) this.domElement = renderer.domElement - this.playerHeight = 1.6 + this.playerHeight = 1.62 this.isSneaking = false }