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') }