Repository: PrismarineJS/mineflayer-pathfinder Branch: master Commit: d1f4d7fdbebc Files: 38 Total size: 201.6 KB Directory structure: gitextract_nd4wfh09/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ ├── commands.yml │ └── npm-publish.yml ├── .gitignore ├── .gitpod.yml ├── .npmrc ├── LICENSE ├── examples/ │ ├── bench.js │ ├── blockInteraction.js │ ├── callback.js │ ├── chaining-goals.js │ ├── example.js │ ├── exclusionArea.js │ ├── movements.js │ ├── multiple.js │ ├── promise.js │ └── tutorial/ │ ├── basic.js │ ├── goalComposite.js │ └── goalsExplained.md ├── history.md ├── index.d.ts ├── index.js ├── lib/ │ ├── astar.js │ ├── goals.js │ ├── goto.js │ ├── heap.js │ ├── interactable.json │ ├── lock.js │ ├── move.js │ ├── movements.js │ ├── passableEntities.json │ ├── physics.js │ └── shapes.js ├── package.json ├── readme.md └── test/ ├── internalTest.js └── schematics/ └── parkour1.schem ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily open-pull-requests-limit: 10 ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [24.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test ================================================ FILE: .github/workflows/commands.yml ================================================ name: Repo Commands on: issue_comment: # Handle comment commands types: [created] pull_request: # Handle renamed PRs types: [edited] permissions: contents: write jobs: comment-trigger: runs-on: ubuntu-latest steps: - name: Check out repository uses: actions/checkout@v3 - name: Run command handlers uses: PrismarineJS/prismarine-repo-actions@master with: token: ${{ secrets.PAT_PASSWORD }} install-command: npm install /fixlint.fix-command: npm run fix ================================================ FILE: .github/workflows/npm-publish.yml ================================================ name: npm-publish on: push: branches: - master # Change this to your default branch jobs: npm-publish: name: npm-publish runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@master - name: Set up Node.js uses: actions/setup-node@master with: node-version: 14.0.0 - id: publish uses: JS-DevTools/npm-publish@v1 with: token: ${{ secrets.NPM_AUTH_TOKEN }} - name: Create Release if: steps.publish.outputs.type != 'none' id: create_release uses: actions/create-release@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ steps.publish.outputs.version }} release_name: Release ${{ steps.publish.outputs.version }} body: ${{ steps.publish.outputs.version }} draft: false prerelease: false ================================================ FILE: .gitignore ================================================ node_modules package-lock.json yarn.lock .vscode ================================================ FILE: .gitpod.yml ================================================ tasks: - command: npm install ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Karang Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: examples/bench.js ================================================ // Simple test to evaluate how much time it takes to find a path of 100 blocks const mineflayer = require('mineflayer') const { pathfinder, Movements } = require('mineflayer-pathfinder') const { GoalXZ } = require('mineflayer-pathfinder').goals const { performance } = require('perf_hooks') if (process.argv.length > 6) { console.log('Usage : node bench.js [] [] [] []') process.exit(1) } const bot = mineflayer.createBot({ host: process.argv[2] || 'localhost', port: parseInt(process.argv[3]) || 25565, username: process.argv[4] || 'bench', password: process.argv[5] }) bot.loadPlugin(pathfinder) bot.on('error', (err) => console.log(err)) const createTime = performance.now() bot.once('spawn', () => { console.log('Spawning took ' + (performance.now() - createTime).toFixed(2) + ' ms.') const defaultMove = new Movements(bot) const goal = new GoalXZ(bot.entity.position.x + 100, bot.entity.position.z) const results = bot.pathfinder.getPathTo(defaultMove, goal, 10000) console.log('I can get there in ' + results.path.length + ' moves. Computation took ' + results.time.toFixed(2) + ' ms.') bot.quit() process.exit() }) ================================================ FILE: examples/blockInteraction.js ================================================ const mineflayer = require('mineflayer') const { pathfinder, Movements } = require('mineflayer-pathfinder') const { GoalNear, GoalBlock, GoalXZ, GoalY, GoalFollow, GoalPlaceBlock, GoalLookAtBlock } = require('mineflayer-pathfinder').goals const Vec3 = require('vec3').Vec3 if (process.argv.length > 6) { console.log('Usage : node blockInteraction.js [] [] [] []') process.exit(1) } const bot = mineflayer.createBot({ host: process.argv[2] || 'localhost', port: parseInt(process.argv[3]) || 25565, username: process.argv[4] || 'blockPlacer', password: process.argv[5] }) bot.once('spawn', () => { console.info('Joined the server') bot.loadPlugin(pathfinder) const defaultMove = new Movements(bot) bot.pathfinder.setMovements(defaultMove) bot.on('chat', async (username, message) => { const target = bot.players[username].entity if (message.startsWith('place')) { const [, itemName] = message.split(' ') if (!target) { bot.chat('I can\'t see you') return } const itemsInInventory = bot.inventory.items().filter(item => item.name.includes(itemName)) if (itemsInInventory.length === 0) { bot.chat('I dont have ' + itemName) return } try { const rayBlock = rayTraceEntitySight(target) if (!rayBlock) { bot.chat('Block is out of reach') return } const face = directionToVector(rayBlock.face) await bot.pathfinder.goto(new GoalPlaceBlock(rayBlock.position.offset(face.x, face.y, face.z), bot.world, { range: 4 })) await bot.equip(itemsInInventory[0], 'hand') await bot.lookAt(rayBlock.position.offset(face.x * 0.5 + 0.5, face.y * 0.5 + 0.5, face.z * 0.5 + 0.5)) await bot.placeBlock(rayBlock, face) } catch (e) { console.error(e) } } else if (message.startsWith('break')) { if (!target) { bot.chat('I can\'t see you') return } try { const rayBlock = rayTraceEntitySight(target) if (!rayBlock) { bot.chat('Block is out of reach') return } await bot.pathfinder.goto(new GoalLookAtBlock(rayBlock.position, bot.world, { range: 4 })) const bestHarvestTool = bot.pathfinder.bestHarvestTool(bot.blockAt(rayBlock.position)) if (bestHarvestTool) await bot.equip(bestHarvestTool, 'hand') await bot.dig(bot.blockAt(rayBlock.position), true, 'raycast') } catch (e) { console.error(e) } } else if (message === 'come') { if (!target) { bot.chat('I don\'t see you !') return } const p = target.position bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalNear(p.x, p.y, p.z, 1)) } else if (message === 'stop') { bot.pathfinder.stop() } else if (message === 'follow') { bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalFollow(target, 1), true) // follow is a dynamic goal: setGoal(goal, dynamic=true) // when reached, the goal will stay active and will not // emit an event } else if (message.startsWith('goto')) { const cmd = message.split(' ') if (cmd.length === 4) { // goto x y z const x = parseInt(cmd[1], 10) const y = parseInt(cmd[2], 10) const z = parseInt(cmd[3], 10) bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalBlock(x, y, z)) } else if (cmd.length === 3) { // goto x z const x = parseInt(cmd[1], 10) const z = parseInt(cmd[2], 10) bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalXZ(x, z)) } else if (cmd.length === 2) { // goto y const y = parseInt(cmd[1], 10) bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalY(y)) } } }) const rayTraceEntitySight = function (entity) { if (bot.world?.raycast) { const { height, position, yaw, pitch } = entity const x = -Math.sin(yaw) * Math.cos(pitch) const y = Math.sin(pitch) const z = -Math.cos(yaw) * Math.cos(pitch) const rayBlock = bot.world.raycast(position.offset(0, height, 0), new Vec3(x, y, z), 120) if (rayBlock) { return rayBlock } } else { throw Error('bot.world.raycast does not exists. Try updating prismarine-world.') } } }) bot.on('error', console.error) bot.on('kicked', console.error) function directionToVector (dir) { if (dir > 5 || dir < 0) return null if (dir === 0) { return new Vec3(0, -1, 0) } else if (dir === 1) { return new Vec3(0, 1, 0) } else if (dir === 2) { return new Vec3(0, 0, -1) } else if (dir === 3) { return new Vec3(0, 0, 1) } else if (dir === 4) { return new Vec3(-1, 0, 0) } else if (dir === 5) { return new Vec3(1, 0, 0) } } ================================================ FILE: examples/callback.js ================================================ // This example uses promises instead of events like "goal_reached" for a cleaner look const mineflayer = require('mineflayer') const pathfinder = require('mineflayer-pathfinder').pathfinder const Movements = require('mineflayer-pathfinder').Movements const { GoalNear } = require('mineflayer-pathfinder').goals const bot = mineflayer.createBot({ username: 'Player' }) // Load plugins bot.loadPlugin(pathfinder) bot.once('spawn', () => { // Set pathfinder movements const defaultMove = new Movements(bot) bot.pathfinder.setMovements(defaultMove) bot.on('chat', async (username, message) => { // If username is the same as the Bot's username, don't continue if (username === bot.username) return // Only continue when the message is "come" if (message === 'come') { const target = bot.players[username] ? bot.players[username].entity : null // If Player doesn't exist, don't continue if (!target) return bot.chat("I don't see you !") // Await pathfinder to complete the goal, then move to bot.chat and print "I've arrived !" bot.pathfinder.goto(new GoalNear(target.position.x, target.position.y, target.position.z, 1)).then(announceArrived) } }) function announceArrived () { const botPosition = bot.entity.position bot.chat(`I've arrived, I'm at ${botPosition.x}, ${botPosition.y}, ${botPosition.z}!`) } }) ================================================ FILE: examples/chaining-goals.js ================================================ /* Pathfinder Chaining Goals example This example shows how to chain goals together. Run this example with: node examples/chaining-goals.js [host] [port] [mail/username] [is online `true`] If you want to connect to an offline server use a username instead of an email and no password. If you want to join a online server use your email and follow the instructions in the command line to authenticated with microsoft auth. In Game Chat commands: come - Path finds to the chatting player's position when in render distance. follow - Follows the chatting player's entity until `stop` is chatted stop - Stops the bot from following or path finding point - Set a checkpoint at the chatting player's position walk - Walk to all set checkpoints */ // Import all the modules we need const mineflayer = require('mineflayer') const { pathfinder, Movements } = require('mineflayer-pathfinder') const { GoalNear, GoalBlock, GoalFollow } = require('mineflayer-pathfinder').goals if (process.argv.length > 6) { console.log('Usage : node chaining-goals.js [] [] ' + '[] []') process.exit(1) } // Create the bot const bot = mineflayer.createBot({ host: process.argv[2] || 'localhost', port: parseInt(process.argv[3]) || 25565, username: process.argv[4] || 'checkpointBot', // to join offline servers auth type has to be 'mojang' (???) auth: process.argv[5] === 'true' ? 'microsoft' : 'mojang', // Skip validation when joining a offline server skipValidation: process.argv[5] !== 'true' }) // Load the pathfinder plugin bot.loadPlugin(pathfinder) // Wait for the bot to spawn in the world bot.once('spawn', () => { // We create different movement generators for different type of activity const defaultMove = new Movements(bot) bot.pathfinder.setMovements(defaultMove) // Print debug messages when the path changes bot.on('path_update', (r) => { const nodesPerTick = (r.visitedNodes * 50 / r.time).toFixed(2) console.log(`I can get there in ${r.path.length} moves. ` + `Computation took ${r.time.toFixed(2)} ms (${r.visitedNodes} nodes` + `, ${nodesPerTick} nodes/tick)`) }) bot.on('goal_reached', (goal) => { console.log('Here I am !') }) bot.on('path_reset', (reason) => { console.log(`Path was reset for reason: ${reason}`) }) let checkpoints = [] // Make pathfinder walk to all checkpoints in order async function walkCheckpoints () { if (checkpoints.length === 0) { bot.chat('There are no checkpoints') return } // Remove all checkpoints when starting to walking const checkPointCopy = [...checkpoints] checkpoints = [] for (const checkpoint of checkPointCopy) { // Make a new goal to goto. GoalBlock will make the bot walk to the // block position off checkpoint. const goal = new GoalBlock(checkpoint.x, checkpoint.y, checkpoint.z) try { // Use await to make sure the bot is at the checkpoint before moving on await bot.pathfinder.goto(goal) } catch (error) { console.log('Got error from goto', error.message) // If we get an error we quit the loop return } } } // Listen for chat messages chatted by other players // Note: This may not work on every server as mineflayer uses regex to match // chat messages. Some servers may use chat messages that do not match the // regex. bot.on('chat', (username, message) => { if (username === bot.username) return // Ignore our own messages // Get the player entity from the username. // Note: This might not work on some servers where the players nametag name // dose not match the chat message name. const target = bot.players[username] ? bot.players[username].entity : null if (message === 'come') { if (!target) { bot.chat('I don\'t see you !') return } const p = target.position bot.pathfinder.setGoal(new GoalNear(p.x, p.y, p.z, 1)) } else if (message === 'follow') { bot.pathfinder.setGoal(new GoalFollow(target, 3), true) // follow is a dynamic goal: setGoal(goal, dynamic=true) // when reached, the goal will stay active and will not // emit an event } else if (message === 'stop') { bot.pathfinder.stop() // Also resets the current goal } else if (message === 'point') { if (!target) { bot.chat('I don\'t see you !') return } const pos = target.position.floored() checkpoints.push(pos) bot.chat(`Checkpoint ${pos} set`) } else if (message === 'walk') { walkCheckpoints() .then(() => { bot.chat('Done') }) .catch(console.error) } }) }) ================================================ FILE: examples/example.js ================================================ const mineflayer = require('mineflayer') const { pathfinder, Movements } = require('mineflayer-pathfinder') const { GoalNear, GoalBlock, GoalXZ, GoalY, GoalInvert, GoalFollow, GoalBreakBlock } = require('mineflayer-pathfinder').goals if (process.argv.length > 6) { console.log('Usage : node example.js [] [] [] []') process.exit(1) } const bot = mineflayer.createBot({ host: process.argv[2] || 'localhost', port: parseInt(process.argv[3]) || 25565, username: process.argv[4] || 'pathfinder', password: process.argv[5] }) bot.loadPlugin(pathfinder) bot.once('spawn', () => { // We create different movement generators for different type of activity const defaultMove = new Movements(bot) bot.on('path_update', (r) => { const nodesPerTick = (r.visitedNodes * 50 / r.time).toFixed(2) console.log(`I can get there in ${r.path.length} moves. Computation took ${r.time.toFixed(2)} ms (${r.visitedNodes} nodes, ${nodesPerTick} nodes/tick)`) }) bot.on('goal_reached', (goal) => { console.log('Here I am !') }) bot.on('path_reset', (reason) => { console.log(`Path was reset for reason: ${reason}`) }) bot.on('chat', (username, message) => { if (username === bot.username) return const target = bot.players[username] ? bot.players[username].entity : null if (message === 'come') { if (!target) { bot.chat('I don\'t see you !') return } const p = target.position bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalNear(p.x, p.y, p.z, 1)) } else if (message.startsWith('goto')) { const cmd = message.split(' ') if (cmd.length === 4) { // goto x y z const x = parseInt(cmd[1], 10) const y = parseInt(cmd[2], 10) const z = parseInt(cmd[3], 10) bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalBlock(x, y, z)) } else if (cmd.length === 3) { // goto x z const x = parseInt(cmd[1], 10) const z = parseInt(cmd[2], 10) bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalXZ(x, z)) } else if (cmd.length === 2) { // goto y const y = parseInt(cmd[1], 10) bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalY(y)) } } else if (message === 'follow') { bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalFollow(target, 3), true) // follow is a dynamic goal: setGoal(goal, dynamic=true) // when reached, the goal will stay active and will not // emit an event } else if (message === 'avoid') { bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalInvert(new GoalFollow(target, 5)), true) } else if (message === 'stop') { bot.pathfinder.stop() } else if (message === 'break') { if (!target) { bot.chat('I can\'t see you!') return } const p = target.position.offset(0, -1, 0) const goal = new GoalBreakBlock(p.x, p.y, p.z, bot) bot.pathfinder.goto(goal) .then(() => { bot.dig(bot.blockAt(p), 'raycast') .catch(err => console.error('digging error', err)) }, (err) => { console.error('Pathfing error', err) }) } }) }) ================================================ FILE: examples/exclusionArea.js ================================================ /* Pathfinder Exclusion Area example This example shows the use of exclusion areas with the Movement Class. In Game Chat commands: come - Path finds to the chatting player's position when in render distance. exclude this (break | step | place) - Exclude a spherical area off size of type break, step or place at the chatting player's position when in render distance goto (x y z) | (x z) | y - Goto a specific coordinate follow - Follows the chatting player's entity until stop is chatted stop - Stops the bot from following or path finding */ const mineflayer = require('mineflayer') const { pathfinder, Movements } = require('mineflayer-pathfinder') const { GoalNear, GoalBlock, GoalXZ, GoalY, GoalFollow } = require('mineflayer-pathfinder').goals if (process.argv.length > 6) { console.log('Usage : node example.js [] [] [] []') process.exit(1) } const bot = mineflayer.createBot({ host: process.argv[2] || 'localhost', port: parseInt(process.argv[3]) || 25565, username: process.argv[4] || 'exclusionAreaBot', password: process.argv[5] }) bot.loadPlugin(pathfinder) bot.once('spawn', () => { // We create different movement generators for different type of activity const defaultMove = new Movements(bot) bot.pathfinder.setMovements(defaultMove) bot.on('path_update', (r) => { const nodesPerTick = (r.visitedNodes * 50 / r.time).toFixed(2) console.log(`I can get there in ${r.path.length} moves. ` + `Computation took ${r.time.toFixed(2)} ms (${r.visitedNodes} nodes` + `, ${nodesPerTick} nodes/tick)`) }) bot.on('goal_reached', (goal) => { console.log('Here I am !') }) bot.on('path_reset', (reason) => { console.log(`Path was reset for reason: ${reason}`) }) bot.on('chat', (username, message) => { if (username === bot.username) return const target = bot.players[username] ? bot.players[username].entity : null if (message === 'come') { if (!target) { bot.chat('I don\'t see you !') return } const p = target.position bot.pathfinder.setGoal(new GoalNear(p.x, p.y, p.z, 1)) } else if (message.startsWith('exclude')) { const cmd = message.split(' ') if (cmd[1] === 'this') { if (!target) { bot.chat('I can\'t see you') return } const type = cmd[2].trim() if (!['break', 'step', 'place'].includes(type.toLowerCase())) { return bot.chat('type must be "break", "step" or "place"') } const radius = Number(cmd[3]) const center = target.position.floored() if (isNaN(radius)) return bot.chat('Radius must be a number') // Import typings for intellisense /** * @param {import('mineflayer-pathfinder').SafeBlock} block block */ const isExcluded = (block) => { return block.position.distanceTo(center) <= radius ? 0 : 100 } switch (type.toLowerCase()) { case 'step': bot.pathfinder.movements.exclusionAreasStep.push(isExcluded) break case 'break': bot.pathfinder.movements.exclusionAreasBreak.push(isExcluded) break case 'place': bot.pathfinder.movements.exclusionAreasPlace.push(isExcluded) break } // At 5. The bot avoids the area most of the time but can still move into and out of it. bot.pathfinder.movements.exclusionAreaPower = 5 bot.pathfinder.setMovements(bot.pathfinder.movements) bot.chat(`Added exclusion area circle around ${center.toString()} with radius ${radius}`) } else { bot.chat('Usage: exclude this (break | step | place) ') } } else if (message.startsWith('goto')) { const cmd = message.split(' ') if (cmd.length === 4) { // goto x y z const x = parseInt(cmd[1], 10) const y = parseInt(cmd[2], 10) const z = parseInt(cmd[3], 10) bot.pathfinder.setGoal(new GoalBlock(x, y, z)) } else if (cmd.length === 3) { // goto x z const x = parseInt(cmd[1], 10) const z = parseInt(cmd[2], 10) bot.pathfinder.setGoal(new GoalXZ(x, z)) } else if (cmd.length === 2) { // goto y const y = parseInt(cmd[1], 10) bot.pathfinder.setGoal(new GoalY(y)) } } else if (message === 'follow') { bot.pathfinder.setGoal(new GoalFollow(target, 3), true) // follow is a dynamic goal: setGoal(goal, dynamic=true) // when reached, the goal will stay active and will not // emit an event } else if (message === 'stop') { bot.pathfinder.stop() // Also resets the current goal } }) }) ================================================ FILE: examples/movements.js ================================================ /* * This example demonstrates how easy it is to change the default movement * * Below are a few options you can edit in the Movement Class * but remember to check out the API documentation to find even more! * * This bot also follows a player when called called out to it. */ const mineflayer = require('mineflayer') const { pathfinder, Movements } = require('mineflayer-pathfinder') const { GoalNear } = require('mineflayer-pathfinder').goals const bot = mineflayer.createBot({ host: process.argv[2], port: parseInt(process.argv[3]), username: process.argv[4] ? process.argv[4] : 'movementsbot', password: process.argv[5] }) bot.loadPlugin(pathfinder) bot.once('spawn', () => { /* * pathfinder comes with default moves preinitialized (a instance of the movement class) * the moves come with default logic, like how much it can fall * what blocks are used to scaffold, and what blocks to avoid. */ // To get started create a instance of the Movements class const customMoves = new Movements(bot) // To make changes to the behaviour, customize the properties of the instance customMoves.canDig = false customMoves.allow1by1towers = false customMoves.scafoldingBlocks.push(bot.registry.itemsByName.stone.id) // Thing to note scaffoldingBlocks are an array while other namespaces are usually sets customMoves.blocksToAvoid.add(bot.registry.blocksByName.carrot.id) // To initialize the new movements use the .setMovements method. bot.pathfinder.setMovements(customMoves) bot.on('chat', function (username, message) { if (username === bot.username) return if (message === 'come') { const target = bot.players[username]?.entity if (!target) { bot.chat('I don\'t see you !') return } const p = target.position bot.pathfinder.setGoal(new GoalNear(p.x, p.y, p.z, 1)) } }) }) ================================================ FILE: examples/multiple.js ================================================ const mineflayer = require('mineflayer') const { pathfinder, Movements } = require('mineflayer-pathfinder') const { GoalInvert, GoalFollow } = require('mineflayer-pathfinder').goals mineflayer.multiple = (bots, constructor) => { const { Worker, isMainThread, workerData } = require('worker_threads') if (isMainThread) { const threads = [] for (const i in bots) { threads.push(new Worker(__filename, { workerData: bots[i] })) } } else { constructor(workerData) } } const bots = [] for (let i = 0; i < 40; i++) { bots.push({ username: `Bot${i}` }) } mineflayer.multiple(bots, ({ username }) => { const bot = mineflayer.createBot({ username, viewDistance: 'tiny' }) bot.loadPlugin(pathfinder) bot.once('spawn', () => { // We create different movement generators for different type of activity const defaultMove = new Movements(bot) defaultMove.allowFreeMotion = true bot.pathfinder.searchRadius = 10 bot.on('path_update', (results) => { console.log('[' + username + '] I can get there in ' + results.path.length + ' moves. Computation took ' + results.time.toFixed(2) + ' ms.') }) bot.on('goal_reached', (goal) => { console.log('[' + username + '] Here I am !') }) bot.on('chat', (username, message) => { if (username === bot.username) return const target = bot.players[username].entity if (message === 'follow') { bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalFollow(target, 5), true) } else if (message === 'avoid') { bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalInvert(new GoalFollow(target, 5)), true) } else if (message === 'stop') { bot.pathfinder.setGoal(null) } }) }) }) ================================================ FILE: examples/promise.js ================================================ // This example uses promises instead of events like "goal_reached" for a cleaner look const mineflayer = require('mineflayer') const pathfinder = require('mineflayer-pathfinder').pathfinder const Movements = require('mineflayer-pathfinder').Movements const { GoalNear } = require('mineflayer-pathfinder').goals const bot = mineflayer.createBot({ username: 'Player' }) // Load pathfinder bot.loadPlugin(pathfinder) bot.once('spawn', () => { // Set pathfinder movements const defaultMove = new Movements(bot) bot.pathfinder.setMovements(defaultMove) bot.on('chat', async (username, message) => { // If username is the same as the Bot's username, don't continue if (username === bot.username) return // Only continue when the message is "come" if (message === 'come') { const target = bot.players[username] ? bot.players[username].entity : null // If Player doesn't exist, don't continue if (!target) return bot.chat("I don't see you !") // Await pathfinder to complete the goal, then move to bot.chat and print "I've arrived !" await bot.pathfinder.goto(new GoalNear(target.position.x, target.position.y, target.position.z, 1)) bot.chat("I've arrived!") } }) }) ================================================ FILE: examples/tutorial/basic.js ================================================ /* * This example shows the usage of the GoalBlock * goal for mineflayer-pathfinder * * See a more detailed explanation here: * https://github.com/PrismarineJS/mineflayer-pathfinder/blob/master/examples/tutorial/goalsExplained.md * * Made by Jovan04 06/07/2023 */ const mineflayer = require('mineflayer') // import mineflayer, pathfinder, the Movements class, and our goal(s) const { pathfinder, Movements, goals: { GoalBlock } } = require('mineflayer-pathfinder') const bot = mineflayer.createBot({ // create our bot host: 'localhost', port: 25565, username: 'Pathfinder', auth: 'offline' }) bot.once('spawn', () => { bot.loadPlugin(pathfinder) // load pathfinder plugin into the bot const defaultMovements = new Movements(bot) // create a new instance of the `Movements` class bot.pathfinder.setMovements(defaultMovements) // set the bot's movements to the `Movements` we just created }) bot.on('chat', async (username, message) => { if (username === bot.username) return // make bot ignore its own messages if (message === 'go') { // this is our trigger message (only works on servers with vanilla chat) bot.chat('Going to my goal!') const myGoal = new GoalBlock(15, 3, 75) await bot.pathfinder.goto(myGoal) bot.chat('Arrived at my goal!') } }) ================================================ FILE: examples/tutorial/goalComposite.js ================================================ /* * This example shows the usage of the * GoalCompositeAny and GoalCompositeAll * goals for mineflayer-pathfinder * * See a more detailed explanation here: * https://github.com/PrismarineJS/mineflayer-pathfinder/blob/master/examples/tutorial/goalsExplained.md * * Made by Jovan04 06/07/2023 */ // import mineflayer & related libraries const mineflayer = require('mineflayer') const { pathfinder, Movements, goals: { GoalNear, GoalCompositeAny, GoalCompositeAll } } = require('mineflayer-pathfinder') // create mineflayer bot const bot = mineflayer.createBot({ host: 'localhost', port: 25565, version: '1.18.2', auth: 'offline', username: 'biffed' }) // load pathfinder plugin and set our bot's Movements bot.once('spawn', () => { bot.loadPlugin(pathfinder) bot.pathfinder.setMovements(new Movements(bot)) }) bot.on('chat', async (username, message) => { if (username === bot.username) return // create three separate GoalNear goals at different locations, with a range of 5; the bot needs to be within 5 blocks of a given goal to satisfy it const LapisGoal = new GoalNear(0, 1, 3, 5) const GoldGoal = new GoalNear(3, 1, -2, 5) const DiamondGoal = new GoalNear(-3, 1, -2, 5) const goalsArray = [LapisGoal, GoldGoal, DiamondGoal] if (message === 'GoalCompositeAny') { bot.chat('Traveling with GoalCompositeAny') // create a new GoalCompositeAny: see documentation for a more detailed explanation const goalAny = new GoalCompositeAny(goalsArray) // and travel to it await bot.pathfinder.goto(goalAny) bot.chat('Done traveling with GoalCompositeAny') } if (message === 'GoalCompositeAll') { bot.chat('Traveling with GoalCompositeAll') // create a new GoalCompositeAll: see documentation for a more detailed explanation const goalAll = new GoalCompositeAll(goalsArray) // and travel to it await bot.pathfinder.goto(goalAll) bot.chat('Done traveling with GoalCompositeAll') } }) ================================================ FILE: examples/tutorial/goalsExplained.md ================================================ # Mineflayer-Pathfinder: Goals ## A (more) detailed explanation by Jovan04 This page is an explanation about goals in mineflayer-pathfinder. A `Goal` is an instance of a class that allows the user to specify a location that they want a mineflayer bot to go to. Goals are the backbone of mineflayer because they provide an easy way to control your mineflayer bot with mineflayer-pathfinder. ### General Goals It's useful to think about goals in pathfinder as conditions that need to be fulfilled. For example, one of the most common goals is `GoalBlock`. As the [documentation](../../readme.md#goals) for the `GoalBlock` goal says: > One specific block that the player should stand inside at foot level Thus, we have our condition. In order to complete the `GoalBlock` goal, our bot needs to get its feet inside the specified block. Simple as that. That's great and all, but how do we use it? [Here's](./basic.js) a quick example. Let's walk through everything it does. First, we need to import mineflayer, as well as pathfinder and things related to it: ```js const mineflayer = require('mineflayer') const { pathfinder, Movements, goals:{ GoalBlock } } = require('mineflayer-pathfinder') ``` Here, we import a few things from pathfinder: * the pathfinder plugin itself * the Movements() class, which defines how our bot is allowed to move * the `goals` object, and from it the `GoalBlock` goal Next, we'll create our bot. This is just like creating any other Mineflayer bot: ```js const bot = mineflayer.createBot({ host: 'localhost', port: 25565, username: 'Pathfinder', auth: 'offline' }) ``` We'll also add a `spawn` event listener to load the pathfinder plugin and create the bot's Movements class: ```js bot.once('spawn', () => { bot.loadPlugin(pathfinder) // load pathfinder plugin into the bot const defaultMovements = new Movements(bot) // create a new instance of the `Movements` class bot.pathfinder.setMovements(defaultMovements) // set the bot's movements to the `Movements` we just created }) ``` The `Movements` class essentially tells the bot what moves it's allowed to make. This includes, but isn't limited to: What blocks (if any) it can place/break, whether it can pillar straight up, and how far it's allowed to fall. What we've done so far is mostly boilerplate; you'll probably use it every time you use pathfinder. Now, we'll create a chat listener that we'll use to make pathfinder move: ```js bot.on('chat', async (username, message) => { if (username === bot.username) return // make bot ignore its own messages if (message === 'go') { // this is our trigger message (only works on servers with vanilla chat) // our pathfinder code goes here! } }) ``` Now, let's go back to the `GoalBlock` goal. If we look at the documentation again, we can see that the GoalBlock takes three arguments: an `x` coordinate, a `y` coordinate, and a `z` coordinate, all integers. In order to use the `GoalBlock` goal, we would need to create a new instance of the `GoalBlock` class with those coordinates. If we wanted our bot to pathfind to the coordinates **15, 3, 75**, we could do that like this: ```js const myGoal = new GoalBlock(15, 3, 75) ``` Now that we have our goal, how do we use it? Simple! Pathfinder has a method for traveling to a goal, `goto`. We can access it through `bot.pathfinder.goto(goal)`, and we'll use the `await` Javascript keyword to make our code wait for the bot to finish walking to the goal beore continuing: ```js await bot.pathfinder.goto(myGoal) ``` We can also use `bot.chat` to have the bot tell us when it starts and finishes going to the goal. If we add that to the lines we just wrote, it can look something like this: ```js bot.chat('Going to my goal!') const myGoal = new GoalBlock(15, 3, 75) await bot.pathfinder.goto(myGoal) bot.chat('Arrived at my goal!') ``` Let's put those four lines inside our chat listener, likeso: ```js bot.on('chat', async (username, message) => { if (username === bot.username) return // make bot ignore its own messages if (message === 'go') { // this is our trigger message (only works on servers with vanilla chat) bot.chat('Going to my goal!') const myGoal = new GoalBlock(15, 3, 75) await bot.pathfinder.goto(myGoal) bot.chat('Arrived at my goal!') } }) ``` And now we're done! We can type `go` in chat, and the bot will walk to the coordinates we specified. Remember that `GoalBlock` makes the bot put its feet in the block we specified. You can view and download the full example script [here](./basic.js). And, there you have it. That's how you use the `GoalBlock` goal! Most of the other goals are used in a similar way, but with different arguments. You can look at [the documentation](../../readme.md#goals) for those. However, there are a few goals that are a little confusing. ### Composite Goals The Composite goals, `GoalCompositeAny` and `GoalCompositeAll`, are both quite different from most other goals. Instead of being standalone goals themselves, they allow you to combine other goals in interesting ways. They're called *compos*ite goals because they're *compos*ed of (or made up of) other goals. But before we can talk about the composite goals in more detail, we should talk about the `GoalNear` goal. The composite goals are made up of other goals, and `GoalNear` is a good example. `GoalNear` is almost the same as `GoalBlock`, but with one extra argument. Where `GoalBlock` only has `x, y, z`, `GoalNear` has `x, y, z, range`. The additional argument, `range`, specifies how far away from the target block the bot can be in order to still satisfy the goal. For example, the goal `GoalNear(15, 3, 75, 5)` would be satisfied once the bot is within 5 blocks of **15, 3, 75**. Now, let's set up our program to use composite goals. In the [composite goal example](./goalComposite.js), we make three goals: `LapisGoal`, `GoldGoal`, and `DiamondGoal`. They correspond to standing within 5 blocks of a Lapis block, a Gold block, and a Diamond block, respectively (see picture below): ```js const LapisGoal = new GoalNear(0, 1, 3, 5) // our bot needs to stand within 5 blocks of the point (0, 1, 3) in order to satisfy this goal (blue circle below) const GoldGoal = new GoalNear(3, 1, -2, 5) // our bot needs to stand within 5 blocks of the point (3, 1, -2) in order to satisfy this goal (yellow circle below) const DiamondGoal = new GoalNear(-3, 1, -2, 5) // our bot needs to stand within 5 blocks of the point (-3, 1, -2) in order to satisfy this goal (white circle below) ``` ![Diagram of the three goals](goalComposite-goals.png) #### GoalCompositeAny The first type of composite goal is called `GoalCompositeAny`. Being a composite goal, `GoalCompositeAny` is a goal made up of other goals. In order for your bot to complete `GoalCompositeAny`, it needs to satisfy *any one* of the contained goals. Above, we made three goals (`LapisGoal`, `GoldGoal`, and `DiamondGoal`). Let's put them into an array: ```js const goalsArray = [LapisGoal, GoldGoal, DiamondGoal] // array containing all three of our goals; we'll use this array in our `GoalCompositeAny` goal ``` And create a new `GoalCompositeAny` with that array: ```js const goalAny = new GoalCompositeAny(goalsArray) ``` Now, just like any other goal, we can tell pathfinder to `goto` our new `GoalCompositeAny`: ```js await bot.pathfinder.goto(goalAny) ``` The `GoalCompositeAny` is completed when the bot completes any one of the goals it was created with. For our example above, `goalAny` will be completed when the bot completes any one of `LapisGoal`, `GoldGoal`, or `DiamondGoal`. In other words, `goalAny` will be completed when the bot's location is inside the blue circle *or* the yellow circle *or* the white circle (see image above). #### GoalCompositeAll The other type of composite goal is called `GoalCompositeAll`. Being a composite goal, `GoalCompositeAll` is a goal made up of other goals. In order for your bot to complete `GoalCompositeAll`, it needs to satisfy *all* of the contained goals. Above, we made three goals (`LapisGoal`, `GoldGoal`, and `DiamondGoal`). Let's put them into an array: ```js const goalsArray = [LapisGoal, GoldGoal, DiamondGoal] // array containing all three of our goals; we'll use this array in our `GoalCompositeAll` goal ``` And create a new `GoalCompositeAll` with that array: ```js const goalAll = new GoalCompositeAll(goalsArray) ``` Now, just like any other goal, we can tell pathfinder to `goto` our new `GoalCompositeAll`: ```js await bot.pathfinder.goto(goalAll) ``` The `GoalCompositeAll` is completed when the bot completes all of the goals it was created with. For our example above, `goalAll` will be completed when the bot completes all of `LapisGoal`, `GoldGoal`, or `DiamondGoal`. In other words, `goalAll` will be completed when the bot's location is inside the blue circle *and* the yellow circle *and* the white circle (see image above). It's important to clarify that the bot needs to be in all three circles at the same time. Going from one circle to another to the last is not a valid path to complete the goal. And there you have it! That's a basic introduction to using goals in mineflayer-pathfinder. If any of this was confusing, or you'd like help with something more complicated, feel free to join the [PrismarineJS Discord server](https://discord.gg/GsEFRM8). We're always happy to provide help for mineflayer and other PrismarineJS libraries. ================================================ FILE: history.md ================================================ # History # 2.4.5 * [Fix block update resets for optimized paths (@m000z0rz)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/330) * [reword .stop() for more clarity (@Jovan-04)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/327) * [add more detailed tutorial for mineflayer-pathfinder goals (@Jovan-04)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/326) * [Fixed typo: "physicsTick" (@FreezeEngine)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/324) # 2.4.4 * [Update readme.md (@Vinciepincie)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/320) * Fix file linting (@IceTank) * Fix possible reference error for block updates (@IceTank) * [Fix a bunch of spelling/grammar errors (@182exe)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/316) # 2.4.3 * [Change canOpenDoors default value to false because its buggy (@IceTank)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/318) * [Added missing constructor definitions for GoalCompositeAny and GoalCompositeAll (@rutexd)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/315) * [Added "sneak" on interact blocks to avoid open it (@sefirosweb)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/314) * [Block face position fix (@WhoTho)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/312) * [Remove mcData param in movements.js (@rtm516)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/311) # 2.4.2 * Fix pathfinder trying to make a parkour jump that fails most times * Fix pathfinder not going below level 0 * [Fix wheat not being break able (@maximmasiutin)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/303) * [Add parameter typing to Composite goals (@Ic3Tank)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/302) * [Fix GoalLookAtBlock documentation (@Ic3Tank)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/301) # 2.4.1 * [Made some Goal methods none abstract (@IceTank)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/298) # 2.4.0 * [mcData to registry refactoring (@Epirito)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/293) # 2.3.3 * [Add missing types on GoalPlaceBlock and GoalLookAtBlock (@IceTank)](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/289) # 2.3.2 * Revert broken goto implementation update. # 2.3.1 * Fix reference error in `getNumEntitiesAt` (https://github.com/PrismarineJS/mineflayer-pathfinder/commit/2e7b3daff2ee5fa0aaf52db4553f769189b8d03f) # 2.3.0 * [Add entity Avoidance Feature](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/281) * [Fix bugs in movements.js](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/276) * [Update Dependencies](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/277) * [Fix issue with starting paths](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/280) # 2.2.0 * [Add events typings](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/266) * [Force look at blocks when breaking them](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/274) * [Fix bot sometimes not triggering path end correctly](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/268) * [Fix missing null check at block update](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/269) * [Bump mocha to 10.x](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/265) # 2.1.1 * Fix GitHub action for publishing # 2.1.0 * [Add automated tests](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/262) * [Add getPathFromTo function](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/255) * [Fix path optimization check](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/254) * [Bumb minecraft data to version 3.x](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/262) * [Add goal chaining example](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/256) # 2.0.0 * [Remove callbacks](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/248) * [Export GoalLookAtBlock and deprecate GoalBreakBlock](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/249) # 1.10.0 * [Add exclusion area mechanism](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/220) * [Add movement class example](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/247) * [Add infiniteLiquidDropdownDistance to movements](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/211) * [Added dontMineUnderFallingBlock to movements](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/239/files) * [Add ability to open fence gates](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/217) * [Bump mineflayer to 4.0.0](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/245) * [Throw error in goto when stop() is called](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/240) * [Update README.md](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/246) * Typing fixes: * [tickTimeout](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/241) * [GoalLookAtBlock](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/238) * [Fix dynamic goals with entities](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/206) * [Fix default scaffolding blocks](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/243) * [Fix event handler when stop() is called](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/184) # 1.9.1 * [Fixed unhandled promise rejection introduced in 1.0.0](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/235#event-5854609665) # 1.9.0 * [Fixed floor check](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/208) * [Avoid cobwebs](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/210) * [Fixed diagonal move not considering collision height when jumping up diagonally](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/216) * [Fixed movements for older versions](https://github.com/PrismarineJS/mineflayer-pathfinder/pull/226) # 1.8.0 * Fixed placeBlock example * Fixed Readme typos * Fixed bot placing wrong blocks as scaffolding * Fixed GoalNearXZ * Fixed typings * Fixed index.d.ts compile errors # 1.7.0 * Add GoalNearXZ * Improve docs # 1.6.3 * Add setGoal null to typings * Add safeOrBreak check to getMoveDiagonal * Fix reference to LOSWhenPlacingBlocks * Fixed raycasting not considering block face * Add GoalPlaceBlock typing * Add placeBlock.js example * Add callback.js and promise.js example * Fix reference error in GoalPlaceBlock * Function to stop path finding when safe # 1.6.2 * Fix swimming in lava * Fix TypeScript headers * Add +1 to movement cost when going forward and up 1 block * Fix bot trying to go underwater * Add `path_reset` event # 1.6.1 * Add option to limit search range * Expose tickTimeout # 1.6.0 * Add GoalPlaceBlock * Fix various parkour moves * Fix goto # 1.5.0 * Improve diagonal movements (add up/down) * Expose A* context in result * Fix fences * Fix carpets # 1.4.0 * Legit bot bridging (with sneak) * Fixed bug for detect when mining is stopped correctly * Fix GoalGetToBlock ## 1.3.7 * Promisified goto ## 1.3.0 * Add ladder support * Add ability to drop in water from high places * Improve movement execution ## 1.2.0 * Use physics to predict motion and choose best controls * Add more parkour moves * Sprint and sprint-jump over gaps ## 1.1.2 * Set every non diggable block automatically from mcdata * Fix jumps in snow ## 1.1.1 * Fix 1x1 towering * Fix path starting on shorter blocks ## 1.1.0 * Fixed crash with null positions * API in the readme * Expose movements and goal ## 1.0.12 * Added `canDig` movements variable * Added `goto(goal, cb)` function ## 1.0.11 * Added `goal_updated` event * Movements are now initialized by default * Fixed Typescript headers * Fixed bugs with block height when jumping ## 1.0.10 * Fixed "cannot read property 'shapes' of null" bug * Exposed `thinkTimeout` pathfinder variable ## 1.0.9 * Added simple postprocessing fallback for unsuitable positions ## 1.0.8 * Fixed null pointer exception for "getPositionOnTopOf" ## 1.0.7 * Improved post processing for standing on more block types * Improved tool selection when breaking blocks * Retrieve player state from Prismarine-Physics * Fixed bug with parkour cooldown * Added Typescript headers * Fixed bug with clearing controls while recalculating path * Removed path recalculation detection radius ## 1.0.6 * Added basic parkour movements * Movement nodes are now stored as classes * Astar algorithm is now stored as a class * Improved blocks-to-break estimation in the path * Fixed 12.x Node.js compatibility in example bot ## 1.0.5 * Added multiple bot example * Added experimental "free-motion" * Added composite goal * Added `isMoving()` function * Added `isMining()` function * Added `isBuilding()` function * Added `isThinking()` function ## 1.0.4 * Paths are now recalculated on chunk loading to fix long paths * Minor bug fixes * Moved scaffolding blocks from index.js to movements.js internally * Updated readme todo list * Added 1x1 tower creation ## 1.0.3 * Fixed `goal_reached` not being called if bot is already at the goal * Control state is cleared when path is reset * Fixed example bot code in readme * Improved readme * Fixed bug with place/dig logic * Added swimming support ## 1.0.2 * Exposed goals and movements classes * Fixed bugs with bot stopping incorrectly * Improved readme * Added performance benchmarks * Added build CI support ## 1.0.1 * Added deployment CI support * Added standard * Fixed bug with not canceling digging when resetting path * Fixed undefined pos error * Added configurable fall height * Added dynamic goals * Added automatic path recalculation * Added 1x1 digging holes * Added more movement abilities * Added internal scaffolding block count ## 1.0.0 * Initial release ================================================ FILE: index.d.ts ================================================ import { Bot } from 'mineflayer'; import { IndexedData } from 'minecraft-data'; import { Item } from 'prismarine-item'; import { Vec3 } from 'vec3'; import { Block } from 'prismarine-block'; import { Entity } from 'prismarine-entity'; import { World } from 'prismarine-world' import AStar from './lib/astar'; declare module 'mineflayer-pathfinder' { export function pathfinder(bot: Bot): void; export interface Pathfinder { thinkTimeout: number; /** ms, amount of thinking per tick (max 50 ms) */ tickTimeout: number; readonly goal: goals.Goal | null; readonly movements: Movements; bestHarvestTool(block: Block): Item | null; getPathTo( movements: Movements, goal: goals.Goal, timeout?: number ): ComputedPath; getPathFromTo( movements: Movements, startPos: Vec3 | null, goal: goals.Goal, options?: { optimizePath?: boolean, resetEntityIntersects?: boolean, timeout?: number, tickTimeout?: number, searchRadius?: number, startMove?: Move } ): IterableIterator<{ result: ComputedPath, astarContext: AStar }> setGoal(goal: goals.Goal | null, dynamic?: boolean): void; setMovements(movements: Movements): void; goto(goal: goals.Goal, callback?: Callback): Promise; stop(): void; isMoving(): boolean; isMining(): boolean; isBuilding(): boolean; } export namespace goals { export abstract class Goal { public abstract heuristic(node: Move): number; public abstract isEnd(node: Move): boolean; public hasChanged(): boolean; public isValid(): boolean; } export class GoalBlock extends Goal { public constructor(x: number, y: number, z: number); public x: number; public y: number; public z: number; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalNear extends Goal { public constructor(x: number, y: number, z: number, range: number); public x: number; public y: number; public z: number; public rangeSq: number; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalXZ extends Goal { public constructor(x: number, z: number); public x: number; public z: number; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalNearXZ extends Goal { public constructor(x: number, z: number, range: number); public x: number; public z: number; public rangeSq: number; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalY extends Goal { public constructor(y: number); public y: number; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalGetToBlock extends Goal { public constructor(x: number, y: number, z: number); public x: number; public y: number; public z: number; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalCompositeAny extends Goal { public constructor(goals: T[] = []); public goals: T[]; public push(goal: Goal): void; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalCompositeAll extends Goal { public constructor(goals: T[] = []); public goals: T[]; public push(goal: Goal): void; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalInvert extends Goal { public constructor(goal: Goal); public goal: Goal; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalFollow extends Goal { public constructor(entity: Entity, range: number); public x: number; public y: number; public z: number; public entity: Entity; public rangeSq: number; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalPlaceBlock extends Goal { public options: { range: number; LOS: boolean; faces: [Vec3, Vec3, Vec3, Vec3, Vec3, Vec3]; facing: number; half: boolean; } public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; public constructor(pos: Vec3, world: World, options: GoalPlaceBlockOptions) } export class GoalLookAtBlock extends Goal { public constructor(pos: Vec3, world: World, options?: { reach?: number, entityHeight?: number }) public pos: Vec3; public reach: number; public entityHeight: number; public world: World; public heuristic(node: Move): number; public isEnd(node: Move): boolean; public hasChanged(): boolean; } export class GoalBreakBlock extends GoalLookAtBlock {} } export class Movements { public constructor(bot: Bot); public bot: Bot; public canDig: boolean; public canOpenDoors: boolean; public dontCreateFlow: boolean; public dontMineUnderFallingBlock: boolean; public allow1by1towers: boolean; public allowFreeMotion: boolean; public allowParkour: boolean; public allowSprinting: boolean; /** * Test for entities that may obstruct path or prevent block placement. Grabs updated entities every new path */ public allowEntityDetection: boolean; /** * Set of entities (by mcdata name) to completely avoid when using entity detection */ public entitiesToAvoid: Set; /** * Set of entities (by mcdata name) to ignore when using entity detection */ public passableEntities: Set; /** * Set of blocks (by mcdata name) that pathfinder should not attempt to place blocks or 'right click' on */ public interactableBlocks: Set; public blocksCantBreak: Set; public blocksToAvoid: Set; public liquids: Set; public gravityBlocks: Set; public climbables: Set public emptyBlocks: Set public replaceables: Set public fences: Set public carpets: Set public openable: Set public scafoldingBlocks: number[]; public maxDropDown: number; public infiniteLiquidDropdownDistance: boolean; public digCost: number; public placeCost: number; /** * Extra cost multiplier for moving through an entity hitbox (besides passable ones). */ public entityCost: number; /** Exclusion Area that adds extra cost or prevents the bot from stepping onto positions included. * @example * ```js movements.exclusionAreas = [(block) => { return block.type === someIdType ? 100 : 0 // Prevents the bot from breaking a specific block. By adding 100 to the cost. }, (block) => { return someVec3Pos.distanceTo(block.position) < 5 ? 100 : 0 // Prevents the bot from getting near to a specific location }] ``` */ public exclusionAreasStep: [(block: SafeBlock) => number]; /** * Exclusion area for blocks to break. Works in the same way as {@link exclusionAreasStep} does. */ public exclusionAreasBreak: [(block: SafeBlock) => number]; /** * Exclusion area for placing blocks. Note only works for positions not block values as placed blocks are determined by the bots inventory content. Works in the same way as {@link exclusionAreasStep} does. */ public exclusionAreasPlace: [(block: SafeBlock) => number]; /** * A dictionary of the number of entities intersecting each floored block coordinate. * Updated automatically each path but, you may mix in your own entries before calculating a path if desired (generally for testing). * To prevent this from being cleared automatically before generating a path see getPathFromTo() * formatted entityIntersections['x,y,z'] = #ents */ public entityIntersections: {string: number}; public exclusionPlace(block: SafeBlock): number; public exclusionStep(block: SafeBlock): number; public exclusionBreak(block: SafeBlock): number; public countScaffoldingItems(): number; public getScaffoldingItem(): Item | null; public clearCollisionIndex(): void; /** * Finds blocks intersected by entity bounding boxes * and sets the number of ents intersecting in a dict. * Ignores entities that do not affect block placement */ public updateCollisionIndex(): void; /** * Gets number of entities who's bounding box intersects the node + offset * @param {import('vec3').Vec3} pos node position * @param {number} dx X axis offset * @param {number} dy Y axis offset * @param {number} dz Z axis offset * @returns {number} Number of entities intersecting block */ public getNumEntitiesAt(pos: Vec3, dx: number, dy: number, dz: number): number; public getBlock(pos: Vec3, dx: number, dy: number, dz: number): SafeBlock; public safeToBreak(block: SafeBlock): boolean; public safeOrBreak(block: SafeBlock): number; public getMoveJumpUp(node: Move, dir: XZCoordinates, neighbors: Move[]): void; public getMoveForward(node: Move, dir: XZCoordinates, neighbors: Move[]): void; public getMoveDiagonal(node: Move, dir: XZCoordinates, neighbors: Move[]): void; public getMoveDropDown(node: Move, dir: XZCoordinates, neighbors: Move[]): void; public getMoveParkourForward(node: Move, dir: XZCoordinates, neighbors: Move[]): void; public getMoveJumpUp(node: Move, dir: XZCoordinates, neighbors: Move[]): void; public getMoveUp(node: Move, neighbors: Move[]): void; public getMoveDown(node: Move, neighbors: Move[]): void; public getLandingBlock(node: Move, dir: XZCoordinates): SafeBlock; public getNeighbors(node: Move): Move[]; } // this is a class, but its not exported so we use an interface export interface Move extends XYZCoordinates { remainingBlocks: number; cost: number; toBreak: Move[]; toPlace: Move[]; parkour: boolean; hash: string; } type Callback = (error?: Error) => void; interface PathBase { cost: number; time: number; visitedNodes: number; generatedNodes: number; path: Move[]; } export interface ComputedPath extends PathBase { status: 'noPath' | 'timeout' | 'success'; } export interface PartiallyComputedPath extends PathBase { status: 'noPath' | 'timeout' | 'success' | 'partial'; } export interface XZCoordinates { x: number; z: number; } export interface XYZCoordinates extends XZCoordinates { y: number; } export interface SafeBlock extends Block { safe: boolean; physical: boolean; liquid: boolean; height: number; replaceable: boolean; climbable: boolean; openable: boolean; } export interface GoalPlaceBlockOptions { range: number; LOS: boolean; faces: Vec3[]; facing: 'north' | 'east' | 'south' | 'west' | 'up' | 'down'; } } declare module 'mineflayer' { interface BotEvents { goal_reached: (goal: Goal) => void; path_update: (path: PartiallyComputedPath) => void; goal_updated: (goal: Goal, dynamic: boolean) => void; path_reset: ( reason: 'goal_updated' | 'movements_updated' | 'block_updated' | 'chunk_loaded' | 'goal_moved' | 'dig_error' | 'no_scaffolding_blocks' | 'place_error' | 'stuck' ) => void; path_stop: () => void; } interface Bot { pathfinder: Pathfinder } } ================================================ FILE: index.js ================================================ const { performance } = require('perf_hooks') const AStar = require('./lib/astar') const Move = require('./lib/move') const Movements = require('./lib/movements') const gotoUtil = require('./lib/goto') const Lock = require('./lib/lock') const Vec3 = require('vec3').Vec3 const Physics = require('./lib/physics') const nbt = require('prismarine-nbt') const interactableBlocks = require('./lib/interactable.json') function inject (bot) { const waterType = bot.registry.blocksByName.water.id const ladderId = bot.registry.blocksByName.ladder.id const vineId = bot.registry.blocksByName.vine.id let stateMovements = new Movements(bot) let stateGoal = null let astarContext = null let astartTimedout = false let dynamicGoal = false let path = [] let pathUpdated = false let digging = false let placing = false let placingBlock = null let lastNodeTime = performance.now() let returningPos = null let stopPathing = false const physics = new Physics(bot) const lockPlaceBlock = new Lock() const lockEquipItem = new Lock() const lockUseBlock = new Lock() bot.pathfinder = {} bot.pathfinder.thinkTimeout = 5000 // ms bot.pathfinder.tickTimeout = 40 // ms, amount of thinking per tick (max 50 ms) bot.pathfinder.searchRadius = -1 // in blocks, limits of the search area, -1: don't limit the search bot.pathfinder.enablePathShortcut = false // disabled by default as it can cause bugs in specific configurations bot.pathfinder.LOSWhenPlacingBlocks = true bot.pathfinder.bestHarvestTool = (block) => { const availableTools = bot.inventory.items() const effects = bot.entity.effects let fastest = Number.MAX_VALUE let bestTool = null for (const tool of availableTools) { const enchants = (tool && tool.nbt) ? nbt.simplify(tool.nbt).Enchantments : [] const digTime = block.digTime(tool ? tool.type : null, false, false, false, enchants, effects) if (digTime < fastest) { fastest = digTime bestTool = tool } } return bestTool } bot.pathfinder.getPathTo = (movements, goal, timeout) => { const generator = bot.pathfinder.getPathFromTo(movements, bot.entity.position, goal, { timeout }) const { value: { result, astarContext: context } } = generator.next() astarContext = context return result } bot.pathfinder.getPathFromTo = function * (movements, startPos, goal, options = {}) { const optimizePath = options.optimizePath ?? true const resetEntityIntersects = options.resetEntityIntersects ?? true const timeout = options.timeout ?? bot.pathfinder.thinkTimeout const tickTimeout = options.tickTimeout ?? bot.pathfinder.tickTimeout const searchRadius = options.searchRadius ?? bot.pathfinder.searchRadius let start if (options.startMove) { start = options.startMove } else { const p = startPos.floored() const dy = startPos.y - p.y const b = bot.blockAt(p) // The block we are standing in // Offset the floored bot position by one if we are standing on a block that has not the full height but is solid const offset = (b && dy > 0.001 && bot.entity.onGround && !stateMovements.emptyBlocks.has(b.type)) ? 1 : 0 start = new Move(p.x, p.y + offset, p.z, movements.countScaffoldingItems(), 0) } if (movements.allowEntityDetection) { if (resetEntityIntersects) { movements.clearCollisionIndex() } movements.updateCollisionIndex() } const astarContext = new AStar(start, movements, goal, timeout, tickTimeout, searchRadius) let result = astarContext.compute() if (optimizePath) result.path = postProcessPath(result.path) yield { result, astarContext } while (result.status === 'partial') { result = astarContext.compute() if (optimizePath) result.path = postProcessPath(result.path) yield { result, astarContext } } } Object.defineProperties(bot.pathfinder, { goal: { get () { return stateGoal } }, movements: { get () { return stateMovements } } }) function detectDiggingStopped () { digging = false bot.removeAllListeners('diggingAborted', detectDiggingStopped) bot.removeAllListeners('diggingCompleted', detectDiggingStopped) } function resetPath (reason, clearStates = true) { if (!stopPathing && path.length > 0) bot.emit('path_reset', reason) path = [] if (digging) { bot.on('diggingAborted', detectDiggingStopped) bot.on('diggingCompleted', detectDiggingStopped) bot.stopDigging() } placing = false pathUpdated = false astarContext = null lockEquipItem.release() lockPlaceBlock.release() lockUseBlock.release() stateMovements.clearCollisionIndex() if (clearStates) bot.clearControlStates() if (stopPathing) return stop() } bot.pathfinder.setGoal = (goal, dynamic = false) => { stateGoal = goal dynamicGoal = dynamic bot.emit('goal_updated', goal, dynamic) resetPath('goal_updated') } bot.pathfinder.setMovements = (movements) => { stateMovements = movements resetPath('movements_updated') } bot.pathfinder.isMoving = () => path.length > 0 bot.pathfinder.isMining = () => digging bot.pathfinder.isBuilding = () => placing bot.pathfinder.goto = (goal) => { return gotoUtil(bot, goal) } bot.pathfinder.stop = () => { stopPathing = true } bot.on('physicsTick', monitorMovement) function postProcessPath (path) { for (let i = 0; i < path.length; i++) { const curPoint = path[i] if (curPoint.toBreak.length > 0 || curPoint.toPlace.length > 0) break const b = bot.blockAt(new Vec3(curPoint.x, curPoint.y, curPoint.z)) if (b && (b.type === waterType || ((b.type === ladderId || b.type === vineId) && i + 1 < path.length && path[i + 1].y < curPoint.y))) { curPoint.x = Math.floor(curPoint.x) + 0.5 curPoint.y = Math.floor(curPoint.y) curPoint.z = Math.floor(curPoint.z) + 0.5 continue } let np = getPositionOnTopOf(b) if (np === null) np = getPositionOnTopOf(bot.blockAt(new Vec3(curPoint.x, curPoint.y - 1, curPoint.z))) if (np) { curPoint.x = np.x curPoint.y = np.y curPoint.z = np.z } else { curPoint.x = Math.floor(curPoint.x) + 0.5 curPoint.y = curPoint.y - 1 curPoint.z = Math.floor(curPoint.z) + 0.5 } } if (!bot.pathfinder.enablePathShortcut || stateMovements.exclusionAreasStep.length !== 0 || path.length === 0) return path const newPath = [] let lastNode = bot.entity.position for (let i = 1; i < path.length; i++) { const node = path[i] if (Math.abs(node.y - lastNode.y) > 0.5 || node.toBreak.length > 0 || node.toPlace.length > 0 || !physics.canStraightLineBetween(lastNode, node)) { newPath.push(path[i - 1]) lastNode = path[i - 1] } } newPath.push(path[path.length - 1]) return newPath } function pathFromPlayer (path) { if (path.length === 0) return let minI = 0 let minDistance = 1000 for (let i = 0; i < path.length; i++) { const node = path[i] if (node.toBreak.length !== 0 || node.toPlace.length !== 0) break const dist = bot.entity.position.distanceSquared(node) if (dist < minDistance) { minDistance = dist minI = i } } // check if we are between 2 nodes const n1 = path[minI] // check if node already reached const dx = n1.x - bot.entity.position.x const dy = n1.y - bot.entity.position.y const dz = n1.z - bot.entity.position.z const reached = Math.abs(dx) <= 0.35 && Math.abs(dz) <= 0.35 && Math.abs(dy) < 1 if (minI + 1 < path.length && n1.toBreak.length === 0 && n1.toPlace.length === 0) { const n2 = path[minI + 1] const d2 = bot.entity.position.distanceSquared(n2) const d12 = n1.distanceSquared(n2) minI += d12 > d2 || reached ? 1 : 0 } path.splice(0, minI) } function isPositionNearPath (pos, path) { let prevNode = null for (const node of path) { let comparisonPoint = null if ( prevNode === null || ( Math.abs(prevNode.x - node.x) <= 2 && Math.abs(prevNode.y - node.y) <= 2 && Math.abs(prevNode.z - node.z) <= 2 ) ) { // Unoptimized path, or close enough to last point // to just check against the current point comparisonPoint = node } else { // Optimized path - the points are far enough apart // that we need to check the space between them too // First, a quick check - if point it outside the path // segment's AABB, then it isn't near. const minBound = prevNode.min(node) const maxBound = prevNode.max(node) if ( pos.x - 0.5 < minBound.x - 1 || pos.x - 0.5 > maxBound.x + 1 || pos.y - 0.5 < minBound.y - 2 || pos.y - 0.5 > maxBound.y + 2 || pos.z - 0.5 < minBound.z - 1 || pos.z - 0.5 > maxBound.z + 1 ) { continue } comparisonPoint = closestPointOnLineSegment(pos, prevNode, node) } const dx = Math.abs(comparisonPoint.x - pos.x - 0.5) const dy = Math.abs(comparisonPoint.y - pos.y - 0.5) const dz = Math.abs(comparisonPoint.z - pos.z - 0.5) if (dx <= 1 && dy <= 2 && dz <= 1) return true prevNode = node } return false } function closestPointOnLineSegment (point, segmentStart, segmentEnd) { const segmentLength = segmentEnd.minus(segmentStart).norm() if (segmentLength === 0) { return segmentStart } // t is like an interpolation from segmentStart to segmentEnd // for the closest point on the line let t = (point.minus(segmentStart)).dot(segmentEnd.minus(segmentStart)) / segmentLength // bound t to be on the segment t = Math.max(0, Math.min(1, t)) return segmentStart.plus(segmentEnd.minus(segmentStart).scaled(t)) } // Return the average x/z position of the highest standing positions // in the block. function getPositionOnTopOf (block) { if (!block || block.shapes.length === 0) return null const p = new Vec3(0.5, 0, 0.5) let n = 1 for (const shape of block.shapes) { const h = shape[4] if (h === p.y) { p.x += (shape[0] + shape[3]) / 2 p.z += (shape[2] + shape[5]) / 2 n++ } else if (h > p.y) { n = 2 p.x = 0.5 + (shape[0] + shape[3]) / 2 p.y = h p.z = 0.5 + (shape[2] + shape[5]) / 2 } } p.x /= n p.z /= n return block.position.plus(p) } /** * Stop the bot's movement and recenter to the center off the block when the bot's hitbox is partially beyond the * current blocks dimensions. */ function fullStop () { bot.clearControlStates() // Force horizontal velocity to 0 (otherwise inertia can move us too far) // Kind of cheaty, but the server will not tell the difference bot.entity.velocity.x = 0 bot.entity.velocity.z = 0 const blockX = Math.floor(bot.entity.position.x) + 0.5 const blockZ = Math.floor(bot.entity.position.z) + 0.5 // Make sure our bounding box don't collide with neighboring blocks // otherwise recenter the position if (Math.abs(bot.entity.position.x - blockX) > 0.2) { bot.entity.position.x = blockX } if (Math.abs(bot.entity.position.z - blockZ) > 0.2) { bot.entity.position.z = blockZ } } function moveToEdge (refBlock, edge) { // If allowed turn instantly should maybe be a bot option const allowInstantTurn = false function getViewVector (pitch, yaw) { const csPitch = Math.cos(pitch) const snPitch = Math.sin(pitch) const csYaw = Math.cos(yaw) const snYaw = Math.sin(yaw) return new Vec3(-snYaw * csPitch, snPitch, -csYaw * csPitch) } // Target viewing direction while approaching edge // The Bot approaches the edge while looking in the opposite direction from where it needs to go // The target Pitch angle is roughly the angle the bot has to look down for when it is in the position // to place the next block const targetBlockPos = refBlock.offset(edge.x + 0.5, edge.y, edge.z + 0.5) const targetPosDelta = bot.entity.position.clone().subtract(targetBlockPos) const targetYaw = Math.atan2(-targetPosDelta.x, -targetPosDelta.z) const targetPitch = -1.421 const viewVector = getViewVector(targetPitch, targetYaw) // While the bot is not in the right position rotate the view and press back while crouching if (bot.entity.position.distanceTo(refBlock.clone().offset(edge.x + 0.5, 1, edge.z + 0.5)) > 0.4) { bot.lookAt(bot.entity.position.offset(viewVector.x, viewVector.y, viewVector.z), allowInstantTurn) bot.setControlState('sneak', true) bot.setControlState('back', true) return false } bot.setControlState('back', false) return true } function moveToBlock (pos) { // minDistanceSq = Min distance sqrt to the target pos were the bot is centered enough to place blocks around him const minDistanceSq = 0.2 * 0.2 const targetPos = pos.clone().offset(0.5, 0, 0.5) if (bot.entity.position.distanceSquared(targetPos) > minDistanceSq) { bot.lookAt(targetPos) bot.setControlState('forward', true) return false } bot.setControlState('forward', false) return true } function stop () { stopPathing = false stateGoal = null path = [] bot.emit('path_stop') fullStop() } bot.on('blockUpdate', (oldBlock, newBlock) => { if (!oldBlock || !newBlock) return if (isPositionNearPath(oldBlock.position, path) && oldBlock.type !== newBlock.type) { resetPath('block_updated', false) } }) bot.on('chunkColumnLoad', (chunk) => { // Reset only if the new chunk is adjacent to a visited chunk if (astarContext) { const cx = chunk.x >> 4 const cz = chunk.z >> 4 if (astarContext.visitedChunks.has(`${cx - 1},${cz}`) || astarContext.visitedChunks.has(`${cx},${cz - 1}`) || astarContext.visitedChunks.has(`${cx + 1},${cz}`) || astarContext.visitedChunks.has(`${cx},${cz + 1}`)) { resetPath('chunk_loaded', false) } } }) function monitorMovement () { // Test freemotion if (stateMovements && stateMovements.allowFreeMotion && stateGoal && stateGoal.entity) { const target = stateGoal.entity if (physics.canStraightLine([target.position])) { bot.lookAt(target.position.offset(0, 1.6, 0)) if (target.position.distanceSquared(bot.entity.position) > stateGoal.rangeSq) { bot.setControlState('forward', true) } else { bot.clearControlStates() } return } } if (stateGoal) { if (!stateGoal.isValid()) { stop() } else if (stateGoal.hasChanged()) { resetPath('goal_moved', false) } } if (astarContext && astartTimedout) { const results = astarContext.compute() results.path = postProcessPath(results.path) pathFromPlayer(results.path) bot.emit('path_update', results) path = results.path astartTimedout = results.status === 'partial' } if (bot.pathfinder.LOSWhenPlacingBlocks && returningPos) { if (!moveToBlock(returningPos)) return returningPos = null } if (path.length === 0) { lastNodeTime = performance.now() if (stateGoal && stateMovements) { if (stateGoal.isEnd(bot.entity.position.floored())) { if (!dynamicGoal) { bot.emit('goal_reached', stateGoal) stateGoal = null fullStop() } } else if (!pathUpdated) { const results = bot.pathfinder.getPathTo(stateMovements, stateGoal) bot.emit('path_update', results) path = results.path astartTimedout = results.status === 'partial' pathUpdated = true } } } if (path.length === 0) { return } let nextPoint = path[0] const p = bot.entity.position // Handle digging if (digging || nextPoint.toBreak.length > 0) { if (!digging && bot.entity.onGround) { digging = true const b = nextPoint.toBreak.shift() const block = bot.blockAt(new Vec3(b.x, b.y, b.z), false) const tool = bot.pathfinder.bestHarvestTool(block) fullStop() const digBlock = () => { bot.dig(block, true) .catch(_ignoreError => { resetPath('dig_error') }) .then(function () { lastNodeTime = performance.now() digging = false }) } if (!tool) { digBlock() } else { bot.equip(tool, 'hand') .catch(_ignoreError => {}) .then(() => digBlock()) } } return } // Handle block placement // TODO: sneak when placing or make sure the block is not interactive if (placing || nextPoint.toPlace.length > 0) { if (!placing) { placing = true placingBlock = nextPoint.toPlace.shift() fullStop() } // Open gates or doors if (placingBlock?.useOne) { if (!lockUseBlock.tryAcquire()) return bot.activateBlock(bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z))).then(() => { lockUseBlock.release() placingBlock = nextPoint.toPlace.shift() }, err => { console.error(err) lockUseBlock.release() }) return } const block = stateMovements.getScaffoldingItem() if (!block) { resetPath('no_scaffolding_blocks') return } if (bot.pathfinder.LOSWhenPlacingBlocks && placingBlock.y === bot.entity.position.floored().y - 1 && placingBlock.dy === 0) { if (!moveToEdge(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z), new Vec3(placingBlock.dx, 0, placingBlock.dz))) return } let canPlace = true if (placingBlock.jump) { bot.setControlState('jump', true) canPlace = placingBlock.y + 1 < bot.entity.position.y } if (canPlace) { if (!lockEquipItem.tryAcquire()) return bot.equip(block, 'hand') .then(function () { lockEquipItem.release() const refBlock = bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z), false) if (!lockPlaceBlock.tryAcquire()) return if (interactableBlocks.includes(refBlock.name)) { bot.setControlState('sneak', true) } bot.placeBlock(refBlock, new Vec3(placingBlock.dx, placingBlock.dy, placingBlock.dz)) .then(function () { // Dont release Sneak if the block placement was not successful bot.setControlState('sneak', false) if (bot.pathfinder.LOSWhenPlacingBlocks && placingBlock.returnPos) returningPos = placingBlock.returnPos.clone() }) .catch(_ignoreError => { resetPath('place_error') }) .then(() => { lockPlaceBlock.release() placing = false lastNodeTime = performance.now() }) }) .catch(_ignoreError => {}) } return } let dx = nextPoint.x - p.x const dy = nextPoint.y - p.y let dz = nextPoint.z - p.z if (Math.abs(dx) <= 0.35 && Math.abs(dz) <= 0.35 && Math.abs(dy) < 1) { // arrived at next point lastNodeTime = performance.now() if (stopPathing) { stop() return } path.shift() if (path.length === 0) { // done // If the block the bot is standing on is not a full block only checking for the floored position can fail as // the distance to the goal can get greater then 0 when the vector is floored. if (!dynamicGoal && stateGoal && (stateGoal.isEnd(p.floored()) || stateGoal.isEnd(p.floored().offset(0, 1, 0)))) { bot.emit('goal_reached', stateGoal) stateGoal = null } fullStop() return } // not done yet nextPoint = path[0] if (nextPoint.toBreak.length > 0 || nextPoint.toPlace.length > 0) { fullStop() return } dx = nextPoint.x - p.x dz = nextPoint.z - p.z } bot.look(Math.atan2(-dx, -dz), 0) bot.setControlState('forward', true) bot.setControlState('jump', false) if (bot.entity.isInWater) { bot.setControlState('jump', true) bot.setControlState('sprint', false) } else if (stateMovements.allowSprinting && physics.canStraightLine(path, true)) { bot.setControlState('jump', false) bot.setControlState('sprint', true) } else if (stateMovements.allowSprinting && physics.canSprintJump(path)) { bot.setControlState('jump', true) bot.setControlState('sprint', true) } else if (physics.canStraightLine(path)) { bot.setControlState('jump', false) bot.setControlState('sprint', false) } else if (physics.canWalkJump(path)) { bot.setControlState('jump', true) bot.setControlState('sprint', false) } else { bot.setControlState('forward', false) bot.setControlState('sprint', false) } // check for futility if (performance.now() - lastNodeTime > 3500) { // should never take this long to go to the next node resetPath('stuck') } } } module.exports = { pathfinder: inject, Movements: require('./lib/movements'), goals: require('./lib/goals') } ================================================ FILE: lib/astar.js ================================================ const { performance } = require('perf_hooks') const Heap = require('./heap.js') class PathNode { constructor () { this.data = null this.g = 0 this.h = 0 this.f = 0 this.parent = null } set (data, g, h, parent = null) { this.data = data this.g = g this.h = h this.f = g + h this.parent = parent return this } } function reconstructPath (node) { const path = [] while (node.parent) { path.push(node.data) node = node.parent } return path.reverse() } class AStar { constructor (start, movements, goal, timeout, tickTimeout = 40, searchRadius = -1) { this.startTime = performance.now() this.movements = movements this.goal = goal this.timeout = timeout this.tickTimeout = tickTimeout this.closedDataSet = new Set() this.openHeap = new Heap() this.openDataMap = new Map() const startNode = new PathNode().set(start, 0, goal.heuristic(start)) this.openHeap.push(startNode) this.openDataMap.set(startNode.data.hash, startNode) this.bestNode = startNode this.maxCost = searchRadius < 0 ? -1 : startNode.h + searchRadius this.visitedChunks = new Set() } makeResult (status, node) { return { status, cost: node.g, time: performance.now() - this.startTime, visitedNodes: this.closedDataSet.size, generatedNodes: this.closedDataSet.size + this.openHeap.size(), path: reconstructPath(node), context: this } } compute () { const computeStartTime = performance.now() while (!this.openHeap.isEmpty()) { if (performance.now() - computeStartTime > this.tickTimeout) { // compute time per tick return this.makeResult('partial', this.bestNode) } if (performance.now() - this.startTime > this.timeout) { // total compute time return this.makeResult('timeout', this.bestNode) } const node = this.openHeap.pop() if (this.goal.isEnd(node.data)) { return this.makeResult('success', node) } // not done yet this.openDataMap.delete(node.data.hash) this.closedDataSet.add(node.data.hash) this.visitedChunks.add(`${node.data.x >> 4},${node.data.z >> 4}`) const neighbors = this.movements.getNeighbors(node.data) for (const neighborData of neighbors) { if (this.closedDataSet.has(neighborData.hash)) { continue // skip closed neighbors } const gFromThisNode = node.g + neighborData.cost let neighborNode = this.openDataMap.get(neighborData.hash) let update = false const heuristic = this.goal.heuristic(neighborData) if (this.maxCost > 0 && gFromThisNode + heuristic > this.maxCost) continue if (neighborNode === undefined) { // add neighbor to the open set neighborNode = new PathNode() // properties will be set later this.openDataMap.set(neighborData.hash, neighborNode) } else { if (neighborNode.g < gFromThisNode) { // skip this one because another route is faster continue } update = true } // found a new or better route. // update this neighbor with this node as its new parent neighborNode.set(neighborData, gFromThisNode, heuristic, node) if (neighborNode.h < this.bestNode.h) this.bestNode = neighborNode if (update) { this.openHeap.update(neighborNode) } else { this.openHeap.push(neighborNode) } } } // all the neighbors of every accessible node have been exhausted return this.makeResult('noPath', this.bestNode) } } module.exports = AStar ================================================ FILE: lib/goals.js ================================================ const { Vec3 } = require('vec3') const { getShapeFaceCenters } = require('./shapes') // Goal base class class Goal { // Return the distance between node and the goal heuristic (node) { return 0 } // Return true if the node has reach the goal isEnd (node) { return true } // Return true if the goal has changed and the current path // should be invalidated and computed again hasChanged () { return false } // Returns true if the goal is still valid for the goal, // for the GoalFollow this would be true if the entity is not null isValid () { return true } } // One specific block that the player should stand inside at foot level class GoalBlock extends Goal { constructor (x, y, z) { super() this.x = Math.floor(x) this.y = Math.floor(y) this.z = Math.floor(z) } heuristic (node) { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z return distanceXZ(dx, dz) + Math.abs(dy) } isEnd (node) { return node.x === this.x && node.y === this.y && node.z === this.z } } // A block position that the player should get within a certain radius of, used for following entities class GoalNear extends Goal { constructor (x, y, z, range) { super() this.x = Math.floor(x) this.y = Math.floor(y) this.z = Math.floor(z) this.rangeSq = range * range } heuristic (node) { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z return distanceXZ(dx, dz) + Math.abs(dy) } isEnd (node) { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z return (dx * dx + dy * dy + dz * dz) <= this.rangeSq } } // Useful for long-range goals that don't have a specific Y level class GoalXZ extends Goal { constructor (x, z) { super() this.x = Math.floor(x) this.z = Math.floor(z) } heuristic (node) { const dx = this.x - node.x const dz = this.z - node.z return distanceXZ(dx, dz) } isEnd (node) { return node.x === this.x && node.z === this.z } } // Useful for finding builds that you don't have an exact Y level for, just an approximate X and Z level class GoalNearXZ extends Goal { constructor (x, z, range) { super() this.x = Math.floor(x) this.z = Math.floor(z) this.rangeSq = range * range } heuristic (node) { const dx = this.x - node.x const dz = this.z - node.z return distanceXZ(dx, dz) } isEnd (node) { const dx = this.x - node.x const dz = this.z - node.z return (dx * dx + dz * dz) <= this.rangeSq } } // Goal is a Y coordinate class GoalY extends Goal { constructor (y) { super() this.y = Math.floor(y) } heuristic (node) { const dy = this.y - node.y return Math.abs(dy) } isEnd (node) { return node.y === this.y } } // Don't get into the block, but get directly adjacent to it. Useful for chests. class GoalGetToBlock extends Goal { constructor (x, y, z) { super() this.x = Math.floor(x) this.y = Math.floor(y) this.z = Math.floor(z) } heuristic (node) { const dx = node.x - this.x const dy = node.y - this.y const dz = node.z - this.z return distanceXZ(dx, dz) + Math.abs(dy < 0 ? dy + 1 : dy) } isEnd (node) { const dx = node.x - this.x const dy = node.y - this.y const dz = node.z - this.z return Math.abs(dx) + Math.abs(dy < 0 ? dy + 1 : dy) + Math.abs(dz) === 1 } } // Path into a position were a blockface of block at x y z is visible. class GoalLookAtBlock extends Goal { constructor (pos, world, options = {}) { super() this.pos = pos this.world = world this.reach = options.reach || 4.5 // default survival: 4.5 creative: 5 this.entityHeight = options.entityHeight || 1.6 } heuristic (node) { const dx = node.x - this.pos.x const dy = node.y - this.pos.y const dz = node.z - this.pos.z return distanceXZ(dx, dz) + Math.abs(dy < 0 ? dy + 1 : dy) } isEnd (node) { if (node.distanceTo(this.pos.offset(0, this.entityHeight, 0)) > this.reach) return false // Check faces that could be seen from the current position. If the delta is smaller then 0.5 that means the bot cam most likely not see the face as the block is 1 block thick // this could be false for blocks that have a smaller bounding box then 1x1x1 const dx = node.x - (this.pos.x + 0.5) const dy = node.y + this.entityHeight - (this.pos.y + 0.5) // -0.5 because the bot position is calculated from the block position that is inside its feet so 0.5 - 1 = -0.5 const dz = node.z - (this.pos.z + 0.5) // Check y first then x and z const visibleFaces = { y: Math.sign(Math.abs(dy) > 0.5 ? dy : 0), x: Math.sign(Math.abs(dx) > 0.5 ? dx : 0), z: Math.sign(Math.abs(dz) > 0.5 ? dz : 0) } const validFaces = [] for (const i in visibleFaces) { if (!visibleFaces[i]) { // skip as this face is not visible continue } const targetPos = new Vec3(this.pos.x, this.pos.y, this.pos.z).offset(0.5 + (i === 'x' ? visibleFaces[i] * 0.5 : 0), 0.5 + (i === 'y' ? visibleFaces[i] * 0.5 : 0), 0.5 + (i === 'z' ? visibleFaces[i] * 0.5 : 0)) const startPos = new Vec3(node.x + 0.5, node.y + this.entityHeight, node.z + 0.5) const rayPos = this.world.raycast(startPos, targetPos.clone().subtract(startPos).normalize(), this.reach)?.position if (rayPos && rayPos.x === this.pos.x && rayPos.y === this.pos.y && rayPos.z === this.pos.z) { validFaces.push({ face: rayPos.face, targetPos }) } } return validFaces.length !== 0 } } // Path into a position were a blockface of block at x y z is visible. // You'll manually need to break the block. THIS WONT BREAK IT class GoalBreakBlock extends Goal { constructor (x, y, z, bot, options = {}) { super() this.goal = new GoalLookAtBlock(new Vec3(x, y, z), bot, options) } isEnd (node) { return this.goal.isEnd(node) } heuristic (node) { return this.goal.heuristic(node) } } // A composite of many goals, any one of which satisfies the composite. // For example, a GoalCompositeAny of block goals for every oak log in loaded // chunks would result in it pathing to the easiest oak log to get to class GoalCompositeAny extends Goal { constructor (goals = []) { super() this.goals = goals } push (goal) { this.goals.push(goal) } heuristic (node) { let min = Number.MAX_VALUE for (const i in this.goals) { min = Math.min(min, this.goals[i].heuristic(node)) } return min } isEnd (node) { for (const i in this.goals) { if (this.goals[i].isEnd(node)) return true } return false } hasChanged () { for (const i in this.goals) { if (this.goals[i].hasChanged()) return true } return false } isValid () { return this.goals.reduce((pre, curr) => pre && curr.isValid(), true) } } // A composite of many goals, all of them needs to be satisfied. class GoalCompositeAll extends Goal { constructor (goals = []) { super() this.goals = goals } push (goal) { this.goals.push(goal) } heuristic (node) { let max = Number.MIN_VALUE for (const i in this.goals) { max = Math.max(max, this.goals[i].heuristic(node)) } return max } isEnd (node) { for (const i in this.goals) { if (!this.goals[i].isEnd(node)) return false } return true } hasChanged () { for (const i in this.goals) { if (this.goals[i].hasChanged()) return true } return false } isValid () { return this.goals.reduce((pre, curr) => pre && curr.isValid(), true) } } class GoalInvert extends Goal { constructor (goal) { super() this.goal = goal } heuristic (node) { return -this.goal.heuristic(node) } isEnd (node) { return !this.goal.isEnd(node) } hasChanged () { return this.goal.hasChanged() } isValid () { return this.goal.isValid() } } class GoalFollow extends Goal { constructor (entity, range) { super() this.entity = entity this.x = Math.floor(entity.position.x) this.y = Math.floor(entity.position.y) this.z = Math.floor(entity.position.z) this.rangeSq = range * range } heuristic (node) { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z return distanceXZ(dx, dz) + Math.abs(dy) } isEnd (node) { const dx = this.x - node.x const dy = this.y - node.y const dz = this.z - node.z return (dx * dx + dy * dy + dz * dz) <= this.rangeSq } hasChanged () { const p = this.entity.position.floored() const dx = this.x - p.x const dy = this.y - p.y const dz = this.z - p.z if ((dx * dx + dy * dy + dz * dz) > this.rangeSq) { this.x = p.x this.y = p.y this.z = p.z return true } return false } isValid () { return this.entity != null } } function distanceXZ (dx, dz) { dx = Math.abs(dx) dz = Math.abs(dz) return Math.abs(dx - dz) + Math.min(dx, dz) * Math.SQRT2 } /** * Options: * - range - maximum distance from the clicked face * - faces - the directions of the faces the player can click * - facing - the direction the player must be facing * - facing3D - boolean, facing is 3D (true) or 2D (false) * - half - 'top' or 'bottom', the half that must be clicked * - LOS - true or false, should the bot have line of sight off the placement face. Default true. */ class GoalPlaceBlock extends Goal { constructor (pos, world, options) { super() this.pos = pos.floored() this.world = world this.options = options if (!this.options.range) this.options.range = 5 if (!('LOS' in this.options)) this.options.LOS = true if (!this.options.faces) { this.options.faces = [new Vec3(0, -1, 0), new Vec3(0, 1, 0), new Vec3(0, 0, -1), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(1, 0, 0)] } this.options.facing = ['north', 'east', 'south', 'west', 'up', 'down'].indexOf(this.options.facing) this.facesPos = [] for (const dir of this.options.faces) { const ref = this.pos.plus(dir) const refBlock = this.world.getBlock(ref) if (!refBlock) continue for (const center of getShapeFaceCenters(refBlock.shapes, dir.scaled(-1), this.options.half)) { this.facesPos.push([dir, center.add(ref), ref]) } } } heuristic (node) { const dx = node.x - this.pos.x const dy = node.y - this.pos.y const dz = node.z - this.pos.z return distanceXZ(dx, dz) + Math.abs(dy < 0 ? dy + 1 : dy) } isEnd (node) { if (this.isStandingIn(node)) return false const headPos = node.offset(0.5, 1.6, 0.5) return this.getFaceAndRef(headPos) !== null } getFaceAndRef (headPos) { for (const [face, to, ref] of this.facesPos) { const dir = to.minus(headPos) if (dir.norm() > this.options.range) continue if (!this.checkFacing(dir)) continue if (!this.options.LOS) { return { face, to, ref } } const block = this.world.raycast(headPos, dir.normalize(), this.options.range) if (block && block.position.equals(ref) && block.face === vectorToDirection(face.scaled(-1))) { return { face, to, ref } } } return null } checkFacing (dir) { if (this.options.facing < 0) return true if (this.options.facing3D) { const dH = Math.sqrt(dir.x * dir.x + dir.z * dir.z) const vAngle = Math.atan2(dir.y, dH) * 180 / Math.PI if (vAngle > 45) return this.options.facing === 4 if (vAngle < -45) return this.options.facing === 5 } const angle = Math.atan2(dir.x, -dir.z) * 180 / Math.PI + 180 // Convert to [0,360[ const facing = Math.floor(angle / 90 + 0.5) & 0x3 if (this.options.facing === facing) return true return false } isStandingIn (node) { const dx = node.x - this.pos.x const dy = node.y - this.pos.y const dz = node.z - this.pos.z return (Math.abs(dx) + Math.abs(dy < 0 ? dy + 1 : dy) + Math.abs(dz)) < 1 } } function vectorToDirection (v) { if (v.y < 0) { return 0 } else if (v.y > 0) { return 1 } else if (v.z < 0) { return 2 } else if (v.z > 0) { return 3 } else if (v.x < 0) { return 4 } else if (v.x > 0) { return 5 } } module.exports = { Goal, GoalBlock, GoalNear, GoalXZ, GoalNearXZ, GoalY, GoalGetToBlock, GoalCompositeAny, GoalCompositeAll, GoalInvert, GoalFollow, GoalPlaceBlock, GoalBreakBlock, GoalLookAtBlock } ================================================ FILE: lib/goto.js ================================================ function error (name, message) { const err = new Error(message) err.name = name return err } /** * Adds a easy-to-use API wrapper for quickly executing a goal and running * a callback when that goal is reached. This function serves to remove a * lot of boilerplate code for quickly executing a goal. * * @param {Bot} bot - The bot. * @param {Goal} goal - The goal to execute. * @returns {Promise} - resolves on success, rejects on error */ function goto (bot, goal) { return new Promise((resolve, reject) => { function goalReached () { cleanup() } function noPathListener (results) { if (results.path.length === 0) { cleanup() } else if (results.status === 'noPath') { cleanup(error('NoPath', 'No path to the goal!')) } else if (results.status === 'timeout') { cleanup(error('Timeout', 'Took to long to decide path to goal!')) } } function goalChangedListener (newGoal) { if (newGoal !== goal) { cleanup(error('GoalChanged', 'The goal was changed before it could be completed!')) } } function pathStopped () { cleanup(error('PathStopped', 'Path was stopped before it could be completed! Thus, the desired goal was not reached.')) } function cleanup (err) { bot.removeListener('goal_reached', goalReached) bot.removeListener('path_update', noPathListener) bot.removeListener('goal_updated', goalChangedListener) bot.removeListener('path_stop', pathStopped) // Run callback on next event stack to let pathfinder properly cleanup, // otherwise chaining waypoints does not work properly. setTimeout(() => { if (err) { reject(err) } else { resolve() } }, 0) } bot.on('path_stop', pathStopped) bot.on('goal_reached', goalReached) bot.on('path_update', noPathListener) bot.on('goal_updated', goalChangedListener) bot.pathfinder.setGoal(goal) }) } module.exports = goto ================================================ FILE: lib/heap.js ================================================ class BinaryHeapOpenSet { constructor () { // Initialing the array heap and adding a dummy element at index 0 this.heap = [null] } size () { return this.heap.length - 1 } isEmpty () { return this.heap.length === 1 } push (val) { // Inserting the new node at the end of the heap array this.heap.push(val) // Finding the correct position for the new node let current = this.heap.length - 1 let parent = current >>> 1 // Traversing up the parent node until the current node is greater than the parent while (current > 1 && this.heap[parent].f > this.heap[current].f) { [this.heap[parent], this.heap[current]] = [this.heap[current], this.heap[parent]] current = parent parent = current >>> 1 } } update (val) { let current = this.heap.indexOf(val) let parent = current >>> 1 // Traversing up the parent node until the current node is greater than the parent while (current > 1 && this.heap[parent].f > this.heap[current].f) { [this.heap[parent], this.heap[current]] = [this.heap[current], this.heap[parent]] current = parent parent = current >>> 1 } } pop () { // Smallest element is at the index 1 in the heap array const smallest = this.heap[1] this.heap[1] = this.heap[this.heap.length - 1] this.heap.splice(this.heap.length - 1) const size = this.heap.length - 1 if (size < 2) return smallest const val = this.heap[1] let index = 1 let smallerChild = 2 const cost = val.f do { let smallerChildNode = this.heap[smallerChild] if (smallerChild < size - 1) { const rightChildNode = this.heap[smallerChild + 1] if (smallerChildNode.f > rightChildNode.f) { smallerChild++ smallerChildNode = rightChildNode } } if (cost <= smallerChildNode.f) { break } this.heap[index] = smallerChildNode this.heap[smallerChild] = val index = smallerChild smallerChild *= 2 } while (smallerChild <= size) return smallest } } module.exports = BinaryHeapOpenSet ================================================ FILE: lib/interactable.json ================================================ [ "acacia_door", "acacia_fence_gate", "acacia_button", "acacia_trapdoor", "anvil", "armor_stand", "barrel", "beacon", "bed_block", "bell", "birch_boat", "birch_button", "birch_door", "birch_fence_gate", "birch_trapdoor", "black_bed", "black_shulker_box", "blast_furnace", "blue_bed", "blue_shulker_box", "brewing_stand", "brown_bed", "brown_shulker_box", "campfire", "cauldron", "chest", "chest_minecart", "chipped_anvil", "command", "command_block", "command_block_minecart", "comparator", "composter", "crafting_table", "cyan_bed", "cyan_shulker_box", "damaged_anvil", "dark_oak_boat", "dark_oak_button", "dark_oak_fence_gate", "dark_oak_trapdoor", "dark_oak_door", "daylight_detector", "daylight_detector_inverted", "diode", "diode_block_off", "diode_block_on", "dispenser", "door", "dragon_egg", "dropper", "enchanting_table", "enchantment_table", "end_crystal", "end_portal_frame", "ender_portal_frame", "ender_chest", "explosive_minecart", "farmland", "fence_gate", "fletching_table", "flower_pot", "furnace", "furnace_minecart", "gray_bed", "gray_shulker_box", "green_bed", "green_shulker_box", "hopper", "hopper_minecart", "iron_door", "iron_trapdoor", "item_frame", "jukebox", "jungle_button", "jungle_boat", "jungle_door", "jungle_fence_gate", "jungle_trapdoor", "lever", "light_blue_bed", "light_blue_shulker_box", "light_gray_bed", "light_gray_shulker_box", "lime_bed", "lime_shulker_box", "magenta_bed", "magenta_shulker_box", "minecart", "note_block", "oak_boat", "oak_button", "oak_door", "oak_fence_gate", "oak_trapdoor", "orange_bed", "orange_shulker_box", "pink_bed", "pink_shulker_box", "powered_minecart", "purple_bed", "purple_shulker_box", "red_bed", "red_shulker_box", "redstone_ore", "redstone_comparator_off", "redstone_comparator_on", "repeating_command_block", "repeater", "powered_repeater", "unpowered_repeater", "redstone_torch", "saddle", "shulker_box", "sign", "sign_post", "smithing_table", "smoker", "spruce_boat", "spruce_button", "spruce_door", "spruce_fence_gate", "stonecutter", "stone_button", "storage_minecart", "tnt_minecart", "tnt", "trap_door", "trapped_chest", "white_bed", "white_shulker_box", "wood_button", "yellow_bed", "yelow_shulker_box" ] ================================================ FILE: lib/lock.js ================================================ const { EventEmitter, on } = require('events') class Lock { constructor () { this._locked = false this._emitter = new EventEmitter() } /** * Synchronous. Returns true if the lock was acquired. Return false if the lock is already held by something else. * @returns {boolean} */ tryAcquire () { if (!this._locked) { this._locked = true return true } return false } /** * Asynchronous. Resolves when the lock was acquired. * @returns {Promise} */ async acquire () { if (!this._locked) { this._locked = true return } // Cannot use for await without a variable. But the variable is never used. So eslint complains ¯\_(ツ)_/¯ for await (const _ of on(this._emitter, 'release')) { // eslint-disable-line if (!this._locked) { this._locked = true return } } } /** * Releases the lock. */ release () { this._locked = false setImmediate(() => this._emitter.emit('release')) } } module.exports = Lock ================================================ FILE: lib/move.js ================================================ const { Vec3 } = require('vec3') class Move extends Vec3 { constructor (x, y, z, remainingBlocks, cost, toBreak = [], toPlace = [], parkour = false) { super(Math.floor(x), Math.floor(y), Math.floor(z)) this.remainingBlocks = remainingBlocks this.cost = cost this.toBreak = toBreak this.toPlace = toPlace this.parkour = parkour this.hash = this.x + ',' + this.y + ',' + this.z } } module.exports = Move ================================================ FILE: lib/movements.js ================================================ const { Vec3 } = require('vec3') const nbt = require('prismarine-nbt') const Move = require('./move') const cardinalDirections = [ { x: -1, z: 0 }, // West { x: 1, z: 0 }, // East { x: 0, z: -1 }, // North { x: 0, z: 1 } // South ] const diagonalDirections = [ { x: -1, z: -1 }, { x: -1, z: 1 }, { x: 1, z: -1 }, { x: 1, z: 1 } ] class Movements { constructor (bot) { const registry = bot.registry this.bot = bot this.canDig = true this.digCost = 1 this.placeCost = 1 this.liquidCost = 1 this.entityCost = 1 this.dontCreateFlow = true this.dontMineUnderFallingBlock = true this.allow1by1towers = true this.allowFreeMotion = false this.allowParkour = true this.allowSprinting = true this.allowEntityDetection = true this.entitiesToAvoid = new Set() this.passableEntities = new Set(require('./passableEntities.json')) this.interactableBlocks = new Set(require('./interactable.json')) this.blocksCantBreak = new Set() this.blocksCantBreak.add(registry.blocksByName.chest.id) registry.blocksArray.forEach(block => { if (block.diggable) return this.blocksCantBreak.add(block.id) }) this.blocksToAvoid = new Set() this.blocksToAvoid.add(registry.blocksByName.fire.id) if (registry.blocksByName.cobweb) this.blocksToAvoid.add(registry.blocksByName.cobweb.id) if (registry.blocksByName.web) this.blocksToAvoid.add(registry.blocksByName.web.id) this.blocksToAvoid.add(registry.blocksByName.lava.id) this.liquids = new Set() this.liquids.add(registry.blocksByName.water.id) this.liquids.add(registry.blocksByName.lava.id) this.gravityBlocks = new Set() this.gravityBlocks.add(registry.blocksByName.sand.id) this.gravityBlocks.add(registry.blocksByName.gravel.id) this.climbables = new Set() this.climbables.add(registry.blocksByName.ladder.id) // this.climbables.add(registry.blocksByName.vine.id) this.emptyBlocks = new Set() this.replaceables = new Set() this.replaceables.add(registry.blocksByName.air.id) if (registry.blocksByName.cave_air) this.replaceables.add(registry.blocksByName.cave_air.id) if (registry.blocksByName.void_air) this.replaceables.add(registry.blocksByName.void_air.id) this.replaceables.add(registry.blocksByName.water.id) this.replaceables.add(registry.blocksByName.lava.id) this.scafoldingBlocks = [] this.scafoldingBlocks.push(registry.itemsByName.dirt.id) this.scafoldingBlocks.push(registry.itemsByName.cobblestone.id) const Block = require('prismarine-block')(bot.registry) this.fences = new Set() this.carpets = new Set() this.openable = new Set() registry.blocksArray.map(x => Block.fromStateId(x.minStateId, 0)).forEach(block => { if (block.shapes.length > 0) { // Fences or any block taller than 1, they will be considered as non-physical to avoid // trying to walk on them if (block.shapes[0][4] > 1) this.fences.add(block.type) // Carpets or any blocks smaller than 0.1, they will be considered as safe to walk in if (block.shapes[0][4] < 0.1) this.carpets.add(block.type) } else if (block.shapes.length === 0) { this.emptyBlocks.add(block.type) } }) registry.blocksArray.forEach(block => { if (this.interactableBlocks.has(block.name) && block.name.toLowerCase().includes('gate') && !block.name.toLowerCase().includes('iron')) { // console.info(block) this.openable.add(block.id) } }) this.canOpenDoors = false // Causes issues. Probably due to none paper servers. this.exclusionAreasStep = [] this.exclusionAreasBreak = [] this.exclusionAreasPlace = [] this.maxDropDown = 4 this.infiniteLiquidDropdownDistance = true this.entityIntersections = {} } exclusionPlace (block) { if (this.exclusionAreasPlace.length === 0) return 0 let weight = 0 for (const a of this.exclusionAreasPlace) { weight += a(block) } return weight } exclusionStep (block) { if (this.exclusionAreasStep.length === 0) return 0 let weight = 0 for (const a of this.exclusionAreasStep) { weight += a(block) } return weight } exclusionBreak (block) { if (this.exclusionAreasBreak.length === 0) return 0 let weight = 0 for (const a of this.exclusionAreasBreak) { weight += a(block) } return weight } countScaffoldingItems () { let count = 0 const items = this.bot.inventory.items() for (const id of this.scafoldingBlocks) { for (const j in items) { const item = items[j] if (item.type === id) count += item.count } } return count } getScaffoldingItem () { const items = this.bot.inventory.items() for (const id of this.scafoldingBlocks) { for (const j in items) { const item = items[j] if (item.type === id) return item } } return null } clearCollisionIndex () { this.entityIntersections = {} } /** * Finds blocks intersected by entity bounding boxes * and sets the number of ents intersecting in a dict. * Ignores entities that do not affect block placement */ updateCollisionIndex () { for (const ent of Object.values(this.bot.entities)) { if (ent === this.bot.entity) { continue } const avoidedEnt = this.entitiesToAvoid.has(ent.name) if (avoidedEnt || !this.passableEntities.has(ent.name)) { const entSquareRadius = ent.width / 2.0 const minY = Math.floor(ent.position.y) const maxY = Math.ceil(ent.position.y + ent.height) const minX = Math.floor(ent.position.x - entSquareRadius) const maxX = Math.ceil(ent.position.x + entSquareRadius) const minZ = Math.floor(ent.position.z - entSquareRadius) const maxZ = Math.ceil(ent.position.z + entSquareRadius) const cost = avoidedEnt ? 100 : 1 for (let y = minY; y < maxY; y++) { for (let x = minX; x < maxX; x++) { for (let z = minZ; z < maxZ; z++) { this.entityIntersections[`${x},${y},${z}`] = this.entityIntersections[`${x},${y},${z}`] ?? 0 this.entityIntersections[`${x},${y},${z}`] += cost // More ents = more weight } } } } } } /** * Gets number of entities who's bounding box intersects the node + offset * @param {import('vec3').Vec3} pos node position * @param {number} dx X axis offset * @param {number} dy Y axis offset * @param {number} dz Z axis offset * @returns {number} Number of entities intersecting block */ getNumEntitiesAt (pos, dx, dy, dz) { if (this.allowEntityDetection === false) return 0 if (!pos) return 0 const y = pos.y + dy const x = pos.x + dx const z = pos.z + dz return this.entityIntersections[`${x},${y},${z}`] ?? 0 } getBlock (pos, dx, dy, dz) { const b = pos ? this.bot.blockAt(new Vec3(pos.x + dx, pos.y + dy, pos.z + dz), false) : null if (!b) { return { replaceable: false, canFall: false, safe: false, physical: false, liquid: false, climbable: false, height: dy, openable: false } } b.climbable = this.climbables.has(b.type) b.safe = (b.boundingBox === 'empty' || b.climbable || this.carpets.has(b.type)) && !this.blocksToAvoid.has(b.type) b.physical = b.boundingBox === 'block' && !this.fences.has(b.type) b.replaceable = this.replaceables.has(b.type) && !b.physical b.liquid = this.liquids.has(b.type) b.height = pos.y + dy b.canFall = this.gravityBlocks.has(b.type) b.openable = this.openable.has(b.type) for (const shape of b.shapes) { b.height = Math.max(b.height, pos.y + dy + shape[4]) } return b } /** * Takes into account if the block is within a break exclusion area. * @param {import('prismarine-block').Block} block * @returns */ safeToBreak (block) { if (!this.canDig) { return false } if (this.dontCreateFlow) { // false if next to liquid if (this.getBlock(block.position, 0, 1, 0).liquid) return false if (this.getBlock(block.position, -1, 0, 0).liquid) return false if (this.getBlock(block.position, 1, 0, 0).liquid) return false if (this.getBlock(block.position, 0, 0, -1).liquid) return false if (this.getBlock(block.position, 0, 0, 1).liquid) return false } if (this.dontMineUnderFallingBlock) { // TODO: Determine if there are other blocks holding the entity up if (this.getBlock(block.position, 0, 1, 0).canFall || (this.getNumEntitiesAt(block.position, 0, 1, 0) > 0)) { return false } } return block.type && !this.blocksCantBreak.has(block.type) && this.exclusionBreak(block) < 100 } /** * Takes into account if the block is within the stepExclusionAreas. And returns 100 if a block to be broken is within break exclusion areas. * @param {import('prismarine-block').Block} block block * @param {[]} toBreak * @returns {number} */ safeOrBreak (block, toBreak) { let cost = 0 cost += this.exclusionStep(block) // Is excluded so can't move or break cost += this.getNumEntitiesAt(block.position, 0, 0, 0) * this.entityCost if (block.safe) return cost if (!this.safeToBreak(block)) return 100 // Can't break, so can't move toBreak.push(block.position) if (block.physical) cost += this.getNumEntitiesAt(block.position, 0, 1, 0) * this.entityCost // Add entity cost if there is an entity above (a breakable block) that will fall const tool = this.bot.pathfinder.bestHarvestTool(block) const enchants = (tool && tool.nbt) ? nbt.simplify(tool.nbt).Enchantments : [] const effects = this.bot.entity.effects const digTime = block.digTime(tool ? tool.type : null, false, false, false, enchants, effects) const laborCost = (1 + 3 * digTime / 1000) * this.digCost cost += laborCost return cost } getMoveJumpUp (node, dir, neighbors) { const blockA = this.getBlock(node, 0, 2, 0) const blockH = this.getBlock(node, dir.x, 2, dir.z) const blockB = this.getBlock(node, dir.x, 1, dir.z) const blockC = this.getBlock(node, dir.x, 0, dir.z) let cost = 2 // move cost (move+jump) const toBreak = [] const toPlace = [] if (blockA.physical && (this.getNumEntitiesAt(blockA.position, 0, 1, 0) > 0)) return // Blocks A, B and H are above C, D and the player's space, we need to make sure there are no entities that will fall down onto our building space if we break them if (blockH.physical && (this.getNumEntitiesAt(blockH.position, 0, 1, 0) > 0)) return if (blockB.physical && !blockH.physical && !blockC.physical && (this.getNumEntitiesAt(blockB.position, 0, 1, 0) > 0)) return // It is fine if an ent falls on B so long as we don't need to replace block C if (!blockC.physical) { if (node.remainingBlocks === 0) return // not enough blocks to place if (this.getNumEntitiesAt(blockC.position, 0, 0, 0) > 0) return // Check for any entities in the way of a block placement const blockD = this.getBlock(node, dir.x, -1, dir.z) if (!blockD.physical) { if (node.remainingBlocks === 1) return // not enough blocks to place if (this.getNumEntitiesAt(blockD.position, 0, 0, 0) > 0) return // Check for any entities in the way of a block placement if (!blockD.replaceable) { if (!this.safeToBreak(blockD)) return cost += this.exclusionBreak(blockD) toBreak.push(blockD.position) } cost += this.exclusionPlace(blockD) toPlace.push({ x: node.x, y: node.y - 1, z: node.z, dx: dir.x, dy: 0, dz: dir.z, returnPos: new Vec3(node.x, node.y, node.z) }) cost += this.placeCost // additional cost for placing a block } if (!blockC.replaceable) { if (!this.safeToBreak(blockC)) return cost += this.exclusionBreak(blockC) toBreak.push(blockC.position) } cost += this.exclusionPlace(blockC) toPlace.push({ x: node.x + dir.x, y: node.y - 1, z: node.z + dir.z, dx: 0, dy: 1, dz: 0 }) cost += this.placeCost // additional cost for placing a block blockC.height += 1 } const block0 = this.getBlock(node, 0, -1, 0) if (blockC.height - block0.height > 1.2) return // Too high to jump cost += this.safeOrBreak(blockA, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockH, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockB, toBreak) if (cost > 100) return neighbors.push(new Move(blockB.position.x, blockB.position.y, blockB.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveForward (node, dir, neighbors) { const blockB = this.getBlock(node, dir.x, 1, dir.z) const blockC = this.getBlock(node, dir.x, 0, dir.z) const blockD = this.getBlock(node, dir.x, -1, dir.z) let cost = 1 // move cost cost += this.exclusionStep(blockC) const toBreak = [] const toPlace = [] if (!blockD.physical && !blockC.liquid) { if (node.remainingBlocks === 0) return // not enough blocks to place if (this.getNumEntitiesAt(blockD.position, 0, 0, 0) > 0) return // D intersects an entity hitbox if (!blockD.replaceable) { if (!this.safeToBreak(blockD)) return cost += this.exclusionBreak(blockD) toBreak.push(blockD.position) } cost += this.exclusionPlace(blockD) toPlace.push({ x: node.x, y: node.y - 1, z: node.z, dx: dir.x, dy: 0, dz: dir.z }) cost += this.placeCost // additional cost for placing a block } cost += this.safeOrBreak(blockB, toBreak) if (cost > 100) return // Open fence gates if (this.canOpenDoors && blockC.openable && blockC.shapes && blockC.shapes.length !== 0) { toPlace.push({ x: node.x + dir.x, y: node.y, z: node.z + dir.z, dx: 0, dy: 0, dz: 0, useOne: true }) // Indicate that a block should be used on this block not placed } else { cost += this.safeOrBreak(blockC, toBreak) if (cost > 100) return } if (this.getBlock(node, 0, 0, 0).liquid) cost += this.liquidCost neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveDiagonal (node, dir, neighbors) { let cost = Math.SQRT2 // move cost const toBreak = [] const blockC = this.getBlock(node, dir.x, 0, dir.z) // Landing block or standing on block when jumping up by 1 const y = blockC.physical ? 1 : 0 const block0 = this.getBlock(node, 0, -1, 0) let cost1 = 0 const toBreak1 = [] const blockB1 = this.getBlock(node, 0, y + 1, dir.z) const blockC1 = this.getBlock(node, 0, y, dir.z) const blockD1 = this.getBlock(node, 0, y - 1, dir.z) cost1 += this.safeOrBreak(blockB1, toBreak1) cost1 += this.safeOrBreak(blockC1, toBreak1) if (blockD1.height - block0.height > 1.2) cost1 += this.safeOrBreak(blockD1, toBreak1) let cost2 = 0 const toBreak2 = [] const blockB2 = this.getBlock(node, dir.x, y + 1, 0) const blockC2 = this.getBlock(node, dir.x, y, 0) const blockD2 = this.getBlock(node, dir.x, y - 1, 0) cost2 += this.safeOrBreak(blockB2, toBreak2) cost2 += this.safeOrBreak(blockC2, toBreak2) if (blockD2.height - block0.height > 1.2) cost2 += this.safeOrBreak(blockD2, toBreak2) if (cost1 < cost2) { cost += cost1 toBreak.push(...toBreak1) } else { cost += cost2 toBreak.push(...toBreak2) } if (cost > 100) return cost += this.safeOrBreak(this.getBlock(node, dir.x, y, dir.z), toBreak) if (cost > 100) return cost += this.safeOrBreak(this.getBlock(node, dir.x, y + 1, dir.z), toBreak) if (cost > 100) return if (this.getBlock(node, 0, 0, 0).liquid) cost += this.liquidCost const blockD = this.getBlock(node, dir.x, -1, dir.z) if (y === 1) { // Case jump up by 1 if (blockC.height - block0.height > 1.2) return // Too high to jump cost += this.safeOrBreak(this.getBlock(node, 0, 2, 0), toBreak) if (cost > 100) return cost += 1 neighbors.push(new Move(blockC.position.x, blockC.position.y + 1, blockC.position.z, node.remainingBlocks, cost, toBreak)) } else if (blockD.physical || blockC.liquid) { neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks, cost, toBreak)) } else if (this.getBlock(node, dir.x, -2, dir.z).physical || blockD.liquid) { if (!blockD.safe) return // don't self-immolate cost += this.getNumEntitiesAt(blockC.position, 0, -1, 0) * this.entityCost neighbors.push(new Move(blockC.position.x, blockC.position.y - 1, blockC.position.z, node.remainingBlocks, cost, toBreak)) } } getLandingBlock (node, dir) { let blockLand = this.getBlock(node, dir.x, -2, dir.z) while (blockLand.position && blockLand.position.y > this.bot.game.minY) { if (blockLand.liquid && blockLand.safe) return blockLand if (blockLand.physical) { if (node.y - blockLand.position.y <= this.maxDropDown) return this.getBlock(blockLand.position, 0, 1, 0) return null } if (!blockLand.safe) return null blockLand = this.getBlock(blockLand.position, 0, -1, 0) } return null } getMoveDropDown (node, dir, neighbors) { const blockB = this.getBlock(node, dir.x, 1, dir.z) const blockC = this.getBlock(node, dir.x, 0, dir.z) const blockD = this.getBlock(node, dir.x, -1, dir.z) let cost = 1 // move cost const toBreak = [] const toPlace = [] const blockLand = this.getLandingBlock(node, dir) if (!blockLand) return if (!this.infiniteLiquidDropdownDistance && ((node.y - blockLand.position.y) > this.maxDropDown)) return // Don't drop down into water cost += this.safeOrBreak(blockB, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockC, toBreak) if (cost > 100) return cost += this.safeOrBreak(blockD, toBreak) if (cost > 100) return if (blockC.liquid) return // dont go underwater cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities neighbors.push(new Move(blockLand.position.x, blockLand.position.y, blockLand.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveDown (node, neighbors) { const block0 = this.getBlock(node, 0, -1, 0) let cost = 1 // move cost const toBreak = [] const toPlace = [] const blockLand = this.getLandingBlock(node, { x: 0, z: 0 }) if (!blockLand) return cost += this.safeOrBreak(block0, toBreak) if (cost > 100) return if (this.getBlock(node, 0, 0, 0).liquid) return // dont go underwater cost += this.getNumEntitiesAt(blockLand.position, 0, 0, 0) * this.entityCost // add cost for entities neighbors.push(new Move(blockLand.position.x, blockLand.position.y, blockLand.position.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } getMoveUp (node, neighbors) { const block1 = this.getBlock(node, 0, 0, 0) if (block1.liquid) return if (this.getNumEntitiesAt(node, 0, 0, 0) > 0) return // an entity (besides the player) is blocking the building area const block2 = this.getBlock(node, 0, 2, 0) let cost = 1 // move cost const toBreak = [] const toPlace = [] cost += this.safeOrBreak(block2, toBreak) if (cost > 100) return if (!block1.climbable) { if (!this.allow1by1towers || node.remainingBlocks === 0) return // not enough blocks to place if (!block1.replaceable) { if (!this.safeToBreak(block1)) return toBreak.push(block1.position) } const block0 = this.getBlock(node, 0, -1, 0) if (block0.physical && block0.height - node.y < -0.2) return // cannot jump-place from a half block cost += this.exclusionPlace(block1) toPlace.push({ x: node.x, y: node.y - 1, z: node.z, dx: 0, dy: 1, dz: 0, jump: true }) cost += this.placeCost // additional cost for placing a block } if (cost > 100) return neighbors.push(new Move(node.x, node.y + 1, node.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace)) } // Jump up, down or forward over a 1 block gap getMoveParkourForward (node, dir, neighbors) { const block0 = this.getBlock(node, 0, -1, 0) const block1 = this.getBlock(node, dir.x, -1, dir.z) if ((block1.physical && block1.height >= block0.height) || !this.getBlock(node, dir.x, 0, dir.z).safe || !this.getBlock(node, dir.x, 1, dir.z).safe) return if (this.getBlock(node, 0, 0, 0).liquid) return // cant jump from water let cost = 1 // Leaving entities at the ceiling level (along path) out for now because there are few cases where that will be important cost += this.getNumEntitiesAt(node, dir.x, 0, dir.z) * this.entityCost // If we have a block on the ceiling, we cannot jump but we can still fall let ceilingClear = this.getBlock(node, 0, 2, 0).safe && this.getBlock(node, dir.x, 2, dir.z).safe // Similarly for the down path let floorCleared = !this.getBlock(node, dir.x, -2, dir.z).physical const maxD = this.allowSprinting ? 4 : 2 for (let d = 2; d <= maxD; d++) { const dx = dir.x * d const dz = dir.z * d const blockA = this.getBlock(node, dx, 2, dz) const blockB = this.getBlock(node, dx, 1, dz) const blockC = this.getBlock(node, dx, 0, dz) const blockD = this.getBlock(node, dx, -1, dz) if (blockC.safe) cost += this.getNumEntitiesAt(blockC.position, 0, 0, 0) * this.entityCost if (ceilingClear && blockB.safe && blockC.safe && blockD.physical) { cost += this.exclusionStep(blockB) // Forward neighbors.push(new Move(blockC.position.x, blockC.position.y, blockC.position.z, node.remainingBlocks, cost, [], [], true)) break } else if (ceilingClear && blockB.safe && blockC.physical) { // Up if (blockA.safe && d !== 4) { // 4 Blocks forward 1 block up is very difficult and fails often cost += this.exclusionStep(blockA) if (blockC.height - block0.height > 1.2) break // Too high to jump cost += this.getNumEntitiesAt(blockB.position, 0, 0, 0) * this.entityCost neighbors.push(new Move(blockB.position.x, blockB.position.y, blockB.position.z, node.remainingBlocks, cost, [], [], true)) break } } else if ((ceilingClear || d === 2) && blockB.safe && blockC.safe && blockD.safe && floorCleared) { // Down const blockE = this.getBlock(node, dx, -2, dz) if (blockE.physical) { cost += this.exclusionStep(blockD) cost += this.getNumEntitiesAt(blockD.position, 0, 0, 0) * this.entityCost neighbors.push(new Move(blockD.position.x, blockD.position.y, blockD.position.z, node.remainingBlocks, cost, [], [], true)) } floorCleared = floorCleared && !blockE.physical } else if (!blockB.safe || !blockC.safe) { break } ceilingClear = ceilingClear && blockA.safe } } // for each cardinal direction: // "." is head. "+" is feet and current location. // "#" is initial floor which is always solid. "a"-"u" are blocks to check // // --0123-- horizontalOffset // | // +2 aho // +1 .bip // 0 +cjq // -1 #dkr // -2 els // -3 fmt // -4 gn // | // dy getNeighbors (node) { const neighbors = [] // Simple moves in 4 cardinal points for (const i in cardinalDirections) { const dir = cardinalDirections[i] this.getMoveForward(node, dir, neighbors) this.getMoveJumpUp(node, dir, neighbors) this.getMoveDropDown(node, dir, neighbors) if (this.allowParkour) { this.getMoveParkourForward(node, dir, neighbors) } } // Diagonals for (const i in diagonalDirections) { const dir = diagonalDirections[i] this.getMoveDiagonal(node, dir, neighbors) } this.getMoveDown(node, neighbors) this.getMoveUp(node, neighbors) return neighbors } } module.exports = Movements ================================================ FILE: lib/passableEntities.json ================================================ [ "falling_block", "tnt", "item", "area_effect_cloud", "item_frame", "leash_knot", "painting", "arrow", "dragon_fireball", "fireball", "llama_spit", "shulker_bullet", "small_fireball", "snowball", "spectral_arrow", "egg", "ender_pearl", "potion", "wither_skull", "end_crystal", "experience_orb", "eye_of_ender", "firework_rocket", "lightning_bolt", "experience_bottle", "trident", "fishing_bobber", "evoker_fangs" ] ================================================ FILE: lib/physics.js ================================================ const { PlayerState } = require('prismarine-physics') class Physics { constructor (bot) { this.bot = bot this.world = { getBlock: (pos) => { return bot.blockAt(pos, false) } } } /** * * @param {function} goal A function is the goal has been reached or not * @param {function} controller Controller that can change the current control State for the next tick * @param {number} ticks Number of ticks to simulate * @param {object} state Starting control state to begin the simulation with * @returns { import('prismarine-physics').PlayerState } A player state of the final simulation tick */ simulateUntil (goal, controller = () => {}, ticks = 1, state = null) { if (!state) { const simulationControl = { forward: this.bot.controlState.forward, back: this.bot.controlState.back, left: this.bot.controlState.left, right: this.bot.controlState.right, jump: this.bot.controlState.jump, sprint: this.bot.controlState.sprint, sneak: this.bot.controlState.sneak } state = new PlayerState(this.bot, simulationControl) } for (let i = 0; i < ticks; i++) { controller(state, i) this.bot.physics.simulatePlayer(state, this.world) if (state.isInLava) return state if (goal(state)) return state } return state } simulateUntilNextTick () { return this.simulateUntil(() => false, () => {}, 1) } simulateUntilOnGround (ticks = 5) { return this.simulateUntil(state => state.onGround, () => {}, ticks) } canStraightLine (path, sprint = false) { const reached = this.getReached(path) const state = this.simulateUntil(reached, this.getController(path[0], false, sprint), 200) if (reached(state)) return true if (sprint) { if (this.canSprintJump(path, 0)) return false } else { if (this.canWalkJump(path, 0)) return false } for (let i = 1; i < 7; i++) { if (sprint) { if (this.canSprintJump(path, i)) return true } else { if (this.canWalkJump(path, i)) return true } } return false } canStraightLineBetween (n1, n2) { const reached = (state) => { const delta = n2.minus(state.pos) const r2 = 0.15 * 0.15 return (delta.x * delta.x + delta.z * delta.z) <= r2 && Math.abs(delta.y) < 0.001 && (state.onGround || state.isInWater) } const simulationControl = { forward: this.bot.controlState.forward, back: this.bot.controlState.back, left: this.bot.controlState.left, right: this.bot.controlState.right, jump: this.bot.controlState.jump, sprint: this.bot.controlState.sprint, sneak: this.bot.controlState.sneak } const state = new PlayerState(this.bot, simulationControl) state.pos.update(n1) this.simulateUntil(reached, this.getController(n2, false, true), Math.floor(5 * n1.distanceTo(n2)), state) return reached(state) } canSprintJump (path, jumpAfter = 0) { const reached = this.getReached(path) const state = this.simulateUntil(reached, this.getController(path[0], true, true, jumpAfter), 20) return reached(state) } canWalkJump (path, jumpAfter = 0) { const reached = this.getReached(path) const state = this.simulateUntil(reached, this.getController(path[0], true, false, jumpAfter), 20) return reached(state) } getReached (path) { return (state) => { const delta = path[0].minus(state.pos) return Math.abs(delta.x) <= 0.35 && Math.abs(delta.z) <= 0.35 && Math.abs(delta.y) < 1 } } getController (nextPoint, jump, sprint, jumpAfter = 0) { return (state, tick) => { const dx = nextPoint.x - state.pos.x const dz = nextPoint.z - state.pos.z state.yaw = Math.atan2(-dx, -dz) state.control.forward = true state.control.jump = jump && tick >= jumpAfter state.control.sprint = sprint } } } module.exports = Physics ================================================ FILE: lib/shapes.js ================================================ const { Vec3 } = require('vec3') function getShapeFaceCenters (shapes, direction, half = null) { const faces = [] for (const shape of shapes) { const halfsize = new Vec3(shape[3] - shape[0], shape[4] - shape[1], shape[5] - shape[2]).scale(0.5) let center = new Vec3(shape[0] + shape[3], shape[1] + shape[4], shape[2] + shape[5]).scale(0.5) center = center.offset(halfsize.x * direction.x, halfsize.y * direction.y, halfsize.z * direction.z) if (half === 'top' && center.y <= 0.5) { if (Math.abs(direction.y) === 0) center.y += halfsize.y - 0.001 if (center.y <= 0.5) continue } else if (half === 'bottom' && center.y >= 0.5) { if (Math.abs(direction.y) === 0) center.y -= halfsize.y - 0.001 if (center.y >= 0.5) continue } faces.push(center) } return faces } module.exports = { getShapeFaceCenters } ================================================ FILE: package.json ================================================ { "name": "mineflayer-pathfinder", "version": "2.4.5", "description": "", "main": "index.js", "scripts": { "mocha_test": "mocha --reporter spec --exit", "mocha_debug": "mocha --inspect-brk --reporter spec --exit", "lint": "standard", "fix": "standard --fix", "test": "npm run lint && npm run mocha_test" }, "author": "Karang", "license": "MIT", "dependencies": { "minecraft-data": "^3.5.1", "prismarine-block": "^1.16.3", "prismarine-entity": "^2.1.1", "prismarine-item": "^1.11.5", "prismarine-nbt": "^2.2.1", "prismarine-physics": "^1.5.2", "vec3": "^0.2.0" }, "devDependencies": { "minecraft-wrap": "^1.2.1", "mineflayer": "^4.3.0", "mineflayer-pathfinder": "file:./", "mocha": "^11.0.1", "prismarine-schematic": "^1.2.3", "standard": "^17.0.0" } } ================================================ FILE: readme.md ================================================ # Mineflayer-pathfinder [![npm version](https://badge.fury.io/js/mineflayer-pathfinder.svg)](https://badge.fury.io/js/mineflayer-pathfinder) ![npm](https://img.shields.io/npm/dt/mineflayer-pathfinder) [![Try it on gitpod](https://img.shields.io/badge/try-on%20gitpod-brightgreen.svg)](https://gitpod.io/#https://github.com/PrismarineJS/mineflayer-pathfinder) [![Issue Hunt](https://github.com/BoostIO/issuehunt-materials/blob/master/v1/issuehunt-shield-v1.svg)](https://issuehunt.io/r/PrismarineJS/mineflayer-pathfinder) Pathfinding plugin for the Minecraft Bot API [Mineflayer](https://github.com/PrismarineJS/mineflayer). Create static, dynamic or composite goals to navigate Minecraft terrain fully autonomously. Mostly stable. Feel free to contribute by making suggestions or posting issues. ## Install ```bash npm install mineflayer-pathfinder ``` ## Tutorial & Explanation For a basic explanation of how to use mineflayer-pathfinder, you can read [this tutorial](./examples/tutorial/goalsExplained.md). ## Video Tutorials For a video tutorial explaining the usage of mineflayer-pathfinder, you can watch the following Youtube videos: [part 1](https://www.youtube.com/watch?v=UWGSf08wQSc) [part 2](https://www.youtube.com/watch?v=ssWE0kXDGJE) ## Example ```js const mineflayer = require('mineflayer') const pathfinder = require('mineflayer-pathfinder').pathfinder const Movements = require('mineflayer-pathfinder').Movements const { GoalNear } = require('mineflayer-pathfinder').goals const bot = mineflayer.createBot({ username: 'Player' }) bot.loadPlugin(pathfinder) bot.once('spawn', () => { const defaultMove = new Movements(bot) bot.on('chat', function(username, message) { if (username === bot.username) return const target = bot.players[username] ? bot.players[username].entity : null if (message === 'come') { if (!target) { bot.chat('I don\'t see you !') return } const p = target.position bot.pathfinder.setMovements(defaultMove) bot.pathfinder.setGoal(new GoalNear(p.x, p.y, p.z, 1)) } }) }) ``` ## Features * Optimized and modernized A* pathfinding * Complexe goals can be specified (inspired by [baritone goals](https://github.com/cabaletta/baritone/blob/master/FEATURES.md#goals) ) * Customizable movements generator * Each movement can have a different cost * Can break/place blocks as part of its deplacement * Automatically update path when environment change * Long distance paths * Can swim * Can avoid entities * Modular and easily extendable with different behavior ## API Considering there are a lot of deep changes that are being worked on, it could take some time before it's done Also, **for now**, there is only the `pathfinder` module, `movements` and `goals` still need to be done # Functions: ### bot.pathfinder.goto(goal) Returns a Promise with the path result. Resolves when the goal is reached. Rejects on error. * `goal` - Goal instance ### bot.pathfinder.bestHarvestTool(block) Returns the best harvesting tool in the inventory for the specified block. * `Returns` - `Item` instance or `null` * `block` - Block instance ### bot.pathfinder.getPathTo(movements, goal, timeout) * `Returns` - The path * `movements` - Movements instance * `goal` - Goal instance * `timeout` - number (optional, default `bot.pathfinder.thinkTimeout`) ### bot.pathfinder.getPathFromTo* (movements, startPos, goal, options = {}) Returns a Generator. The generator computes the path for as longs as no full path is found or `options.timeout` is reached. The generator will block the event loop until a path is found or `options.tickTimeout` (default to 50ms) is reached. * `Returns` - A generator instance. See [MDN function*](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*). * `movements` - Movements instance * `startPos` - A Vec3 instance. The starting position to base the path search from. * `goal` - Goal instance * `options` - A optional options object contains: * `optimizePath` - Boolean Optional. Optimize path for shortcuts like going to the next node in a strait line instead walking only diagonal or along axis. * `resetEntityIntersects` - Boolean Optional. Reset the `entityIntersections` index for `movements`. Default: true * `timeout` - Number Optional. Total computation timeout. * `tickTimeout` - Number Optional. Maximum amount off time before yielding. * `searchRadius` - Number Optional. Max distance to search. * `startMove` - instance of Move Optional. A optional starting position as a Move. Replaces `startPos` as the starting position. ### bot.pathfinder.setGoal(Goal, dynamic) * `goal` - Goal instance * `dynamic` - boolean (optional, default false) ### bot.pathfinder.setMovements(movements) Assigns the movements config. * `movements` - Movements instance ### bot.pathfinder.stop() Stops pathfinding as soon as the bot has reached the next node in the path (this prevents the bot from stopping mid-air). Emits `path_stop` when called. Note: to force stop immediately, use `bot.pathfinder.setGoal(null)` ### bot.pathfinder.isMoving() A function that checks if the bot is currently moving. * `Returns` - boolean ### bot.pathfinder.isMining() A function that checks if the bot is currently mining blocks. * `Returns` - boolean ### bot.pathfinder.isBuilding() A function that checks if the bot is currently placing blocks. * `Returns` - boolean # Properties: ### bot.pathfinder.thinkTimeout Think Timeout in milliseconds. * `Default` - `5000` ### bot.pathfinder.tickTimeout How many milliseconds per tick are allocated to thinking. * `Default` - `40` ### bot.pathfinder.searchRadius The search limiting radius, in blocks, if `-1` the search is not limited by distance. * `Default` - `-1` # Movement class This class configures how pathfinder plans its paths. It configures things like block breaking or different costs for moves. This class can be extended to add or change how pathfinder calculates its moves. ## Usage Pathfinder instantiates the default movement class by itself if no instance is specified. If you want to change values you should create a new instance of the Movements class, change it's values and set it as pathfinders new movement class. ### Example: ```js const { Movements } = require('mineflayer-pathfinder') // Import the Movements class from pathfinder bot.once('spawn', () => { // A new movement instance for specific behavior const defaultMove = new Movements(bot) defaultMove.allow1by1towers = false // Do not build 1x1 towers when going up defaultMove.canDig = false // Disable breaking of blocks when pathing defaultMove.scafoldingBlocks.push(bot.registry.itemsByName['netherrack'].id) // Add nether rack to allowed scaffolding items bot.pathfinder.setMovements(defaultMove) // Update the movement instance pathfinder uses // Do pathfinder things // ... }) ``` ## Movements class default properties Movement class properties and their default values. ### canDig Boolean to allow breaking blocks. * Default `true` ### digCost Additional cost for breaking blocks. * Default - `1` ### placeCost Additional cost for placing blocks. * Default - `1` ### maxDropDown Max drop down distance. Only considers drops that have blocks to land on. * Default - `4` ### infiniteLiquidDropdownDistance Option to ignore maxDropDown distance when the landing position is in water. * Default - `true` ### liquidCost Additional cost for interacting with liquids. * Default - `1` ### entityCost Additional cost for moving through an entity hitbox (besides passable ones). * Default - `1` ### dontCreateFlow Do not break blocks that touch liquid blocks. * Default - `true` ### dontMineUnderFallingBlock Do not break blocks that have a gravityBlock above. * Default - `true` ### allow1by1towers Allow pillaring up on 1x1 towers. * Default - `true` ### allowFreeMotion Allow to walk to the next node/goal in a straight line if terrain allows it. * Default - `false` ### allowParkour Allow parkour jumps like jumps over gaps bigger then 1 block. * Default - `true` ### allowSprinting Allow sprinting when moving. * Default - `true` ### allowEntityDetection Test for entities that may obstruct path or prevent block placement. Grabs updated entities every new path. * Default - `true` ### entitiesToAvoid Set of entities (by bot.registry name) to completely avoid when using entity detection. * instance of `Set` ### passableEntities Set of entities (by bot.registry name) to ignore when using entity detection. * instance of `Set` * Default - See lib/passableEntities.json ### interactableBlocks Set of blocks (by bot.registry name) that pathfinder should not attempt to place blocks or 'right click' on. * instance of `Set` * Default - See lib/interactable.json ### blocksCantBreak Set of block id's pathfinder cannot break. Includes chests and all unbreakable blocks. * instance of `Set` ### blocksToAvoid Set of block id's to avoid. * instance of `Set` ### liquids Set of liquid block id's. * instance of `Set` ### climbables Set of block id's that are climable. Note: Currently unused as pathfinder cannot use climables. * instance of `Set` ### replaceables Set of block id's that can be replaced when placing blocks. * instance of `Set` ### scafoldingBlocks Array of item id's that can be used as scaffolding blocks. * Default - `[]` ### gravityBlocks Set of block id's that can fall on bot's head. * instance of `Set` ### fences Set of block id's that are fences or blocks that have a collision box taller then 1 block. * instance of `Set` ### carpets Set of all carpet block id's or blocks that have a collision box smaller then 0.1. These blocks are considered safe to walk in. * instance of `Set` ### exclusionAreasStep An array of functions that define an area or block to be step on excluded. Every function in the array is parsed the Block the bot is planing to step on. Each function should return a positive number (includes 0) that defines extra cost for that specific Block. 0 means no extra cost, 100 means it is impossible for pathfinder to consider this move. * Array of functions `(block: Block) => number` ### exclusionAreasBreak An array of functions that define an area or block to be break excluded. Every function in the array is parsed the Block the bot is planing to break. Each function should return a positive number (includes 0) that defines extra cost for that specific Block. 0 means no extra cost, 100 means it is impossible for pathfinder to consider this move. * Array of functions `(block: Block) => number` ### exclusionAreasPlace An array of functions that define an area to be block placement excluded. Every function in the array is parsed the current Block the bot is planing to place a block inside (should be air or a replaceable block most of the time). Each function should return a positive number (includes 0) that defines extra cost for that specific Block. 0 means no extra cost, 100 makes it impossible for pathfinder to consider this move. * Array of functions `(block: Block) => number` ### entityIntersections A dictionary of the number of entities intersecting each floored block coordinate. Updated automatically for each path, but you may mix in your own entries before calculating a path if desired (generally for testing). To prevent this from being cleared automatically before generating a path,s see the [path gen options](#botpathfindergetpathfromto-movements-startpos-goal-options--). * Formatted entityIntersections['x,y,z'] = #ents * Dictionary of costs `{string: number}` ### canOpenDoors Enable feature to open Fence Gates. Unreliable and known to be buggy. * Default - `false` # Events: ### goal_reached Called when the goal has been reached. Not called for dynamic goals. ### path_update Called whenever the path is recalculated. Status may be: * `success` a path has been found * `partial` a partial path has been found, computations will continue next tick * `timeout` timed out * `noPath` no path was found ### goal_updated Called whenever a new goal is assigned to the pathfinder. ### path_reset Called when the path is reset, with a reason: * `goal_updated` * `movements_updated` * `block_updated` * `chunk_loaded` * `goal_moved` * `dig_error` * `no_scaffolding_blocks` * `place_error` * `stuck` ### path_stop Called when the pathing has been stopped by `bot.pathfinder.stop()` # Goals: ### Goal Abstract Goal class. Do not instantiate this class. Instead extend it to make a new Goal class. Has abstract methods: - `heuristic(node)` * `node` - A path node * Returns a heuristic number value for a given node. Must be admissible – meaning that it never overestimates the actual cost to get to the goal. - `isEnd(node)` * `node` * Returns a boolean value if the given node is a end node. Implements default methods for: - `isValid()` * Always returns `true` - `hasChanged(node)` * `node` - A path node * Always returns `false` ### GoalBlock(x, y, z) One specific block that the player should stand inside at foot level * `x` - Integer * `y` - Integer * `z` - Integer ### GoalNear(x, y, z, range) A block position that the player should get within a certain radius of * `x` - Integer * `y` - Integer * `z` - Integer * `range` - Integer ### GoalXZ(x, z) Useful for long-range goals that don't have a specific Y level * `x` - Integer * `z` - Integer ### GoalNearXZ(x, z, range) Useful for finding builds that you don't have an exact Y level for, just an approximate X and Z level. * `x` - Integer * `z` - Integer * `range` - Integer ### GoalY(y) Get to a Y level. * `y` - Integer ### GoalGetToBlock(x, y, z) Don't get into the block, but get directly adjacent to it. Useful for chests. * `x` - Integer * `y` - Integer * `z` - Integer ### GoalCompositeAny(Array\?) A composite of many goals, any one of which satisfies the composite. For example, a GoalCompositeAny of block goals for every oak log in loaded chunks would result in it pathing to the easiest oak log to get to. * `Array` - Array of goals ### GoalCompositeAll(Array\?) A composite of multiple goals, requiring all of them to be satisfied. * `Array` - Array of goals ### GoalInvert(goal) Inverts the goal. * `goal` - Goal to invert ### GoalFollow(entity, range) Follows an entity. * `entity` - Entity instance * `range` - Integer ### GoalPlaceBlock(pos, world, options) Position the bot in order to place a block. * `pos` - Vec3 the position of the placed block * `world` - the world of the bot (Can be accessed with `bot.world`) * `options` - object containing all optionals properties: * `range` - maximum distance from the clicked face * `faces` - the directions of the faces the player can click * `facing` - the direction the player must be facing * `facing3D` - boolean, facing is 3D (true) or 2D (false) * `half` - `top` or `bottom`, the half that must be clicked ### GoalLookAtBlock(pos, world, options = {}) Path into a position were a blockface of block at pos is visible. Fourth argument is optional and contains extra options. * `pos` - Vec3 the block position to look at * `world` - the world of the bot (Can be accessed with `bot.world`) * `options` - object containing all optionals properties: * `reach` - number maximum distance from the clicked face. Default `4.5` * `entityHeight` - number Default is `1.6` ### GoalBreakBlock(x, y, z, bot, options) Deprecated. Wrapper for GoalLookAtBlock. Use GoalLookAtBlock instead. ================================================ FILE: test/internalTest.js ================================================ /* eslint-env mocha */ const mineflayer = require('mineflayer') const { goals, pathfinder, Movements } = require('mineflayer-pathfinder') const { Vec3 } = require('vec3') const mc = require('minecraft-protocol') const assert = require('assert') const { v4: uuidv4 } = require('uuid') const PEntity = require('prismarine-entity') const { once, on } = require('events') const { Schematic } = require('prismarine-schematic') const { promises: fs } = require('fs') const path = require('path') const Physics = require('../lib/physics') const Version = '1.16.5' const ServerPort = 25567 /** * Returns a flat bedrock chunk with a single gold block in it. * @param {string} Version version * @returns {import('prismarine-chunk').Chunk} */ function flatMap (Version) { const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position const Block = require('prismarine-block')(Version) const Chunk = require('prismarine-chunk')(Version) const mcData = require('minecraft-data')(Version) const chunk = new Chunk() chunk.initialize((x, y, z) => { if (targetBlock.x === x && targetBlock.y === y && targetBlock.z === z) { return new Block(mcData.blocksByName.gold_block.id, 1, 0) } return y === 0 ? new Block(mcData.blocksByName.bedrock.id, 1, 0) : new Block(mcData.blocksByName.air.id, 1, 0) // Bedrock floor }) return chunk } /** * Reads the schematic parkour1.schem and returns a chunk containing the schematic content. * @param {string} Version version to be used * @returns {Promise} */ async function parkourMap (Version) { const pwd = path.join(__dirname, './schematics/parkour1.schem') const readSchem = await Schematic.read(await fs.readFile(pwd), '1.18.2') const Block = require('prismarine-block')(Version) const Chunk = require('prismarine-chunk')(Version) const mcData = require('minecraft-data')(Version) const chunk = new Chunk() chunk.initialize((x, y, z) => { const block = readSchem.getBlock(new Vec3(x, y, z)) if (block.name === 'air') return null // Different versions off schematic are not compatible with each other. Assumes block names between versions stay the same. const blockVersion = mcData.blocksByName[block.name] if (!blockVersion) return null return new Block(blockVersion.id, 1, 0) }) return chunk } function generateChunkPacket (chunk) { const lights = chunk.dumpLight() return { x: 0, z: 0, groundUp: true, biomes: chunk.dumpBiomes !== undefined ? chunk.dumpBiomes() : undefined, heightmaps: { type: 'compound', name: '', value: { MOTION_BLOCKING: { type: 'longArray', value: new Array(36).fill([0, 0]) } } }, // send fake heightmap bitMap: chunk.getMask(), chunkData: chunk.dump(), blockEntities: [], trustEdges: false, skyLightMask: lights?.skyLightMask, blockLightMask: lights?.blockLightMask, emptySkyLightMask: lights?.emptySkyLightMask, emptyBlockLightMask: lights?.emptyBlockLightMask, skyLight: lights?.skyLight, blockLight: lights?.blockLight } } /** * Create a new 1.16 server and handle when clients connect. * @param {import('minecraft-protocol').Server} server * @param {import('vec3').Vec3} spawnPos * @param {string} Version * @param {boolean} useLoginPacket * @returns {Promise} */ async function newServer (server, chunk, spawnPos, Version, useLoginPacket) { const mcData = require('minecraft-data')(Version) server = mc.createServer({ 'online-mode': false, version: Version, // 25565 - local server, 25566 - proxy server port: ServerPort }) server.on('login', (client) => { let loginPacket if (useLoginPacket) { loginPacket = mcData.loginPacket } else { loginPacket = { entityId: 0, levelType: 'fogetaboutit', gameMode: 0, previousGameMode: 255, worldNames: ['minecraft:overworld'], dimension: 0, worldName: 'minecraft:overworld', hashedSeed: [0, 0], difficulty: 0, maxPlayers: 20, reducedDebugInfo: 1, enableRespawnScreen: true } } client.write('login', loginPacket) client.write('map_chunk', generateChunkPacket(chunk)) client.write('position', { x: spawnPos.x, y: spawnPos.y, z: spawnPos.z, yaw: 0, pitch: 0, flags: 0x00 }) }) await once(server, 'listening') return server } function add1x2Weight (entityIntersections, posX, posY, posZ, weight = 1) { entityIntersections[`${posX},${posY},${posZ}`] = entityIntersections[`${posX},${posY},${posZ}`] ?? 0 entityIntersections[`${posX},${posY + 1},${posZ}`] = entityIntersections[`${posX},${posY + 1},${posZ}`] ?? 0 entityIntersections[`${posX},${posY},${posZ}`] += weight entityIntersections[`${posX},${posY + 1},${posZ}`] += weight } describe('pathfinder Goals', function () { const mcData = require('minecraft-data')(Version) const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */ let bot /** @type { import('minecraft-protocol').Server } */ let server before(async () => { const chunk = flatMap(Version) server = await newServer(server, chunk, spawnPos, Version, true) bot = mineflayer.createBot({ username: 'player', version: Version, port: ServerPort }) await once(bot, 'chunkColumnLoad') }) after(() => { bot.end() bot = null server.close() }) describe('Goals', () => { beforeEach(() => { bot.entity.position = spawnPos.clone() }) it('GoalBlock', () => { const goal = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z) assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = targetBlock.clone() assert.ok(goal.isEnd(bot.entity.position)) }) it('GoalNear', () => { const goal = new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1) assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = targetBlock.offset(1, 0, 0) assert.ok(goal.isEnd(bot.entity.position)) }) it('GoalXZ', () => { const goal = new goals.GoalXZ(targetBlock.x, targetBlock.z) assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = targetBlock.offset(0, 1, 0) assert.ok(goal.isEnd(bot.entity.position)) }) it('GoalNearXZ', () => { const goal = new goals.GoalNearXZ(targetBlock.x, targetBlock.z, 1) assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = targetBlock.offset(1, 0, 0) assert.ok(goal.isEnd(bot.entity.position)) }) it('GoalY', () => { const goal = new goals.GoalY(targetBlock.y + 1) assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = targetBlock.offset(0, 1, 0) assert.ok(goal.isEnd(bot.entity.position)) }) it('GoalGetToBlock', () => { const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z) assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = targetBlock.offset(1, 0, 0) assert.ok(goal.isEnd(bot.entity.position)) }) it('GoalCompositeAny', () => { const targetBlock2 = new Vec3(10, 1, 0) const goal1 = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z) const goal2 = new goals.GoalBlock(targetBlock2.x, targetBlock2.y, targetBlock2.z) const goalComposite = new goals.GoalCompositeAny() goalComposite.goals = [goal1, goal2] assert.ok(!goalComposite.isEnd(bot.entity.position)) bot.entity.position = targetBlock.clone() assert.ok(goalComposite.isEnd(bot.entity.position)) // target block 1 bot.entity.position = targetBlock2.clone() assert.ok(goalComposite.isEnd(bot.entity.position)) // target block 2 }) it('GoalCompositeAll', () => { const targetBlock = new Vec3(2, 1, 0) const block2 = new Vec3(3, 1, 0) const goal1 = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z) const goal2 = new goals.GoalNear(block2.x, block2.y, block2.z, 2) const goalComposite = new goals.GoalCompositeAll() goalComposite.goals = [goal1, goal2] assert.ok(!goalComposite.isEnd(bot.entity.position)) bot.entity.position = targetBlock.offset(0, 0, 0) assert.ok(goalComposite.isEnd(bot.entity.position)) }) it('GoalInvert', () => { const goalBlock = new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z) const goal = new goals.GoalInvert(goalBlock) bot.entity.position = targetBlock.clone() assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = new Vec3(0, 1, 0) assert.ok(goal.isEnd(bot.entity.position)) }) it('GoalPlaceBlock', () => { const placeTarget = targetBlock.offset(0, 1, 0) const goal = new goals.GoalPlaceBlock(placeTarget, bot.world, {}) bot.entity.position = targetBlock.offset(-5, 0, 0) // to far away to reach assert.ok(!goal.isEnd(bot.entity.position.floored())) bot.entity.position = targetBlock.offset(-2, 0, 0) assert.ok(goal.isEnd(bot.entity.position.floored())) }) it('GoalLookAtBlock', () => { const breakTarget = targetBlock.clone() // should be a gold block or any other block thats dig able const goal = new goals.GoalLookAtBlock(breakTarget, bot.world, { reach: 3 }) assert.ok(!goal.isEnd(bot.entity.position.floored())) bot.entity.position = targetBlock.offset(-2, 0, 0) // should now be close enough assert.ok(goal.isEnd(bot.entity.position.floored())) }) }) describe('Goals with entity', () => { beforeEach(() => { bot.entity.position = spawnPos.clone() }) before((done) => { const Entity = PEntity(Version) const chicken = new Entity(mcData.entitiesByName.chicken.id) const client = Object.values(server.clients)[0] client.write('spawn_entity', { // Might only work for 1.16 entityId: chicken.id, objectUUID: uuidv4(), type: chicken.type, x: targetBlock.x, y: targetBlock.y + 1, z: targetBlock.z, pitch: 0, yaw: 0, objectData: 0, velocityX: 0, velocityY: 0, velocityZ: 0 }) setTimeout(done, 100) }) it('GoalFollow', () => { const entity = bot.nearestEntity() || { position: targetBlock.offset(0, 1, 0) } const goal = new goals.GoalFollow(entity, 1) assert.ok(!goal.isEnd(bot.entity.position)) bot.entity.position = targetBlock.clone() assert.ok(goal.isEnd(bot.entity.position)) }) }) }) describe('pathfinder events', function () { const mcData = require('minecraft-data')(Version) const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */ let bot /** @type { import('minecraft-protocol').Server } */ let server before(async () => { const chunk = flatMap(Version) server = await newServer(server, chunk, spawnPos, Version, true) bot = mineflayer.createBot({ username: 'player', version: Version, port: ServerPort }) await once(bot, 'chunkColumnLoad') bot.loadPlugin(pathfinder) bot.pathfinder.setMovements(new Movements(bot, mcData)) }) after(() => server.close()) describe('events', async function () { beforeEach(() => { bot.entity.position = spawnPos.clone() }) afterEach((done) => { bot.pathfinder.setGoal(null) setTimeout(done) const listeners = ['goal_reached', 'goal_updated', 'path_update', 'path_stop'] listeners.forEach(l => bot.removeAllListeners(l)) }) it('goal_reached', function (done) { this.timeout(3000) this.slow(1000) bot.once('goal_reached', () => done()) bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1)) }) it('goal_updated', function (done) { this.timeout(100) bot.once('goal_updated', () => done()) bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1)) }) it('path_update', function (done) { this.timeout(3000) this.slow(1000) bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1)) bot.once('path_update', () => done()) }) it('path_stop', function (done) { this.timeout(3000) this.slow(1000) bot.pathfinder.setGoal(new goals.GoalNear(targetBlock.x, targetBlock.y, targetBlock.z, 1)) bot.once('path_stop', () => done()) bot.pathfinder.stop() }) }) }) describe('pathfinder util functions', function () { const mcData = require('minecraft-data')(Version) const Item = require('prismarine-item')(Version) const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block const itemsToGive = [new Item(mcData.itemsByName.diamond_pickaxe.id, 1), new Item(mcData.itemsByName.dirt.id, 64)] /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */ let bot /** @type { import('minecraft-protocol').Server } */ let server before(async () => { const chunk = flatMap(Version) server = await newServer(server, chunk, spawnPos, Version, true) bot = mineflayer.createBot({ username: 'player', version: Version, port: ServerPort }) await once(bot, 'chunkColumnLoad') itemsToGive.forEach(item => { const slot = bot.inventory.firstEmptyHotbarSlot() bot.inventory.slots[slot] = item }) bot.loadPlugin(pathfinder) bot.pathfinder.setMovements(new Movements(bot, mcData)) }) after(() => server.close()) describe('paththing', function () { this.afterEach((done) => { bot.pathfinder.setGoal(null) bot.entity.position = spawnPos.clone() bot.stopDigging() setTimeout(() => done()) }) it('Goto', async function () { this.timeout(3000) this.slow(1500) await bot.pathfinder.goto(new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z)) }) it('isMoving', function (done) { bot.pathfinder.setGoal(new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z)) const foo = () => { if (bot.pathfinder.isMoving()) { bot.removeListener('physicTick', foo) done() } } bot.on('physicTick', foo) }) // Note: Ordering seams to matter when running the isBuilding test. If run after isMining isBuilding does not seam to work. it('isBuilding', function (done) { this.timeout(5000) this.slow(1500) bot.pathfinder.setGoal(new goals.GoalBlock(targetBlock.x, targetBlock.y + 2, targetBlock.z)) const foo = () => { if (bot.pathfinder.isBuilding()) { bot.removeListener('physicTick', foo) bot.stopDigging() done() } } bot.on('physicTick', foo) }) it('isMining', function (done) { this.timeout(5000) this.slow(1500) bot.pathfinder.setGoal(new goals.GoalBlock(targetBlock.x, targetBlock.y, targetBlock.z)) const foo = () => { if (bot.pathfinder.isMining()) { bot.removeListener('physicTick', foo) bot.stopDigging() done() } } bot.on('physicTick', foo) }) }) it('bestHarvestTool', function () { const block = bot.blockAt(targetBlock) const tool = bot.pathfinder.bestHarvestTool(block) assert.deepStrictEqual(tool, itemsToGive[0]) }) it('getPathTo', function () { const path = bot.pathfinder.getPathTo(bot.pathfinder.movements, new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(path.status, 'success') assert.ok(path.visitedNodes < 5, `Generated path visited nodes to high (${path.visitedNodes} < 5)`) assert.ok(path.generatedNodes < 30, `Generated path nodes to high (${path.generatedNodes} < 30)`) assert.ok(path.path.length === 3, `Generated path length wrong (${path.path.length} === 3)`) assert.ok(path.time < 50, `Generated path took too long (${path.time} < 50)`) }) }) describe('pathfinder Movement', function () { const mcData = require('minecraft-data')(Version) const Item = require('prismarine-item')(Version) const Block = require('prismarine-block')(Version) const targetBlock = new Vec3(12, 1, 8) // a gold block away from the spawn position const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */ let bot /** @type { import('minecraft-protocol').Server } */ let server /** @type { import('mineflayer-pathfinder').Movements } */ let defaultMovement const itemsToGive = [new Item(mcData.itemsByName.diamond_pickaxe.id, 1), new Item(mcData.itemsByName.dirt.id, 64)] before(async () => { const chunk = flatMap(Version) server = await newServer(server, chunk, spawnPos, Version, true) bot = mineflayer.createBot({ username: 'player', version: Version, port: ServerPort }) await once(bot, 'chunkColumnLoad') itemsToGive.forEach(item => { const slot = bot.inventory.firstEmptyHotbarSlot() bot.inventory.slots[slot] = item }) defaultMovement = new Movements(bot, mcData) bot.loadPlugin(pathfinder) bot.pathfinder.setMovements(defaultMovement) }) after(() => server.close()) it('countScaffoldingItems', function () { assert.strictEqual(defaultMovement.countScaffoldingItems(), 64) }) it('getScaffoldingItem', function () { assert.strictEqual(defaultMovement.getScaffoldingItem(), itemsToGive[1]) }) it('getBlock', function () { assert.ok(defaultMovement.getBlock(targetBlock, 0, 0, 0).type === mcData.blocksByName.gold_block.id) }) describe('safeToBreak world editing', function () { this.afterAll(async () => { defaultMovement.canDig = true await bot.world.setBlock(targetBlock.offset(1, 0, 0), new Block(mcData.blocksByName.air.id, 0)) }) it('safeToBreak', async function () { const block = bot.blockAt(targetBlock) assert.ok(defaultMovement.safeToBreak(block)) defaultMovement.canDig = false assert.ok(!defaultMovement.safeToBreak(block)) defaultMovement.canDig = true await bot.world.setBlock(targetBlock.offset(1, 0, 0), new Block(mcData.blocksByName.water.id, 0, 0)) assert.ok(!defaultMovement.safeToBreak(block)) }) }) it('safeOrBreak', function () { const block = defaultMovement.getBlock(targetBlock, 0, 0, 0) const toBreak = [] const extraValue = defaultMovement.safeOrBreak(block, toBreak) assert.ok(extraValue < 100, `safeOrBreak to high for block (${extraValue} < 100)`) assert.ok(toBreak.length === 1, `safeOrBreak toBreak array wrong length ${toBreak.length} (${toBreak.length} === 1)`) }) it('getMoveJumpUp', function () { const block = defaultMovement.getBlock(targetBlock, -1, 0, 0) const dir = new Vec3(1, 0, 0) const neighbors = [] defaultMovement.getMoveJumpUp(block.position, dir, neighbors) assert.ok(neighbors.length === 1, `getMoveJumpUp neighbors not right length (${neighbors.length} === 1)`) }) it('getMoveForward', function () { const dir = new Vec3(1, 0, 0) const neighbors = [] defaultMovement.getMoveForward(targetBlock, dir, neighbors) assert.ok(neighbors.length === 1, `getMoveForward neighbors not right length (${neighbors.length} === 1)`) }) it('getMoveDiagonal', function () { const dir = new Vec3(1, 0, 0) const neighbors = [] defaultMovement.getMoveDiagonal(targetBlock, dir, neighbors) assert.ok(neighbors.length === 1, `getMoveDiagonal neighbors not right length (${neighbors.length} === 1)`) }) it('getLandingBlock', function () { const node = targetBlock.offset(-1, 3, 0) const dir = new Vec3(1, 0, 0) const block = defaultMovement.getLandingBlock(node, dir) assert.ok(block != null, 'Landing block is null') if (!block) return assert.ok(block.type === mcData.blocksByName.air.id, `getLandingBlock not the right block (${block.name} === air)`) assert.ok(block.position.offset(0, -1, 0).distanceSquared(targetBlock) === 0, `getLandingBlock not landing (${block.position.offset(0, -1, 0).distanceSquared(targetBlock)}) on target block: ${defaultMovement.getBlock(block.position, 0, -1, 0).name}`) }) it('getMoveDropDown', function () { const dir = new Vec3(1, 0, 0) const neighbors = [] defaultMovement.getMoveDropDown(targetBlock.offset(-1, 4, 0), dir, neighbors) assert.ok(neighbors.length === 1, `getMoveDropDown neighbors not right length (${neighbors.length} === 1)`) }) it('getMoveDown', function () { const neighbors = [] defaultMovement.getMoveDown(targetBlock.offset(0, 4, 0), neighbors) assert.ok(neighbors.length === 1, `getMoveDown neighbors not right length (${neighbors.length} === 1)`) }) it('getMoveUp', function () { const neighbors = [] defaultMovement.getMoveUp(targetBlock.offset(0, 1, 0), neighbors) assert.ok(neighbors.length === 1, `getMoveUp neighbors not right length (${neighbors.length} === 1)`) }) it('getNeighbors', function () { const neighbors = defaultMovement.getNeighbors(targetBlock.offset(0, 1, 0)) assert.ok(neighbors.length > 0, 'getNeighbors length 0') }) }) describe('Parkour path test', function () { const mcData = require('minecraft-data')(Version) const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */ let bot /** @type { import('minecraft-protocol').Server } */ let server /** @type { import('mineflayer-pathfinder').Movements } */ let defaultMovement const parkourSpawn1 = new Vec3(0.5, 3, 12.5) const parkourSpawn2 = new Vec3(5.5, 3, 12.5) before(async () => { this.timeout(5000) const chunk = await parkourMap(Version) server = await newServer(server, chunk, spawnPos, Version, true) bot = mineflayer.createBot({ username: 'player', version: Version, port: ServerPort }) await once(bot, 'chunkColumnLoad') defaultMovement = new Movements(bot, mcData) bot.loadPlugin(pathfinder) bot.pathfinder.setMovements(defaultMovement) }) after(() => server.close()) it('getMoveParkourForward-1', function () { const dirs = [new Vec3(0, 0, 1), new Vec3(0, 0, -1)] for (let i = 0; i < dirs.length; i++) { const dir = dirs[i] // only 2 dirs as the schematic parkour1.schem only has 2 other blocks to path to. const neighbors = [] defaultMovement.getMoveParkourForward(parkourSpawn1, dir, neighbors) assert.ok(neighbors.length === 1, `getMoveParkourForward jump off gold block neighbors not right length (${neighbors.length} === 1)`) } }) it('getMoveParkourForward-2', function () { const dirs = [new Vec3(1, 0, 0), new Vec3(0, 0, 1), new Vec3(-1, 0, 0), new Vec3(0, 0, -1)] for (let i = 0; i < dirs.length; i++) { const dir = dirs[i] const neighbors = [] defaultMovement.getMoveParkourForward(parkourSpawn2, dir, neighbors) assert.ok(neighbors.length === 1, `getMoveParkourForward jump off gold block neighbors not right length (${neighbors.length} === 1)`) } }) }) describe('Physics test', function () { const mcData = require('minecraft-data')(Version) const spawnPos = new Vec3(8.5, 1, 8.5) // Center of the chunk & center of the block /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */ let bot /** @type { import('minecraft-protocol').Server } */ let server /** @type { import('mineflayer-pathfinder').Movements } */ let defaultMovement const parkourSpawn1 = new Vec3(0.5, 3, 12.5) // const parkourSpawn2 = new Vec3(5.5, 3, 12.5) before(async () => { this.timeout(5000) const chunk = await parkourMap(Version) server = await newServer(server, chunk, spawnPos, Version, true) bot = mineflayer.createBot({ username: 'player', version: Version, port: ServerPort }) await once(bot, 'chunkColumnLoad') defaultMovement = new Movements(bot, mcData) bot.loadPlugin(pathfinder) bot.pathfinder.setMovements(defaultMovement) }) after(() => server.close()) it('simulateUntil', async function () { this.slow(1000) this.timeout(2000) const ticksToSimulate = 10 const ticksPressForward = 5 bot.entity.position = parkourSpawn1.clone() bot.entity.velocity = new Vec3(0, 0, 0) // Wait for the bot to be on the ground so bot.entity.onGround == true bot.clearControlStates() await once(bot, 'physicTick') await once(bot, 'physicTick') const physics = new Physics(bot) const simulatedSteps = [] const realSteps = [] const controller = (state, counter) => { state.control.forward = counter <= ticksPressForward state.control.jump = counter <= ticksPressForward simulatedSteps.push(state.pos.toString() + ' Input:' + String(counter <= ticksPressForward)) } const state = physics.simulateUntil(() => false, controller, ticksToSimulate) simulatedSteps.push(state.pos.toString() + ' Input:false') // We have to be carful to not mess up the event scheduling. for await on(bot, 'physicTick') seams to work. // A for loop with just await once(bot, 'physicTick') does not always seam to work. What also works is attaching // a listener to bot with bot.on('physicTick', listener) but this is a lot nicer. let tick = 0 for await (const _ of on(bot, 'physicTick')) { // eslint-disable-line no-unused-vars bot.setControlState('forward', tick <= ticksPressForward) bot.setControlState('jump', tick <= ticksPressForward) realSteps.push(bot.entity.position.toString() + ' Input:' + String(tick <= ticksPressForward)) tick++ if (tick > ticksToSimulate) break } bot.clearControlStates() // console.info(bot.entity.position.toString(), console.info(state.pos.toString())) assert.ok(bot.entity.position.distanceSquared(state.pos) < 0.01, `Simulated states don't match Bot: ${bot.entity.position.toString()} !== Simulation: ${state.pos.toString()}` // + '\nSimulated Steps:\n' // + simulatedSteps.join('\n') + '\n' // + 'Real steps:\n' // + realSteps.join('\n') ) }) // TODO: write test for simulateUntilNextTick }) describe('pathfinder entity avoidance test', function () { const mcData = require('minecraft-data')(Version) const patherOptions = { resetEntityIntersects: false } const maxPathTime = 50 const spawnPos = new Vec3(8.5, 1.0, 8.5) // Center of the chunk & center of the block /** @type { import('mineflayer').Bot & { pathfinder: import('mineflayer-pathfinder').Pathfinder }} */ let bot /** @type { import('minecraft-protocol').Server } */ let server /** @type { import('prismarine-chunk').Chunk } */ let chunk before(async () => { chunk = await parkourMap(Version) server = await newServer(server, chunk, spawnPos, Version, true) bot = mineflayer.createBot({ username: 'player', version: Version, port: ServerPort }) await once(bot, 'chunkColumnLoad') bot.loadPlugin(pathfinder) bot.pathfinder.setMovements(new Movements(bot, mcData)) }) after(() => { bot.end() bot = null server.close() }) /** * Ensure algorithm does not impede performance when handling a large number of entities */ it('entityIndexPerformance', () => { const { performance } = require('perf_hooks') const targetBlock = new Vec3(11.5, 2.0, 10.5) // a gold block away from the spawn position const startPos = new Vec3(11.5, 2.0, 14.5) // Start point for test const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z) for (let i = 1; i <= 10000; i++) { const pos = (i % 2) === 0 ? new Vec3(10.5, 2.0, 12.5) : new Vec3(12.5, 2.0, 12.5) bot.entities[i] = { name: 'testEntity', position: pos, height: 2.0, width: 1.0 } } const beforeTime = performance.now() const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal) const { value: { result } } = generator.next() const timeElapsed = performance.now() - beforeTime bot.pathfinder.movements.clearCollisionIndex() for (let i = 1; i <= 10000; i++) { delete bot.entities[i] } assert.ok(timeElapsed < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) }) /** * Tests if bot will prefer a basic path with less entities * The test course is a 3x3x2 with a divider in the center * [O] = Open, [W] = Wall, [S] = Start, [E] = End * W E W * W O O O W * W O W O W * W O O O W * W S W */ describe('Weighted Path Avoidance', () => { const targetBlock = new Vec3(11.5, 2.0, 10.5) // a gold block away from the spawn position const startPos = new Vec3(11.5, 2.0, 14.5) // Start point for test const firstLeftNode = new Vec3(10.5, 2.0, 12.5) const firstRightNode = new Vec3(12.5, 2.0, 12.5) const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z) beforeEach((done) => { bot.pathfinder.movements.clearCollisionIndex() setTimeout(done, 100) }) /** * By default, algorithm will favor the Left Path * [X] = Ent, [O] = Open, [W] = Wall * O O O * O W O * O O O */ it('defaultPath', () => { const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`) assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`) }) /** * Ensure path with weight is avoided * [X] = Ent, [O] = Open, [W] = Wall * O O O * O W X * O O O */ it('rightBranchObstructed', () => { add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 12) const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`) assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`) }) /** * Ensure path with more weight is avoided * [X] = Ent, [O] = Open, [W] = Wall * O O O * X W X * X O O */ it('leftBranchMoreObstructed', () => { add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 12) add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 12) add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 13) const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`) assert.ok(rightBranch === true, `Generated path did not follow Right Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`) }) /** * Ensure blocks adjacent to diagonal nodes are detected * [X] = Ent, [O] = Open, [W] = Wall * O O X * X W O * O O X */ it('rightBranchDiagsClear', () => { add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 13) add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 11) add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 12) const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`) assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`) }) /** * Ensure blocks adjacent to diagonal nodes are detected * [X] = Ent, [O] = Open, [W] = Wall * X O O * O W X * X O O */ it('leftBranchDiagsClear', () => { add1x2Weight(bot.pathfinder.movements.entityIntersections, 12, 2, 12) add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 13) add1x2Weight(bot.pathfinder.movements.entityIntersections, 10, 2, 11) const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 3, `Generated path length wrong (${path.length} === 3)`) assert.ok(rightBranch === true, `Generated path did not follow Right Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}]`) }) }) /** * Tests if bot will try to path where they cannot build due to an entity and whether it will * try to break a block that would potentially cause an entity to fall. * The test course is a 2x2x4 pit where the start is at the bottom and the end is at the top * [O] = Open, [W] = Wall, [S] = Start, [E] = End * W W W E * W O O W * W S O W * W W W W */ describe('Construction Path Avoidance', () => { const Item = require('prismarine-item')(Version) const scaffoldItemId = mcData.itemsByName.dirt.id const groundYPos = 2 const lidYPos = 5 const forwardPos = { x: 11, z: 6 } const leftPos = { x: 10, z: 6 } const rightPos = { x: 11, z: 7 } const backPos = { x: 10, z: 7 } const targetBlock = new Vec3(forwardPos.x + 1.5, lidYPos + 1.0, forwardPos.z - 0.5) // a gold block away from the spawn position. One block diagonal from forward const startPos = new Vec3(backPos.x + 0.5, groundYPos, backPos.z + 0.5) // Start point for test const firstLeftNode = new Vec3(leftPos.x + 0.5, groundYPos, leftPos.z + 0.5) const firstRightNode = new Vec3(rightPos.x + 0.5, groundYPos, rightPos.z + 0.5) const firstForwardNode = new Vec3(forwardPos.x + 0.5, groundYPos, forwardPos.z + 0.5) const firstBackNode = startPos.clone().plus(new Vec3(-0.5, 1, -0.5)) // Jump up isn't going to half block and targets one block higher const blockersToPlace = [forwardPos, leftPos, rightPos, backPos] const goal = new goals.GoalGetToBlock(targetBlock.x, targetBlock.y, targetBlock.z) /** @type { import('minecraft-protocol').Client } */ let serverClient /** @type { number } */ let hotbarSlot before(() => { serverClient = Object.values(server.clients)[0] hotbarSlot = bot.inventory.firstEmptyHotbarSlot() }) beforeEach((done) => { bot.pathfinder.movements.clearCollisionIndex() bot.inventory.slots[hotbarSlot] = new Item(scaffoldItemId, 64) setTimeout(done, 100) }) afterEach(async () => { blockersToPlace.forEach(hPos => { const blockPos = { x: hPos.x, y: lidYPos, z: hPos.z } serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.air.id }) chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.air.id) }) serverClient.write('map_chunk', generateChunkPacket(chunk)) await once(bot, 'chunkColumnLoad') }) /** * By default, algorithm will favor the Backward Path * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall * W W W W * W O O W * W O O W * W W W W */ it('defaultPath', () => { const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode)) const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`) assert.ok(backwardBranch === true, `Generated path did not follow Backward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`) }) /** * Ensure bot finds a path when it cannot break left blocker with ent on top * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall * W W W W * W O O W * W + O W * W W W W */ it('backPathObstructed', async () => { const blockPos = { x: backPos.x, y: lidYPos, z: backPos.z } serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.dirt.id }) chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.dirt.id) add1x2Weight(bot.pathfinder.movements.entityIntersections, blockPos.x, blockPos.y + 1, blockPos.z) serverClient.write('map_chunk', generateChunkPacket(chunk)) await once(bot, 'chunkColumnLoad') const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode)) const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`) assert.ok(rightBranch === true, `Generated path did not follow Right Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`) }) /** * If there are blocks capping the pit with an entity above each block, ensure bot cannot path since * there are no blocks that can be broken without potentially dropping an entity. Bot is expected * to follow forward branch for as far as possible * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall * W W W W * W + + W * W + + W * W W W W */ it('noPathsBreakingObstructed', async () => { blockersToPlace.forEach(hPos => { const blockPos = { x: hPos.x, y: lidYPos, z: hPos.z } serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.dirt.id }) chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.dirt.id) add1x2Weight(bot.pathfinder.movements.entityIntersections, blockPos.x, blockPos.y + 1, blockPos.z) }) serverClient.write('map_chunk', generateChunkPacket(chunk)) await once(bot, 'chunkColumnLoad') const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode)) const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'noPath') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 2, `Generated path length wrong (${path.length} === 2)`) assert.ok(forwardBranch === true, `Generated path did not attempt to follow Forward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`) }) /** * If there are blocks capping the pit with an entity above each block, ensure bot can path * if allowed in the movements configuration * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall * W W W W * W + + W * W + + W * W W W W */ it('canPathWithBreakingObstructed', async () => { blockersToPlace.forEach(hPos => { const blockPos = { x: hPos.x, y: lidYPos, z: hPos.z } serverClient.write('block_change', { location: blockPos, type: mcData.blocksByName.dirt.id }) chunk.setBlockType(new Vec3(blockPos.x, blockPos.y, blockPos.z), mcData.blocksByName.dirt.id) add1x2Weight(bot.pathfinder.movements.entityIntersections, blockPos.x, blockPos.y + 1, blockPos.z) }) serverClient.write('map_chunk', generateChunkPacket(chunk)) await once(bot, 'chunkColumnLoad') bot.pathfinder.movements.dontMineUnderFallingBlock = false const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path bot.pathfinder.movements.dontMineUnderFallingBlock = true // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode)) const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`) assert.ok(forwardBranch === true, `Generated path did not follow Forward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`) }) /** * If there are entities filling the entire build area, ensure bot cannot path since * entities will prevent any block placement. Bot is expected to follow forward branch * for as far as possible * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall * W W W W * W X X W * W X X W * W W W W */ it('noPathsBuildingObstructed', () => { blockersToPlace.forEach(hPos => { add1x2Weight(bot.pathfinder.movements.entityIntersections, hPos.x, groundYPos, hPos.z) }) const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path const leftBranch = path[0].equals(firstLeftNode) const rightBranch = path[0].equals(firstRightNode) const forwardBranch = path[0].equals(firstForwardNode) const backwardBranch = path[0].equals(firstBackNode) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'noPath') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 1, `Generated path length wrong (${path.length} === 1)`) assert.ok(forwardBranch === true, `Generated path did not attempt to follow Forward Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`) }) /** * If there are entities filling the entire build area except for one space, ensure bot finds a path * [X] = Ent Below, [+] = Ent Above a Block, [O] = Open, [W] = Wall * W W W W * W O X W * W X X W * W W W W */ it('singlePathUnobstructed', () => { blockersToPlace.forEach(hPos => { if ((hPos.x !== leftPos.x) || (hPos.z !== leftPos.z)) { add1x2Weight(bot.pathfinder.movements.entityIntersections, hPos.x, groundYPos, hPos.z) } }) const generator = bot.pathfinder.getPathFromTo(bot.pathfinder.movements, startPos, goal, patherOptions) const { value: { result } } = generator.next() const path = result.path // Look at first and second nodes incase diagonal movements are used const leftBranch = (path[0].equals(firstLeftNode) || path[1].equals(firstLeftNode)) const rightBranch = (path[0].equals(firstRightNode) || path[1].equals(firstRightNode)) const forwardBranch = (path[0].equals(firstForwardNode) || path[1].equals(firstForwardNode)) const backwardBranch = (path[0].equals(firstBackNode) || path[1].equals(firstBackNode)) // All depends on the actually path that gets generated. If target block is moved some were else these values have to change. assert.strictEqual(result.status, 'success') assert.ok(result.time < maxPathTime, `Generated path took too long (${result.time} < ${maxPathTime})`) assert.ok(path.length === 6, `Generated path length wrong (${path.length} === 6)`) assert.ok(leftBranch === true, `Generated path did not follow Left Branch [Left Branch: ${leftBranch}, Right Branch: ${rightBranch}, Forward Branch: ${forwardBranch}, Backward Branch: ${backwardBranch}]`) }) }) })