LookAtMySuitBot/js/node_modules/mineflayer-pathfinder/index.js

644 lines
21 KiB
JavaScript

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