├── public ├── index.js ├── css │ └── main.css └── index.html ├── .dockerignore ├── example ├── simplePicture │ ├── .dockerignore │ ├── package.json │ ├── README.md │ ├── Dockerfile │ └── simplePicture.js ├── browserCubeMap │ ├── package.json │ ├── Dockerfile │ ├── README.md │ └── browserView.js └── simplePanorama │ ├── Dockerfile │ ├── README.md │ ├── package.json │ └── simplePanorama.js ├── .gitignore ├── lib ├── download_three.js ├── web.js └── camera.js ├── index.d.ts ├── index.js ├── LICENSE ├── package.json └── README.md /public/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /example/simplePicture/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /example/screenshots/package-lock.json 2 | /example/screenshots/screenshots/* 3 | /package-lock.json 4 | /screenshots 5 | node_modules 6 | -------------------------------------------------------------------------------- /example/simplePicture/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "express": "^4.17.1", 4 | "node-canvas-webgl": "^0.2.6", 5 | "prismarine-viewer": "^1.21.0", 6 | "mineflayer": "^3.7.0", 7 | "mineflayer-pathfinder": "^1.6.3", 8 | "three": "^0.127.0", 9 | "vec3": "^0.1.7" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/browserCubeMap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "postinstall": "node lib/download_three.js" 4 | }, 5 | "dependencies": { 6 | "express": "^4.17.1", 7 | "node-canvas-webgl": "^0.2.6", 8 | "prismarine-viewer": "^1.21.0", 9 | "mineflayer": "^3.7.0", 10 | "three": "^0.127.0", 11 | "vec3": "^0.1.7" 12 | } 13 | } -------------------------------------------------------------------------------- /example/simplePicture/README.md: -------------------------------------------------------------------------------- 1 | ### Docker build 2 | ``` 3 | docker build . -f example/simplePicture/Dockerfile -t simple-image-bot 4 | ``` 5 | 6 | ### Docker run 7 | ```bash 8 | docker run --rm -v ${pwd}/screenshots:/usr/src/app/screenshots --name image-bot -e HOST= -e PORT= -e USERNAME= -e PASSWORD= simple-image-bot 9 | ``` 10 | For connectiong to a local server use `host.docker.internal` for HOST 11 | -------------------------------------------------------------------------------- /example/simplePanorama/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | RUN apt-get update -y 4 | RUN apt-get install -y xserver-xorg-dev libxi-dev xserver-xorg-dev libxext-dev xvfb 5 | 6 | WORKDIR /usr/src/app 7 | 8 | COPY example/simplePanorama/package.json ./ 9 | RUN mkdir screenshots 10 | 11 | RUN npm install 12 | 13 | COPY . . 14 | 15 | # Start the container with the BrowserCubeMap example script 16 | CMD xvfb-run --auto-servernum --server-num=1 --server-args='-ac -screen 0 1280x1024x24' node example/simplePanorama/simplePanorama.js $HOST $PORT $USERNAME $PASSWORD 17 | -------------------------------------------------------------------------------- /lib/download_three.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const https = require('https') 3 | const threeJsPath = 'public/js/three.module.js' 4 | const threeJsUrl = 'https://threejs.org/build/three.module.js' 5 | 6 | try { 7 | if (fs.statSync(threeJsPath).isFile()) { 8 | return 9 | } 10 | } catch (e) { 11 | } 12 | 13 | console.info('Downloading THREE.js from', threeJsUrl) 14 | fs.mkdirSync('public/js', {recursive: true}) 15 | const file = fs.createWriteStream(threeJsPath) 16 | https.get(threeJsUrl, function(response) { 17 | response.pipe(file) 18 | }) 19 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Bot } from 'mineflayer'; 2 | export declare function plugin(bot: Bot): void; 3 | import { Vec3 } from 'vec3'; 4 | 5 | declare module 'panoramaImage' { 6 | import ReadableStream = NodeJS.ReadableStream; 7 | 8 | export function image(bot: Bot): void; 9 | 10 | export interface moduleImage { 11 | takePanoramaPictures(): Promise 12 | } 13 | } 14 | 15 | declare module 'image' { 16 | export function image(bot: Bot): void; 17 | 18 | export interface image { 19 | takePicture(point: Vec3, direction: Vec3): Promise; 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const Camera = require('./lib/camera') 2 | 3 | function panoramaImage (bot) { 4 | bot.panoramaImage = {} 5 | const cam = new Camera(bot) 6 | 7 | // Don't touch things break 8 | bot.panoramaImage.takePanoramaPictures = async function (camPos) { 9 | return cam.takePanoramaPictures(camPos) 10 | } 11 | } 12 | 13 | function image (bot) { 14 | bot.image = {} 15 | const cam = new Camera(bot) 16 | 17 | // Don't touch things break 18 | bot.image.takePicture = async function (point, direction) { 19 | return cam.takePicture(point, direction) 20 | } 21 | } 22 | 23 | module.exports = { 24 | panoramaImage, 25 | image 26 | } 27 | -------------------------------------------------------------------------------- /example/simplePanorama/README.md: -------------------------------------------------------------------------------- 1 | ### Docker build 2 | ``` 3 | docker build . -f example/simplePanorama/Dockerfile -t simple-pano-bot 4 | ``` 5 | 6 | ### Docker run (linux/unix) 7 | ```bash 8 | docker run --rm -v $(pwd)/screenshots:/usr/src/app/screenshots --name simple-pano-bot -e HOST= -e PORT= -e USERNAME= -e PASSWORD= simple-pano-bot 9 | ``` 10 | 11 | ### Docker run (Windows) 12 | ``` 13 | docker run --rm -v ${pwd}/screenshots:/usr/src/app/screenshots --name simple-pano-bot -e HOST= -e PORT= -e USERNAME= -e PASSWORD= simple-pano-bot 14 | ``` 15 | 16 | For connectiong to Docker Host localhost use `host.docker.internal` as HOST 17 | -------------------------------------------------------------------------------- /example/simplePicture/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | # Install stuff needed for node-canvas-webgl to work in docker 4 | RUN apt-get update -y 5 | RUN apt-get install -y xserver-xorg-dev libxi-dev xserver-xorg-dev libxext-dev xvfb 6 | 7 | WORKDIR /usr/src/app 8 | 9 | COPY example/simplePicture/package.json . 10 | 11 | RUN npm install 12 | 13 | COPY . . 14 | 15 | EXPOSE 8080 16 | 17 | # Start the container with the BrowserCubeMap example script 18 | # xvfb-run and the arguments behide it are needed for node-canvas-webgl to work. 19 | CMD xvfb-run --auto-servernum --server-num=1 --server-args='-ac -screen 0 1280x1024x24' node example/simplePicture/simplePicture.js $HOST $PORT $USERNAME $PASSWORD 20 | -------------------------------------------------------------------------------- /example/browserCubeMap/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14 2 | 3 | # Install stuff needed for node-canvas-webgl to work in docker 4 | RUN apt-get update -y 5 | RUN apt-get install -y xserver-xorg-dev libxi-dev xserver-xorg-dev libxext-dev xvfb 6 | 7 | WORKDIR /usr/src/app 8 | 9 | COPY example/browserCubeMap/package.json package.json 10 | COPY lib/download_three.js lib/download_three.js 11 | 12 | RUN npm install 13 | RUN npm run postinstall 14 | 15 | COPY . . 16 | 17 | EXPOSE 8080 18 | 19 | # Start the container with the BrowserCubeMap example script 20 | # xvfb-run and the arguments behide it are needed for node-canvas-webgl to work. 21 | CMD xvfb-run --auto-servernum --server-num=1 --server-args='-ac -screen 0 1280x1024x24' node example/browserCubeMap/browserView.js $HOST $PORT $USERNAME $PASSWORD 22 | -------------------------------------------------------------------------------- /example/browserCubeMap/README.md: -------------------------------------------------------------------------------- 1 | ```bash 2 | cd mineflayer-panorama 3 | docker build . -f example/browserCubeMap/Dockerfile -t panorama-bot 4 | # can be any name not only panorama-bot 5 | 6 | docker run --rm -p 8080:8080 --name pano-bot -e HOST= -e PORT= -e USERNAME= -e PASSWORD= panorama-bot 7 | # pano-bot is the container name 8 | # panorama-bot is the image name 9 | # -p exposes the port Hostport:8080 10 | # -e USERNAME and -e PASSWORD are only required for online servers 11 | # for connecting to localhost use: host.docker.internal as HOST. If you want to connect to a server on the internet put its ip as HOST instead. 12 | 13 | # If for whatever reason the THREE module has not been downloaded by npm you can navigate into the container: 14 | docker exec -it /bin/bash 15 | # and execture the npm install script again 16 | > npm run download_three 17 | ``` -------------------------------------------------------------------------------- /lib/web.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const EventEmitter = require('events').EventEmitter 3 | const path = require('path') 4 | 5 | class WebServer extends EventEmitter { 6 | constructor (bot, PORT) { 7 | super() 8 | this.bot = bot 9 | this.READY = false 10 | this.PORT = PORT 11 | this.app = express() 12 | this.app.use(express.static(path.join(__dirname, '../public'))) 13 | this.app.get('/textures/current.jpeg', async (req, res) => { 14 | console.info('Got current.jpeg taking panorama') 15 | const stream = await this.bot.panoramaImage.takePanoramaPictures() 16 | res.setHeader('content-type', 'image/jpeg') 17 | stream.pipe(res) 18 | stream.on('error', (err) => { 19 | console.error(err) 20 | }) 21 | }) 22 | this.app.listen(this.PORT, () => { 23 | console.info(`Webserver running on port ${this.PORT}`) 24 | this.READY = true 25 | this.emit('ready') 26 | }) 27 | } 28 | } 29 | 30 | module.exports = { 31 | web: WebServer 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 TheDudeFromCI 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Ic3Tank" 4 | }, 5 | "bugs": { 6 | "url": "https://github.com/IceTank/mineflayer-panorama/issues" 7 | }, 8 | "dependencies": { 9 | "express": "^4.17.1", 10 | "node-canvas-webgl": "^0.2.6", 11 | "prismarine-viewer": "^1.19.2", 12 | "three": "^0.127.0", 13 | "vec3": "^0.1.7" 14 | }, 15 | "name": "mineflayer-panorama", 16 | "version": "0.0.1", 17 | "description": "A Mineflayer Plugin for generating Panoramas", 18 | "deprecated": false, 19 | "devDependencies": { 20 | "@types/node": "14.14.5", 21 | "require-self": "0.2.3", 22 | "typescript": "4.0.5", 23 | "mineflayer": "^4.3.0", 24 | "standard": "^16.0.1" 25 | }, 26 | "homepage": "https://github.com/IceTank/mineflayer-panorama#readme", 27 | "keywords": [ 28 | "mineflayer", 29 | "plugin", 30 | "panorama" 31 | ], 32 | "license": "MIT", 33 | "main": "lib/index.js", 34 | "repository": { 35 | "type": "git", 36 | "url": "https://github.com/IceTank/mineflayer-panorama.git" 37 | }, 38 | "scripts": { 39 | "fix": "standard --fix", 40 | "lint": "standard" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/simplePanorama/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Ic3Tank" 4 | }, 5 | "bugs": { 6 | "url": "https://github.com/IceTank/mineflayer-panorama/issues" 7 | }, 8 | "dependencies": { 9 | "express": "^4.17.1", 10 | "mineflayer-pathfinder": "^1.6.1", 11 | "node-canvas-webgl": "^0.2.6", 12 | "prismarine-viewer": "^1.21.0", 13 | "three": "^0.127.0", 14 | "vec3": "^0.1.7" 15 | }, 16 | "name": "mineflayer-panorama", 17 | "version": "0.0.1", 18 | "description": "A Mineflayer Plugin for generating Panoramas", 19 | "deprecated": false, 20 | "devDependencies": { 21 | "@types/node": "14.14.5", 22 | "require-self": "0.2.3", 23 | "typescript": "4.0.5", 24 | "mineflayer": "^3.6.0", 25 | "standard": "^16.0.1" 26 | }, 27 | "homepage": "https://github.com/IceTank/mineflayer-panorama#readme", 28 | "keywords": [ 29 | "mineflayer", 30 | "plugin", 31 | "panorama" 32 | ], 33 | "license": "MIT", 34 | "main": "lib/index.js", 35 | "repository": { 36 | "type": "git", 37 | "url": "https://github.com/IceTank/mineflayer-panorama.git" 38 | }, 39 | "scripts": { 40 | "clean": "rm -rf lib", 41 | "prepare": "node lib/setup.js", 42 | "fix": "standard --fix", 43 | "lint": "standard" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/browserCubeMap/browserView.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example launches a website on port 8080 to display panorama images of the bots current location. 3 | * To launch this example run 'node browserView.js [port] [email or name] [password]'. 4 | */ 5 | 6 | const mineflayer = require('mineflayer') 7 | const panorama = require('../../index').panoramaImage 8 | const server = require('../../lib/web') 9 | 10 | if (process.argv.length < 3 || process.argv.length > 6) { 11 | console.log('Usage : node browserView.js [port] [email or name] [password]') 12 | process.exit(1) 13 | } 14 | 15 | const bot = mineflayer.createBot({ 16 | host: process.argv[2], 17 | port: process.argv[3] ? parseInt(process.argv[3]) : 25565, 18 | username: process.argv[4] ? process.argv[4] : 'screenshot', 19 | password: process.argv[5] 20 | }) 21 | 22 | bot.once('spawn', async () => { 23 | console.info('Bot spawned') 24 | bot.loadPlugin(panorama) 25 | new server.web(bot, 8080) 26 | console.info('Ready to use') 27 | }) 28 | 29 | bot.on('chat', (_, message) => { 30 | if (message === 'exit') process.exit(0) 31 | }) 32 | 33 | bot.on('kicked', (reason) => { 34 | console.info('Kicked for reason', reason) 35 | }) 36 | 37 | bot.on('end', (reason) => { 38 | console.info('Bot disconnected', reason) 39 | }) 40 | 41 | bot.on('error', (err) => { 42 | console.error(err) 43 | }) 44 | -------------------------------------------------------------------------------- /public/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | background-color: #000; 4 | color: #fff; 5 | font-family: Monospace; 6 | font-size: 13px; 7 | line-height: 24px; 8 | overscroll-behavior: none; 9 | } 10 | 11 | a { 12 | color: #ff0; 13 | text-decoration: none; 14 | } 15 | 16 | a:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | button { 21 | cursor: pointer; 22 | text-transform: uppercase; 23 | } 24 | 25 | #info { 26 | position: absolute; 27 | top: 0px; 28 | width: 100%; 29 | padding: 10px; 30 | box-sizing: border-box; 31 | text-align: center; 32 | -moz-user-select: none; 33 | -webkit-user-select: none; 34 | -ms-user-select: none; 35 | user-select: none; 36 | pointer-events: none; 37 | z-index: 1; /* TODO Solve this in HTML */ 38 | } 39 | 40 | a, button, input, select { 41 | pointer-events: auto; 42 | } 43 | 44 | .dg.ac { 45 | -moz-user-select: none; 46 | -webkit-user-select: none; 47 | -ms-user-select: none; 48 | user-select: none; 49 | z-index: 2 !important; /* TODO Solve this in HTML */ 50 | } 51 | 52 | #overlay { 53 | position: absolute; 54 | font-size: 16px; 55 | z-index: 2; 56 | top: 0; 57 | left: 0; 58 | width: 100%; 59 | height: 100%; 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | flex-direction: column; 64 | background: rgba(0,0,0,0.7); 65 | } 66 | 67 | #overlay button { 68 | background: transparent; 69 | border: 0; 70 | border: 1px solid rgb(255, 255, 255); 71 | border-radius: 4px; 72 | color: #ffffff; 73 | padding: 12px 18px; 74 | text-transform: uppercase; 75 | cursor: pointer; 76 | } 77 | 78 | #notSupported { 79 | width: 50%; 80 | margin: auto; 81 | background-color: #f00; 82 | margin-top: 20px; 83 | padding: 10px; 84 | } -------------------------------------------------------------------------------- /example/simplePanorama/simplePanorama.js: -------------------------------------------------------------------------------- 1 | const mineflayer = require('mineflayer') 2 | const panorama = require('../../index') 3 | const fs = require('fs') 4 | const { pathfinder, Movements, goals } = require('mineflayer-pathfinder') 5 | const Vec3 = require('vec3') 6 | 7 | if (process.argv.length < 4 || process.argv.length > 6) { 8 | console.log('Usage : node screenshot.js [] []') 9 | process.exit(1) 10 | } 11 | 12 | let panoramaReady = false 13 | let imageCounter = 0 14 | 15 | const bot = mineflayer.createBot({ 16 | host: process.argv[2], 17 | port: parseInt(process.argv[3]), 18 | username: process.argv[4] ? process.argv[4] : 'screenshot', 19 | password: process.argv[5] 20 | }) 21 | 22 | bot.on('spawn', async () => { 23 | console.info('Bot spawned') 24 | bot.loadPlugins([panorama.panoramaImage, pathfinder]) 25 | bot.pathfinder.setMovements(new Movements(bot, require('minecraft-data')(bot.version))) 26 | await bot.waitForChunksToLoad() 27 | bot.chat('Ready!') 28 | console.info('Ready to use') 29 | panoramaReady = true 30 | }) 31 | 32 | bot.on('chat', async (username, message) => { 33 | if (username === bot.username) return 34 | if (message.startsWith('pano')) { 35 | const cmd = message.split(' ') 36 | let fileName 37 | let pos = null 38 | if (message === 'pano') { 39 | fileName = 'next' 40 | } else if (cmd.length === 3) { 41 | fileName = cmd[1] 42 | pos = Number(cmd[2]) 43 | } else if (cmd.length === 5) { 44 | fileName = cmd[1] 45 | pos = new Vec3(cmd[2], cmd[3], cmd[4]) 46 | } else { 47 | bot.chat('Usage: pano [ | [ | ( )]]') 48 | return 49 | } 50 | bot.chat('Taking panorama Image') 51 | const imageName = await takePanorama(fileName, pos) 52 | bot.chat('Finished saving ' + imageName) 53 | } else if (message === 'come') { 54 | if (!bot.players[username]) { 55 | bot.chat('I can\'t see you') 56 | return 57 | } 58 | const { x, y, z } = bot.players[username].entity.position 59 | bot.pathfinder.setGoal(new goals.GoalNear(x, y, z, 2)) 60 | } else if (message === 'stop') { 61 | bot.pathfinder.setGoal(null) 62 | } else if (message === 'follow') { 63 | if (!bot.players[username]) { 64 | bot.chat('I can\'t see you') 65 | return 66 | } 67 | const target = bot.players[username].entity 68 | bot.pathfinder.setGoal(new goals.GoalFollow(target, 2), true) 69 | } else if (message === 'f') { 70 | bot.chat('F in chat') 71 | bot.quit() 72 | process.exit(0) 73 | } 74 | }) 75 | 76 | async function takePanorama (fileName, pos) { 77 | const fileStream = await bot.panoramaImage.takePanoramaPictures(pos) 78 | if (fileName === 'next') { 79 | fileName = 'image' + String(imageCounter).padStart(4, '0') 80 | imageCounter++ 81 | } 82 | console.info('Checking files') 83 | let stats 84 | try { 85 | stats = await fs.promises.stat('./screenshots') 86 | } catch (e) { 87 | if (!stats?.isDirectory()) { 88 | console.info('Making new folder screenshots') 89 | await fs.promises.mkdir('./screenshots') 90 | } 91 | } 92 | console.info('Writing file') 93 | const file = fs.createWriteStream('./screenshots/' + fileName + '.jpeg') 94 | fileStream.pipe(file) 95 | fileStream.on('error', (err) => { 96 | console.error(err) 97 | }) 98 | return new Promise((resolve) => { 99 | file.on('finish', () => { 100 | resolve(fileName) 101 | }) 102 | }) 103 | } 104 | 105 | bot.on('end', (reason) => { 106 | console.info('Bot disconnected', reason) 107 | }) 108 | 109 | bot.on('error', (err) => { 110 | console.error(err) 111 | }) 112 | -------------------------------------------------------------------------------- /example/simplePicture/simplePicture.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This example launches a website on port 8080 to display panorama images of the bots current location. 3 | * To launch this example run 'node simpleImage.js [port] [email or name] [password]'. 4 | */ 5 | 6 | const mineflayer = require('mineflayer') 7 | const { pathfinder, goals, Movements } = require('mineflayer-pathfinder') 8 | const fs = require('fs') 9 | const { Vec3 } = require('vec3') 10 | const image = require('../../index').image 11 | 12 | console.info('Hallo') 13 | 14 | if (process.argv.length < 3 || process.argv.length > 6) { 15 | console.log('Usage : node simplePicture.js [port] [email or name] [password]') 16 | process.exit(1) 17 | } 18 | 19 | const bot = mineflayer.createBot({ 20 | host: process.argv[2], 21 | port: process.argv[3] ? parseInt(process.argv[3]) : 25565, 22 | username: process.argv[4] ? process.argv[4] : 'picture', 23 | password: process.argv[5] 24 | }) 25 | 26 | imageReady = false 27 | 28 | bot.once('spawn', async () => { 29 | console.info('Bot spawned') 30 | bot.loadPlugins([image, pathfinder]) 31 | bot.pathfinder.setMovements(new Movements(bot, require('minecraft-data')(bot.version))) 32 | await bot.waitForChunksToLoad() 33 | bot.chat('Ready!') 34 | console.info('Ready to use') 35 | imageReady = true 36 | }) 37 | 38 | bot.on('chat', async (username, message) => { 39 | if (username === bot.username) return 40 | if (!imageReady) return bot.chat('No yet ready') 41 | 42 | const target = bot.players[username] 43 | 44 | if (message === 'look') { 45 | if (!target) return bot.chat('I can\'t see you!') 46 | let p = target.entity.position 47 | bot.lookAt(p.offset(0, target.entity.height, 0), true) 48 | } else if (message.startsWith('image')) { 49 | let [, name] = message.split(' ') 50 | if (!name) name = 'image' 51 | let { pitch, yaw } = bot.entity 52 | let direction = getViewVector(pitch, yaw) 53 | await takePicture(name, bot.entity.position.offset(0, bot.entity.height, 0), direction) 54 | bot.chat('Took picture ' + name + '.jpg') 55 | } else if (message === 'come') { 56 | if (!target) return bot.chat('I can\'t see you!') 57 | let {x, y, z} = target.entity.position 58 | bot.pathfinder.setGoal(new goals.GoalNear(x, y, z, 2)) 59 | } else if (message === 'follow') { 60 | if (!target) return bot.chat('I can\'t see you!') 61 | bot.pathfinder.setGoal(new goals.GoalFollow(target.entity, 2), true) 62 | } else if (message === 'stop') { 63 | bot.pathfinder.setGoal(null) 64 | } else if (message === 'exit') { 65 | bot.end() 66 | process.exit(0) 67 | } 68 | }) 69 | 70 | async function takePicture (fileName, pos, direction) { 71 | if (fileName === 'next') { 72 | fileName = 'image' + String(imageCounter).padStart(4, '0') 73 | imageCounter++ 74 | } 75 | console.info('Checking files') 76 | let stats 77 | try { 78 | stats = await fs.promises.stat('./screenshots') 79 | } catch (e) { 80 | if (!stats?.isDirectory()) { 81 | console.info('Making new folder screenshots') 82 | await fs.promises.mkdir('./screenshots') 83 | } 84 | } 85 | console.info('Taking image') 86 | const fileStream = await bot.image.takePicture(pos, direction) 87 | console.info('Writing file') 88 | const file = fs.createWriteStream('./screenshots/' + fileName + '.jpg') 89 | fileStream.pipe(file) 90 | fileStream.on('error', (err) => { 91 | console.error(err) 92 | }) 93 | return new Promise((resolve) => { 94 | file.on('finish', () => { 95 | resolve(fileName) 96 | }) 97 | }) 98 | } 99 | 100 | function getViewVector (pitch, yaw) { 101 | const csPitch = Math.cos(pitch) 102 | const snPitch = Math.sin(pitch) 103 | const csYaw = Math.cos(yaw) 104 | const snYaw = Math.sin(yaw) 105 | return new Vec3(-snYaw * csPitch, snPitch, -csYaw * csPitch) 106 | } 107 | 108 | bot.on('kicked', (reason) => { 109 | console.info('Kicked for reason', reason) 110 | }) 111 | 112 | bot.on('end', (reason) => { 113 | console.info('Bot disconnected', reason) 114 | }) 115 | 116 | bot.on('error', console.error) 117 | -------------------------------------------------------------------------------- /lib/camera.js: -------------------------------------------------------------------------------- 1 | const { Viewer, WorldView, getBufferFromStream } = require('prismarine-viewer').viewer 2 | global.Worker = require('worker_threads').Worker 3 | const THREE = require('three') 4 | const { createCanvas } = require('node-canvas-webgl/lib') 5 | const fs = require('fs').promises 6 | const Vec3 = require('vec3').Vec3 7 | const EventEmitter = require('events').EventEmitter 8 | 9 | class Camera extends EventEmitter { 10 | constructor (bot) { 11 | super() 12 | this.bot = bot 13 | this.cameraReady = false 14 | this.rendererLoadTime = 3000 15 | this.viewDistance = 4 16 | this.cameraHight = 10 17 | this.width = 512 18 | this.height = 512 19 | this.canvas = createCanvas(this.width, this.height) 20 | this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas }) 21 | this.viewer = new Viewer(this.renderer) 22 | this._init().then(() => { 23 | this.emit('camera_ready') 24 | }) 25 | } 26 | 27 | async _init () { 28 | const botPos = this.bot.entity.position 29 | const center = new Vec3(botPos.x, botPos.y + 10, botPos.z) 30 | this.viewer.setVersion(this.bot.version) 31 | 32 | // Load world 33 | this.worldView = new WorldView(this.bot.world, this.viewDistance, center) 34 | this.viewer.listen(this.worldView) 35 | 36 | this.viewer.camera.position.set(center.x, center.y, center.z) 37 | this.viewer.camera.lookAt(center.x + 1, center.y, center.z) 38 | 39 | await this.worldView.init(center) 40 | } 41 | 42 | async takePanoramaPictures (camPos) { 43 | this.renderer = new THREE.WebGLRenderer({ canvas: this.canvas }) 44 | this.viewer = new Viewer(this.renderer) 45 | this.viewer.setVersion(this.bot.version) 46 | this.viewer.camera.fov = 90 47 | this.viewer.camera.updateProjectionMatrix() 48 | let cameraHeight = this.cameraHight 49 | const botPos = this.bot.entity.position 50 | let viewPos = new Vec3(botPos.x, botPos.y, botPos.z) 51 | if (camPos && !isNaN(Number(camPos))) { 52 | cameraHeight = camPos 53 | } else if (camPos && 'x' in camPos && 'y' in camPos && 'z' in camPos) { 54 | viewPos = camPos 55 | cameraHeight = 0 56 | } 57 | // Load world 58 | const worldView = new WorldView(this.bot.world, this.viewDistance, viewPos) 59 | this.viewer.listen(worldView) 60 | this.viewer.camera.position.set(viewPos.x, viewPos.y + cameraHeight, viewPos.z) 61 | await worldView.init(viewPos) 62 | 63 | await this.viewer.waitForChunksToRender() 64 | 65 | const directions = [ 66 | new Vec3(1, 0, 0), new Vec3(-1, 0, 0), new Vec3(0, 1, 0), 67 | new Vec3(0, -1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1) 68 | ] 69 | const locations = [ 70 | { x: 0, y: 0 }, { x: this.width, y: 0 }, { x: this.width * 2, y: 0 }, 71 | { x: 0, y: this.height }, { x: this.width, y: this.height }, { x: this.width * 2, y: this.height } 72 | ] 73 | console.info('Creating canvas') 74 | const panoCanvis = createCanvas(this.width * 3, this.height * 2) 75 | const ctx = panoCanvis.getContext('2d') 76 | for (const i in directions) { 77 | const cameraPos = new Vec3(this.viewer.camera.position.x, this.viewer.camera.position.y, this.viewer.camera.position.z) 78 | const point = cameraPos.add(directions[i]) 79 | this.viewer.camera.lookAt(point.x, point.y, point.z) 80 | this.renderer.render(this.viewer.scene, this.viewer.camera) 81 | 82 | const loc = locations[i] 83 | ctx.drawImage(this.canvas, loc.x, loc.y, this.width, this.height) 84 | } 85 | console.info('Converting to image') 86 | const imageStream = panoCanvis.createJPEGStream({ 87 | bufsize: 4096, 88 | quality: 100, 89 | progressive: false 90 | }) 91 | return imageStream 92 | } 93 | 94 | async takePicture (point, direction) { 95 | if (!isVec(point) || !isVec(direction)) throw Error('invalid arguments') 96 | await this.worldView.updatePosition(point) 97 | this.viewer.camera.position.set(point.x, point.y, point.z) 98 | let { x, y, z } = point.plus(direction) 99 | this.viewer.camera.lookAt(x, y, z) 100 | 101 | console.info('Waiting for world to load') 102 | await this.viewer.waitForChunksToRender() 103 | this.renderer.render(this.viewer.scene, this.viewer.camera) 104 | 105 | const imageStream = this.canvas.createJPEGStream({ 106 | bufsize: 4096, 107 | quality: 100, 108 | progressive: false 109 | }) 110 | return imageStream 111 | } 112 | } 113 | 114 | function isVec(obj) { 115 | if (!obj) return false 116 | return obj instanceof Vec3 117 | } 118 | 119 | module.exports = Camera 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

mineflayer-panorama

2 | 3 |

4 | 5 | 6 | 7 | 8 | 9 | 10 |

11 | 12 | --- 13 | Make Simple Panorama images and view them in your browser! 14 | 15 | ![Download (1)](https://user-images.githubusercontent.com/61137113/113225697-8b005c00-928e-11eb-8bef-4ee1251cabdb.png) 16 | 17 | ## What is this? 18 | 19 | This is a plugin for [Mineflayer](https://github.com/PrismarineJS/mineflayer) a high level Node.js API for creating Minecraft Bots. 20 | Mineflayer-panorama allows you to make pretty panorama images off the world the bot is in. 21 | 22 | ## Getting Started 23 | 24 | This plugin is built using Node and can be installed using: 25 | 26 | ```bash 27 | git clone https://github.com/IceTank/mineflayer-panorama.git 28 | cd mineflayer-panorama 29 | npm install 30 | ``` 31 | 32 | ### Example 33 | 34 | For use as a plugin to make Panorama Images: 35 | ```js 36 | const panorama = require('./index') 37 | const mineflayer = require('mineflayer') 38 | const fs = require('fs') 39 | const bot = mineflayer.createBot({ 40 | host: 'localhost' 41 | }) 42 | 43 | bot.on('spawn', async () => { 44 | console.info('Bot spawned') 45 | bot.loadPlugin(panorama.image) 46 | bot.on('camera_ready', async () => { 47 | await bot.waitForChunksToLoad() 48 | console.info('Ready to use') 49 | let imageStream = await bot.panoramaImage.takePanoramaPictures() 50 | let image = fs.createWriteStream('panorama1.jpeg') 51 | imageStream.pipe(image) 52 | image.on('finish', () => { 53 | console.info('Wrote panorama image panorama1.jpeg') 54 | }) 55 | }) 56 | }) 57 | ``` 58 | 59 | This plugin also includes a webserver that can be used to view the generated images. 60 | The webserver opens a website on a given port and serves the current panorama view of the bot and a Panorama viewer. 61 | For an example see: `example/browserCubeMap/index.js`. 62 | Note: three.module.js has to be provided in `public/js`. If `npm install` did not download it, it has to be added manually. 63 | 64 | ## Running on Windows 65 | The rendering is done by node-canvas-webgl which is tricky to install on Windows. I recommend using a unix based operating system or Docker to run this plugin. 66 | 67 | ## Documentation 68 | 69 | ### `async bot.panoramaImage.takePanoramaPictures(camPos)` 70 | Takes a panorama Image and resolves with a jpeg stream of that image on success. 71 | * `camPos` - a `null`, `number` or `Vec3` like object 72 | * `null` - takes the panorama image from the bot current position 73 | * `number` - takes the panorama image with a high offset at the bots current location 74 | * `Vec3` like - takes the panorama image at a given location. Note: the bot can only render chunks that are loaded. Taking images outside of the current render distance may result in a blank image off the current sky color. 75 | 76 | ### `async bot.panoramaImage.takePicture(lookAt, name)` 77 | Takes a picture of the current world at the bots current position looking at the point `lookAt` 78 | * `lookAt` - a `Vec3` like object of the point the bot should look at 79 | * `name` - the name the jpeg image will be saved as 80 | 81 | ### `WebServer(bot, PORT)` 82 | A class providing a express app to host the panorama viewer on. 83 | When the page is opened the web server automatically calls `bot.panoramaImage.takePanoramaImage` to take an up to date panorama image to display in the panorama viewer. 84 | * `bot` - the mineflayer bot instance 85 | * `PORT` - the port the express app should listen on 86 | 87 | ### `WebServer.READY` - `boolean` if the express app is ready 88 | 89 | ### Events 90 | ### `"ready"` 91 | Emitted when the express app is ready and running 92 | 93 | ## Docker 94 | 95 | Example Dockerfiles are provided in the examples. 96 | To build an example Dockerfile use this command: 97 | ``` 98 | docker build . -f .\example\\Dockerfile -t 99 | ``` 100 | For running the container read the README.md files in the example folders. 101 | 102 | ## TODO 103 | * Add panorama Video streaming/saving 104 | * Add entitys to the panorama view 105 | 106 | ### License 107 | 108 | This project uses the [MIT](https://github.com/TheDudeFromCI/mineflayer-plugin-template/blob/master/LICENSE) license. 109 | 110 | ## Contributions 111 | 112 | This project is accepting PRs and Issues. See something you think can be improved? Go for it! Any and all help is highly appreciated! 113 | 114 | For larger changes, it is recommended to discuss these changes in the issues tab before writing any code. It's also preferred to make many smaller PRs than one large one, where applicable. 115 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | three.module.js webgl - panorama 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 | three.module.js webgl - cube panorama demo 13 |
14 | 15 | 151 | 152 | --------------------------------------------------------------------------------