airi/patches/mineflayer-pathfinder.patch
Rin a5287c08d2
refactor(minecraft): add ETA-based pathfinding timeout with stuck detection and retry limits
Patches mineflayer-pathfinder to implement estimated-time-of-arrival based navigation timeouts (2× estimated travel time + grace period). Adds stuck detection counter that triggers failure after 3 consecutive resets without progress. Introduces patchedGoto wrapper returning {ok, reason, elapsedMs, estimatedTimeMs, message} for all navigation calls. Updates goToPlayer/goToPosition actions to surface timeout
2026-02-19 00:25:06 +08:00

332 lines
14 KiB
Diff

# MIT License
# Copyright (c) 2024 Kolby Nottingham
# 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.
diff --git a/index.js b/index.js
index b38bd30..patched 100644
--- a/index.js
+++ b/index.js
@@ -14,6 +14,7 @@
function inject (bot) {
const waterType = bot.registry.blocksByName.water.id
+ const lavaType = bot.registry.blocksByName.lava.id
const ladderId = bot.registry.blocksByName.ladder.id
const vineId = bot.registry.blocksByName.vine.id
let stateMovements = new Movements(bot)
@@ -29,6 +30,8 @@
let lastNodeTime = performance.now()
let returningPos = null
let stopPathing = false
+ let stuckCount = 0
+ let lastStuckPos = null
const physics = new Physics(bot)
const lockPlaceBlock = new Lock()
const lockEquipItem = new Lock()
@@ -61,6 +64,10 @@
}
bot.pathfinder.getPathTo = (movements, goal, timeout) => {
+ // Update lava avoidance based on current bot state
+ if (movements.updateLavaAvoidance) {
+ movements.updateLavaAvoidance()
+ }
const generator = bot.pathfinder.getPathFromTo(movements, bot.entity.position, goal, { timeout })
const { value: { result, astarContext: context } } = generator.next()
astarContext = context
@@ -136,6 +143,11 @@
lockUseBlock.release()
stateMovements.clearCollisionIndex()
if (clearStates) bot.clearControlStates()
+ // Reset stuck counter when path is reset for non-stuck reasons
+ if (reason !== 'stuck') {
+ stuckCount = 0
+ lastStuckPos = null
+ }
if (stopPathing) return stop()
}
@@ -170,6 +182,16 @@
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))
+
+ // openned doors have small Collision box
+ // that may stop the bot from moving forward
+ if(i === 0 && b?.name.includes('door')) {
+ curPoint.x = Math.floor(curPoint.x) + 0.5
+ curPoint.y = Math.floor(curPoint.y)
+ curPoint.z = Math.floor(curPoint.z) + 0.5
+ continue
+ }
+
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)
@@ -524,6 +546,9 @@
bot.activateBlock(bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z))).then(() => {
lockUseBlock.release()
placingBlock = nextPoint.toPlace.shift()
+ if (!placingBlock) {
+ placing = false
+ }
}, err => {
console.error(err)
lockUseBlock.release()
@@ -550,6 +575,7 @@
lockEquipItem.release()
const refBlock = bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z), false)
if (!lockPlaceBlock.tryAcquire()) return
+ bot.world.setBlockStateId(refBlock.position.offset(placingBlock.dx, placingBlock.dy, placingBlock.dz), 1)
if (interactableBlocks.includes(refBlock.name)) {
bot.setControlState('sneak', true)
}
@@ -557,6 +583,7 @@
.then(function () {
// Dont release Sneak if the block placement was not successful
bot.setControlState('sneak', false)
+ bot.setControlState('jump', false)
if (bot.pathfinder.LOSWhenPlacingBlocks && placingBlock.returnPos) returningPos = placingBlock.returnPos.clone()
})
.catch(_ignoreError => {
@@ -576,7 +603,7 @@
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) {
+ if (Math.abs(dx) <= 0.175 && Math.abs(dz) <= 0.175 && Math.abs(dy) < 1) {
// arrived at next point
lastNodeTime = performance.now()
if (stopPathing) {
@@ -611,6 +638,9 @@
if (bot.entity.isInWater) {
bot.setControlState('jump', true)
bot.setControlState('sprint', false)
+ } else if (bot.entity.isInLava) {
+ bot.setControlState('jump', true)
+ bot.setControlState('sprint', false)
} else if (stateMovements.allowSprinting && physics.canStraightLine(path, true)) {
bot.setControlState('jump', false)
bot.setControlState('sprint', true)
@@ -628,9 +658,22 @@
bot.setControlState('sprint', false)
}
- // check for futility
+ // check for futility — with stuck counter to prevent infinite reset loops
if (performance.now() - lastNodeTime > 3500) {
- // should never take this long to go to the next node
+ const currentPos = bot.entity.position.clone()
+ if (lastStuckPos && currentPos.distanceTo(lastStuckPos) < 1.5) {
+ stuckCount++
+ } else {
+ stuckCount = 1
+ lastStuckPos = currentPos
+ }
+ if (stuckCount >= 3) {
+ // Truly stuck after 3 consecutive resets without progress — give up
+ stuckCount = 0
+ lastStuckPos = null
+ stop()
+ return
+ }
resetPath('stuck')
}
}
diff --git a/lib/movements.js b/lib/movements.js
index a7e3505..patched 100644
--- a/lib/movements.js
+++ b/lib/movements.js
@@ -50,7 +50,12 @@
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)
+
+ // Only avoid lava if bot is not currently in lava
+ // Check if bot.entity exists and is initialized
+ if (!bot.entity || !bot.entity.isInLava) {
+ this.blocksToAvoid.add(registry.blocksByName.lava.id)
+ }
this.liquids = new Set()
this.liquids.add(registry.blocksByName.water.id)
@@ -62,7 +67,13 @@
this.climbables = new Set()
this.climbables.add(registry.blocksByName.ladder.id)
- // this.climbables.add(registry.blocksByName.vine.id)
+ if (registry.blocksByName.vine) this.climbables.add(registry.blocksByName.vine.id)
+ if (registry.blocksByName.weeping_vines) this.climbables.add(registry.blocksByName.weeping_vines.id)
+ if (registry.blocksByName.weeping_vines_plant) this.climbables.add(registry.blocksByName.weeping_vines_plant.id)
+ if (registry.blocksByName.twisting_vines) this.climbables.add(registry.blocksByName.twisting_vines.id)
+ if (registry.blocksByName.twisting_vines_plant) this.climbables.add(registry.blocksByName.twisting_vines_plant.id)
+ if (registry.blocksByName.cave_vines) this.climbables.add(registry.blocksByName.cave_vines.id)
+ if (registry.blocksByName.cave_vines_plant) this.climbables.add(registry.blocksByName.cave_vines_plant.id)
this.emptyBlocks = new Set()
this.replaceables = new Set()
@@ -92,13 +103,15 @@
}
})
registry.blocksArray.forEach(block => {
- if (this.interactableBlocks.has(block.name) && block.name.toLowerCase().includes('gate') && !block.name.toLowerCase().includes('iron')) {
+ if (this.interactableBlocks.has(block.name)
+ && (block.name.toLowerCase().includes('gate') || block.name.toLowerCase().includes('door') || block.name.toLowerCase().includes('trapdoor'))
+ && !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.canOpenDoors = true
this.exclusionAreasStep = []
this.exclusionAreasBreak = []
@@ -230,8 +243,13 @@
}
}
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)
+
+ // Enhanced trapdoor logic - open trapdoors are safe to pass through
+ const isOpenTrapdoor = this.openable.has(b.type) && b.name.includes('trapdoor') && b._properties?.open === true
+ const isClosedTrapdoor = this.openable.has(b.type) && b.name.includes('trapdoor') && b._properties?.open !== true
+
+ b.safe = (b.boundingBox === 'empty' || b.climbable || this.carpets.has(b.type) || isOpenTrapdoor) && !this.blocksToAvoid.has(b.type)
+ b.physical = (b.boundingBox === 'block' && !this.fences.has(b.type)) || isClosedTrapdoor
b.replaceable = this.replaceables.has(b.type) && !b.physical
b.liquid = this.liquids.has(b.type)
b.height = pos.y + dy
@@ -284,6 +302,18 @@
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
+
+ // process door cost
+ if ((this.canOpenDoors && block.openable)
+ || (block.openable && block._properties?.open === true)) {
+ return cost
+ }
+
+ // Handle trapdoors specifically - they can be opened instead of broken
+ if (this.canOpenDoors && block.openable && block.name.includes('trapdoor') && !block.name.includes('iron')) {
+ return cost + 1 // Small cost for opening trapdoor
+ }
+
if (!this.safeToBreak(block)) return 100 // Can't break, so can't move
toBreak.push(block.position)
@@ -387,8 +417,8 @@
cost += this.safeOrBreak(blockB, toBreak)
if (cost > 100) return
- // Open fence gates
- if (this.canOpenDoors && blockC.openable && blockC.shapes && blockC.shapes.length !== 0) {
+ // Open fence gates and doors
+ if (this.canOpenDoors && blockC.openable && !blockC._properties.open) {
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)
@@ -554,6 +584,54 @@
neighbors.push(new Move(node.x, node.y + 1, node.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace))
}
+ getMoveClimbUpThroughTrapdoor (node, neighbors) {
+ const blockCurrent = this.getBlock(node, 0, 0, 0) // Current position (should be climbable)
+ const blockAbove = this.getBlock(node, 0, 1, 0) // Block directly above
+ const blockCeiling = this.getBlock(node, 0, 2, 0) // Trapdoor or ceiling block
+
+ // Only attempt this move if we're on a climbable block (ladder/vine)
+ if (!blockCurrent.climbable) return
+
+ // Check if there's a closed trapdoor above us
+ if (!blockCeiling.openable || blockCeiling._properties?.open === true) return
+
+ let cost = 2 // Base cost for climbing up and opening trapdoor
+ const toBreak = []
+ const toPlace = []
+
+ // Make sure we can break/pass through the block above if needed
+ cost += this.safeOrBreak(blockAbove, toBreak)
+ if (cost > 100) return
+
+ // Add cost for opening the trapdoor
+ toPlace.push({ x: node.x, y: node.y + 2, z: node.z, dx: 0, dy: 0, dz: 0, useOne: true })
+
+ neighbors.push(new Move(node.x, node.y + 2, node.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace))
+ }
+
+ // Enhanced ladder/vine climbing that can handle stepping on top and jumping
+ getMoveClimbTop (node, neighbors) {
+ const blockCurrent = this.getBlock(node, 0, 0, 0) // Current position (should be climbable)
+ const blockAbove = this.getBlock(node, 0, 1, 0) // Block directly above
+
+ // Only attempt this move if we're on a climbable block (ladder/vine)
+ if (!blockCurrent.climbable) return
+
+ // Check if we can step on top of the ladder/vine and then jump up
+ if (!blockAbove.safe) return
+
+ let cost = 2 // Cost for climbing to top of ladder and jumping
+ const toBreak = []
+ const toPlace = []
+
+ // Check if there's space to jump up from the top of the ladder
+ const blockJumpTarget = this.getBlock(node, 0, 2, 0)
+ cost += this.safeOrBreak(blockJumpTarget, toBreak)
+ if (cost > 100) return
+
+ neighbors.push(new Move(node.x, node.y + 2, 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)
@@ -656,8 +734,27 @@
this.getMoveDown(node, neighbors)
this.getMoveUp(node, neighbors)
+ // Enhanced climbing moves for ladders, vines, and trapdoors
+ this.getMoveClimbUpThroughTrapdoor(node, neighbors)
+ this.getMoveClimbTop(node, neighbors)
+
return neighbors
}
+
+ // Update lava avoidance based on bot's current state
+ updateLavaAvoidance () {
+ const registry = this.bot.registry
+ const lavaId = registry.blocksByName.lava.id
+
+ // Check if bot.entity exists and is initialized
+ if (this.bot.entity && this.bot.entity.isInLava) {
+ // If bot is in lava, allow pathfinding through lava to escape
+ this.blocksToAvoid.delete(lavaId)
+ } else {
+ // If bot is not in lava, avoid lava blocks
+ this.blocksToAvoid.add(lavaId)
+ }
+ }
}
module.exports = Movements