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
This commit is contained in:
@@ -20,10 +20,10 @@
|
|||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
diff --git a/index.js b/index.js
|
diff --git a/index.js b/index.js
|
||||||
index b38bd30..2ca8205 100644
|
index b38bd30..patched 100644
|
||||||
--- a/index.js
|
--- a/index.js
|
||||||
+++ b/index.js
|
+++ b/index.js
|
||||||
@@ -14,6 +14,7 @@ const interactableBlocks = require('./lib/interactable.json')
|
@@ -14,6 +14,7 @@
|
||||||
|
|
||||||
function inject (bot) {
|
function inject (bot) {
|
||||||
const waterType = bot.registry.blocksByName.water.id
|
const waterType = bot.registry.blocksByName.water.id
|
||||||
@@ -31,7 +31,16 @@ index b38bd30..2ca8205 100644
|
|||||||
const ladderId = bot.registry.blocksByName.ladder.id
|
const ladderId = bot.registry.blocksByName.ladder.id
|
||||||
const vineId = bot.registry.blocksByName.vine.id
|
const vineId = bot.registry.blocksByName.vine.id
|
||||||
let stateMovements = new Movements(bot)
|
let stateMovements = new Movements(bot)
|
||||||
@@ -61,6 +62,10 @@ function inject (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) => {
|
bot.pathfinder.getPathTo = (movements, goal, timeout) => {
|
||||||
@@ -42,7 +51,19 @@ index b38bd30..2ca8205 100644
|
|||||||
const generator = bot.pathfinder.getPathFromTo(movements, bot.entity.position, goal, { timeout })
|
const generator = bot.pathfinder.getPathFromTo(movements, bot.entity.position, goal, { timeout })
|
||||||
const { value: { result, astarContext: context } } = generator.next()
|
const { value: { result, astarContext: context } } = generator.next()
|
||||||
astarContext = context
|
astarContext = context
|
||||||
@@ -170,6 +175,16 @@ function inject (bot) {
|
@@ -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]
|
const curPoint = path[i]
|
||||||
if (curPoint.toBreak.length > 0 || curPoint.toPlace.length > 0) break
|
if (curPoint.toBreak.length > 0 || curPoint.toPlace.length > 0) break
|
||||||
const b = bot.blockAt(new Vec3(curPoint.x, curPoint.y, curPoint.z))
|
const b = bot.blockAt(new Vec3(curPoint.x, curPoint.y, curPoint.z))
|
||||||
@@ -59,7 +80,7 @@ index b38bd30..2ca8205 100644
|
|||||||
if (b && (b.type === waterType || ((b.type === ladderId || b.type === vineId) && i + 1 < path.length && path[i + 1].y < curPoint.y))) {
|
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.x = Math.floor(curPoint.x) + 0.5
|
||||||
curPoint.y = Math.floor(curPoint.y)
|
curPoint.y = Math.floor(curPoint.y)
|
||||||
@@ -524,6 +539,9 @@ function inject (bot) {
|
@@ -524,6 +546,9 @@
|
||||||
bot.activateBlock(bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z))).then(() => {
|
bot.activateBlock(bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z))).then(() => {
|
||||||
lockUseBlock.release()
|
lockUseBlock.release()
|
||||||
placingBlock = nextPoint.toPlace.shift()
|
placingBlock = nextPoint.toPlace.shift()
|
||||||
@@ -69,7 +90,7 @@ index b38bd30..2ca8205 100644
|
|||||||
}, err => {
|
}, err => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
lockUseBlock.release()
|
lockUseBlock.release()
|
||||||
@@ -550,6 +568,7 @@ function inject (bot) {
|
@@ -550,6 +575,7 @@
|
||||||
lockEquipItem.release()
|
lockEquipItem.release()
|
||||||
const refBlock = bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z), false)
|
const refBlock = bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z), false)
|
||||||
if (!lockPlaceBlock.tryAcquire()) return
|
if (!lockPlaceBlock.tryAcquire()) return
|
||||||
@@ -77,7 +98,7 @@ index b38bd30..2ca8205 100644
|
|||||||
if (interactableBlocks.includes(refBlock.name)) {
|
if (interactableBlocks.includes(refBlock.name)) {
|
||||||
bot.setControlState('sneak', true)
|
bot.setControlState('sneak', true)
|
||||||
}
|
}
|
||||||
@@ -557,6 +576,7 @@ function inject (bot) {
|
@@ -557,6 +583,7 @@
|
||||||
.then(function () {
|
.then(function () {
|
||||||
// Dont release Sneak if the block placement was not successful
|
// Dont release Sneak if the block placement was not successful
|
||||||
bot.setControlState('sneak', false)
|
bot.setControlState('sneak', false)
|
||||||
@@ -85,7 +106,7 @@ index b38bd30..2ca8205 100644
|
|||||||
if (bot.pathfinder.LOSWhenPlacingBlocks && placingBlock.returnPos) returningPos = placingBlock.returnPos.clone()
|
if (bot.pathfinder.LOSWhenPlacingBlocks && placingBlock.returnPos) returningPos = placingBlock.returnPos.clone()
|
||||||
})
|
})
|
||||||
.catch(_ignoreError => {
|
.catch(_ignoreError => {
|
||||||
@@ -576,7 +596,7 @@ function inject (bot) {
|
@@ -576,7 +603,7 @@
|
||||||
let dx = nextPoint.x - p.x
|
let dx = nextPoint.x - p.x
|
||||||
const dy = nextPoint.y - p.y
|
const dy = nextPoint.y - p.y
|
||||||
let dz = nextPoint.z - p.z
|
let dz = nextPoint.z - p.z
|
||||||
@@ -94,7 +115,7 @@ index b38bd30..2ca8205 100644
|
|||||||
// arrived at next point
|
// arrived at next point
|
||||||
lastNodeTime = performance.now()
|
lastNodeTime = performance.now()
|
||||||
if (stopPathing) {
|
if (stopPathing) {
|
||||||
@@ -611,6 +631,9 @@ function inject (bot) {
|
@@ -611,6 +638,9 @@
|
||||||
if (bot.entity.isInWater) {
|
if (bot.entity.isInWater) {
|
||||||
bot.setControlState('jump', true)
|
bot.setControlState('jump', true)
|
||||||
bot.setControlState('sprint', false)
|
bot.setControlState('sprint', false)
|
||||||
@@ -104,11 +125,36 @@ index b38bd30..2ca8205 100644
|
|||||||
} else if (stateMovements.allowSprinting && physics.canStraightLine(path, true)) {
|
} else if (stateMovements.allowSprinting && physics.canStraightLine(path, true)) {
|
||||||
bot.setControlState('jump', false)
|
bot.setControlState('jump', false)
|
||||||
bot.setControlState('sprint', true)
|
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
|
diff --git a/lib/movements.js b/lib/movements.js
|
||||||
index a7e3505..84a0841 100644
|
index a7e3505..patched 100644
|
||||||
--- a/lib/movements.js
|
--- a/lib/movements.js
|
||||||
+++ b/lib/movements.js
|
+++ b/lib/movements.js
|
||||||
@@ -50,7 +50,12 @@ class Movements {
|
@@ -50,7 +50,12 @@
|
||||||
this.blocksToAvoid.add(registry.blocksByName.fire.id)
|
this.blocksToAvoid.add(registry.blocksByName.fire.id)
|
||||||
if (registry.blocksByName.cobweb) this.blocksToAvoid.add(registry.blocksByName.cobweb.id)
|
if (registry.blocksByName.cobweb) this.blocksToAvoid.add(registry.blocksByName.cobweb.id)
|
||||||
if (registry.blocksByName.web) this.blocksToAvoid.add(registry.blocksByName.web.id)
|
if (registry.blocksByName.web) this.blocksToAvoid.add(registry.blocksByName.web.id)
|
||||||
@@ -122,7 +168,7 @@ index a7e3505..84a0841 100644
|
|||||||
|
|
||||||
this.liquids = new Set()
|
this.liquids = new Set()
|
||||||
this.liquids.add(registry.blocksByName.water.id)
|
this.liquids.add(registry.blocksByName.water.id)
|
||||||
@@ -62,7 +67,13 @@ class Movements {
|
@@ -62,7 +67,13 @@
|
||||||
|
|
||||||
this.climbables = new Set()
|
this.climbables = new Set()
|
||||||
this.climbables.add(registry.blocksByName.ladder.id)
|
this.climbables.add(registry.blocksByName.ladder.id)
|
||||||
@@ -137,7 +183,7 @@ index a7e3505..84a0841 100644
|
|||||||
this.emptyBlocks = new Set()
|
this.emptyBlocks = new Set()
|
||||||
|
|
||||||
this.replaceables = new Set()
|
this.replaceables = new Set()
|
||||||
@@ -92,13 +103,15 @@ class Movements {
|
@@ -92,13 +103,15 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
registry.blocksArray.forEach(block => {
|
registry.blocksArray.forEach(block => {
|
||||||
@@ -155,7 +201,7 @@ index a7e3505..84a0841 100644
|
|||||||
|
|
||||||
this.exclusionAreasStep = []
|
this.exclusionAreasStep = []
|
||||||
this.exclusionAreasBreak = []
|
this.exclusionAreasBreak = []
|
||||||
@@ -230,8 +243,13 @@ class Movements {
|
@@ -230,8 +243,13 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b.climbable = this.climbables.has(b.type)
|
b.climbable = this.climbables.has(b.type)
|
||||||
@@ -171,7 +217,7 @@ index a7e3505..84a0841 100644
|
|||||||
b.replaceable = this.replaceables.has(b.type) && !b.physical
|
b.replaceable = this.replaceables.has(b.type) && !b.physical
|
||||||
b.liquid = this.liquids.has(b.type)
|
b.liquid = this.liquids.has(b.type)
|
||||||
b.height = pos.y + dy
|
b.height = pos.y + dy
|
||||||
@@ -284,6 +302,18 @@ class Movements {
|
@@ -284,6 +302,18 @@
|
||||||
cost += this.exclusionStep(block) // Is excluded so can't move or break
|
cost += this.exclusionStep(block) // Is excluded so can't move or break
|
||||||
cost += this.getNumEntitiesAt(block.position, 0, 0, 0) * this.entityCost
|
cost += this.getNumEntitiesAt(block.position, 0, 0, 0) * this.entityCost
|
||||||
if (block.safe) return cost
|
if (block.safe) return cost
|
||||||
@@ -190,7 +236,7 @@ index a7e3505..84a0841 100644
|
|||||||
if (!this.safeToBreak(block)) return 100 // Can't break, so can't move
|
if (!this.safeToBreak(block)) return 100 // Can't break, so can't move
|
||||||
toBreak.push(block.position)
|
toBreak.push(block.position)
|
||||||
|
|
||||||
@@ -387,8 +417,8 @@ class Movements {
|
@@ -387,8 +417,8 @@
|
||||||
cost += this.safeOrBreak(blockB, toBreak)
|
cost += this.safeOrBreak(blockB, toBreak)
|
||||||
if (cost > 100) return
|
if (cost > 100) return
|
||||||
|
|
||||||
@@ -201,7 +247,7 @@ index a7e3505..84a0841 100644
|
|||||||
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
|
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 {
|
} else {
|
||||||
cost += this.safeOrBreak(blockC, toBreak)
|
cost += this.safeOrBreak(blockC, toBreak)
|
||||||
@@ -554,6 +584,54 @@ class Movements {
|
@@ -554,6 +584,54 @@
|
||||||
neighbors.push(new Move(node.x, node.y + 1, node.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace))
|
neighbors.push(new Move(node.x, node.y + 1, node.z, node.remainingBlocks - toPlace.length, cost, toBreak, toPlace))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +302,7 @@ index a7e3505..84a0841 100644
|
|||||||
// Jump up, down or forward over a 1 block gap
|
// Jump up, down or forward over a 1 block gap
|
||||||
getMoveParkourForward (node, dir, neighbors) {
|
getMoveParkourForward (node, dir, neighbors) {
|
||||||
const block0 = this.getBlock(node, 0, -1, 0)
|
const block0 = this.getBlock(node, 0, -1, 0)
|
||||||
@@ -656,8 +734,27 @@ class Movements {
|
@@ -656,8 +734,27 @@
|
||||||
this.getMoveDown(node, neighbors)
|
this.getMoveDown(node, neighbors)
|
||||||
this.getMoveUp(node, neighbors)
|
this.getMoveUp(node, neighbors)
|
||||||
|
|
||||||
|
|||||||
Generated
+9
-9
@@ -250,7 +250,7 @@ overrides:
|
|||||||
|
|
||||||
patchedDependencies:
|
patchedDependencies:
|
||||||
mineflayer-pathfinder:
|
mineflayer-pathfinder:
|
||||||
hash: 4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1
|
hash: 4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797
|
||||||
path: patches/mineflayer-pathfinder.patch
|
path: patches/mineflayer-pathfinder.patch
|
||||||
mineflayer@4.33.0:
|
mineflayer@4.33.0:
|
||||||
hash: 03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a
|
hash: 03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a
|
||||||
@@ -3069,7 +3069,7 @@ importers:
|
|||||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||||
'@wxt-dev/module-vue':
|
'@wxt-dev/module-vue':
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(canvas@3.2.1)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
version: 1.0.3(vite@8.0.0-beta.9(@types/node@24.10.9)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(canvas@3.2.1)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^5.1.6
|
specifier: ^5.1.6
|
||||||
version: 5.1.6
|
version: 5.1.6
|
||||||
@@ -3189,7 +3189,7 @@ importers:
|
|||||||
version: 1.6.0(encoding@0.1.13)
|
version: 1.6.0(encoding@0.1.13)
|
||||||
mineflayer-pathfinder:
|
mineflayer-pathfinder:
|
||||||
specifier: ^2.4.5
|
specifier: ^2.4.5
|
||||||
version: 2.4.5(patch_hash=4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1)
|
version: 2.4.5(patch_hash=4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797)
|
||||||
mineflayer-pvp:
|
mineflayer-pvp:
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(encoding@0.1.13)(prismarine-registry@1.11.0)
|
version: 1.3.2(encoding@0.1.13)(prismarine-registry@1.11.0)
|
||||||
@@ -23309,9 +23309,9 @@ snapshots:
|
|||||||
'@types/filesystem': 0.0.36
|
'@types/filesystem': 0.0.36
|
||||||
'@types/har-format': 1.2.16
|
'@types/har-format': 1.2.16
|
||||||
|
|
||||||
'@wxt-dev/module-vue@1.0.3(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(canvas@3.2.1)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
'@wxt-dev/module-vue@1.0.3(vite@8.0.0-beta.9(@types/node@24.10.9)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))(wxt@0.20.13(@types/node@24.10.9)(canvas@3.2.1)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitejs/plugin-vue': 6.0.3(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
'@vitejs/plugin-vue': 6.0.3(vite@8.0.0-beta.9(@types/node@24.10.9)(esbuild@0.27.2)(jiti@2.6.1)(less@4.5.1)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
|
||||||
wxt: 0.20.13(@types/node@24.10.9)(canvas@3.2.1)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
wxt: 0.20.13(@types/node@24.10.9)(canvas@3.2.1)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.2)(rollup@4.55.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- vite
|
- vite
|
||||||
@@ -27880,13 +27880,13 @@ snapshots:
|
|||||||
mineflayer-collectblock@1.6.0(encoding@0.1.13):
|
mineflayer-collectblock@1.6.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
mineflayer: 4.33.0(patch_hash=03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a)(encoding@0.1.13)
|
mineflayer: 4.33.0(patch_hash=03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a)(encoding@0.1.13)
|
||||||
mineflayer-pathfinder: 2.4.5(patch_hash=4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1)
|
mineflayer-pathfinder: 2.4.5(patch_hash=4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797)
|
||||||
mineflayer-tool: 1.2.0(encoding@0.1.13)
|
mineflayer-tool: 1.2.0(encoding@0.1.13)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
mineflayer-pathfinder@2.4.5(patch_hash=4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1):
|
mineflayer-pathfinder@2.4.5(patch_hash=4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797):
|
||||||
dependencies:
|
dependencies:
|
||||||
minecraft-data: 3.102.3
|
minecraft-data: 3.102.3
|
||||||
prismarine-block: 1.22.0
|
prismarine-block: 1.22.0
|
||||||
@@ -27899,7 +27899,7 @@ snapshots:
|
|||||||
mineflayer-pvp@1.3.2(encoding@0.1.13)(prismarine-registry@1.11.0):
|
mineflayer-pvp@1.3.2(encoding@0.1.13)(prismarine-registry@1.11.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
mineflayer: 4.33.0(patch_hash=03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a)(encoding@0.1.13)
|
mineflayer: 4.33.0(patch_hash=03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a)(encoding@0.1.13)
|
||||||
mineflayer-pathfinder: 2.4.5(patch_hash=4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1)
|
mineflayer-pathfinder: 2.4.5(patch_hash=4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797)
|
||||||
mineflayer-utils: 0.1.4(encoding@0.1.13)(prismarine-registry@1.11.0)
|
mineflayer-utils: 0.1.4(encoding@0.1.13)(prismarine-registry@1.11.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
@@ -27909,7 +27909,7 @@ snapshots:
|
|||||||
mineflayer-tool@1.2.0(encoding@0.1.13):
|
mineflayer-tool@1.2.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
mineflayer: 4.33.0(patch_hash=03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a)(encoding@0.1.13)
|
mineflayer: 4.33.0(patch_hash=03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a)(encoding@0.1.13)
|
||||||
mineflayer-pathfinder: 2.4.5(patch_hash=4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1)
|
mineflayer-pathfinder: 2.4.5(patch_hash=4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797)
|
||||||
prismarine-nbt: 2.8.0
|
prismarine-nbt: 2.8.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
|
|||||||
@@ -82,21 +82,24 @@ export const actionsList: Action[] = [
|
|||||||
const targetStart = getPlayerPos()
|
const targetStart = getPlayerPos()
|
||||||
const distanceToTargetBefore = targetStart ? selfStart.distanceTo(targetStart) : null
|
const distanceToTargetBefore = targetStart ? selfStart.distanceTo(targetStart) : null
|
||||||
|
|
||||||
// TODO estimate time cost based on distance, trigger failure if time runs out
|
const result = await skills.goToPlayer(mineflayer, player_name, closeness)
|
||||||
const ok = await skills.goToPlayer(mineflayer, player_name, closeness)
|
|
||||||
|
|
||||||
const selfEnd = cloneVec3(mineflayer.bot.entity.position)
|
const selfEnd = cloneVec3(mineflayer.bot.entity.position)
|
||||||
const targetEnd = getPlayerPos()
|
const targetEnd = getPlayerPos()
|
||||||
const distanceToTargetAfter = targetEnd ? selfEnd.distanceTo(targetEnd) : null
|
const distanceToTargetAfter = targetEnd ? selfEnd.distanceTo(targetEnd) : null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok,
|
ok: result.ok,
|
||||||
|
reason: result.reason,
|
||||||
target: { player_name, closeness },
|
target: { player_name, closeness },
|
||||||
startPos: toCoord(selfStart),
|
startPos: toCoord(selfStart),
|
||||||
endPos: toCoord(selfEnd),
|
endPos: toCoord(selfEnd),
|
||||||
movedDistance: selfStart.distanceTo(selfEnd),
|
movedDistance: selfStart.distanceTo(selfEnd),
|
||||||
distanceToTargetBefore,
|
distanceToTargetBefore,
|
||||||
distanceToTargetAfter,
|
distanceToTargetAfter,
|
||||||
|
elapsedMs: result.elapsedMs,
|
||||||
|
estimatedTimeMs: result.estimatedTimeMs,
|
||||||
|
message: result.message,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -149,13 +152,14 @@ export const actionsList: Action[] = [
|
|||||||
const targetVec = new Vec3(x, y, z)
|
const targetVec = new Vec3(x, y, z)
|
||||||
const distanceToTargetBefore = selfStart.distanceTo(targetVec)
|
const distanceToTargetBefore = selfStart.distanceTo(targetVec)
|
||||||
|
|
||||||
const ok = await skills.goToPosition(mineflayer, x, y, z, closeness)
|
const result = await skills.goToPosition(mineflayer, x, y, z, closeness)
|
||||||
|
|
||||||
const selfEnd = cloneVec3(mineflayer.bot.entity.position)
|
const selfEnd = cloneVec3(mineflayer.bot.entity.position)
|
||||||
const distanceToTargetAfter = selfEnd.distanceTo(targetVec)
|
const distanceToTargetAfter = selfEnd.distanceTo(targetVec)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok,
|
ok: result.ok,
|
||||||
|
reason: result.reason,
|
||||||
target: { x, y, z, closeness },
|
target: { x, y, z, closeness },
|
||||||
startPos: toCoord(selfStart),
|
startPos: toCoord(selfStart),
|
||||||
endPos: toCoord(selfEnd),
|
endPos: toCoord(selfEnd),
|
||||||
@@ -163,6 +167,9 @@ export const actionsList: Action[] = [
|
|||||||
distanceToTargetBefore,
|
distanceToTargetBefore,
|
||||||
distanceToTargetAfter,
|
distanceToTargetAfter,
|
||||||
withinCloseness: distanceToTargetAfter <= closeness,
|
withinCloseness: distanceToTargetAfter <= closeness,
|
||||||
|
elapsedMs: result.elapsedMs,
|
||||||
|
estimatedTimeMs: result.estimatedTimeMs,
|
||||||
|
message: result.message,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -201,6 +201,10 @@ Common patterns:
|
|||||||
- To cross terrain, go through walls, or reach any reachable coordinate: one `goToCoordinate` call is sufficient.
|
- To cross terrain, go through walls, or reach any reachable coordinate: one `goToCoordinate` call is sufficient.
|
||||||
- **Never** write manual mine-then-move loops. That is what the pathfinder already does internally.
|
- **Never** write manual mine-then-move loops. That is what the pathfinder already does internally.
|
||||||
- `collectBlocks` also uses pathfinding internally to reach and mine target blocks.
|
- `collectBlocks` also uses pathfinding internally to reach and mine target blocks.
|
||||||
|
- Navigation results include `reason`, `elapsedMs`, `estimatedTimeMs`, `movedDistance`, `distanceToTargetAfter`, and `message`.
|
||||||
|
- Pathfinding has an **ETA-based timeout** (2× estimated travel time + grace). The ETA accounts for digging, block placement, parkour, and walking speed.
|
||||||
|
- If navigation fails with `reason: 'timeout'` or `reason: 'stagnation'`, try a closer intermediate waypoint, a different route, or `giveUp`.
|
||||||
|
- If navigation fails with `reason: 'noPath'`, the destination is unreachable from the current position.
|
||||||
|
|
||||||
# Context Management (Mandatory)
|
# Context Management (Mandatory)
|
||||||
You MUST use context boundaries to manage your conversation history. Without them, old messages accumulate and degrade your reasoning quality.
|
You MUST use context boundaries to manage your conversation history. Without them, old messages accumulate and degrade your reasoning quality.
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import pathfinder from 'mineflayer-pathfinder'
|
|||||||
import { ActionError } from '../../utils/errors'
|
import { ActionError } from '../../utils/errors'
|
||||||
import { useLogger } from '../../utils/logger'
|
import { useLogger } from '../../utils/logger'
|
||||||
import { breakBlockAt } from '../blocks'
|
import { breakBlockAt } from '../blocks'
|
||||||
|
import { patchedGoto } from '../patched-goto'
|
||||||
import { getNearestBlocks } from '../world'
|
import { getNearestBlocks } from '../world'
|
||||||
import { expandBlockAliases } from './block-type-normalizer'
|
import { expandBlockAliases } from './block-type-normalizer'
|
||||||
import { ensurePickaxe } from './ensure'
|
import { ensurePickaxe } from './ensure'
|
||||||
@@ -105,7 +106,11 @@ export async function collectBlock(
|
|||||||
veinBlock.position.y,
|
veinBlock.position.y,
|
||||||
veinBlock.position.z,
|
veinBlock.position.z,
|
||||||
)
|
)
|
||||||
await mineflayer.bot.pathfinder.goto(goal)
|
const navResult = await patchedGoto(mineflayer.bot, goal)
|
||||||
|
if (!navResult.ok) {
|
||||||
|
logger.log(`Failed to reach ${blockType} block: ${navResult.reason} — ${navResult.message}`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
// Break the block and collect drops
|
// Break the block and collect drops
|
||||||
await mineAndCollect(mineflayer, veinBlock)
|
await mineAndCollect(mineflayer, veinBlock)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { ActionError } from '../../utils/errors'
|
|||||||
import { useLogger } from '../../utils/logger'
|
import { useLogger } from '../../utils/logger'
|
||||||
import { McData } from '../../utils/mcdata'
|
import { McData } from '../../utils/mcdata'
|
||||||
import { goToPosition } from '../movement'
|
import { goToPosition } from '../movement'
|
||||||
|
import { patchedGoto } from '../patched-goto'
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
|
|
||||||
@@ -161,7 +162,7 @@ export async function placeBlock(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
||||||
await mineflayer.bot.pathfinder.goto(goal)
|
await patchedGoto(mineflayer.bot, goal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move closer if too far
|
// Move closer if too far
|
||||||
@@ -282,7 +283,7 @@ export async function activateNearestBlock(mineflayer: Mineflayer, type: string)
|
|||||||
if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) {
|
if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) {
|
||||||
const pos = block.position
|
const pos = block.position
|
||||||
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
||||||
await mineflayer.bot.pathfinder.goto(new pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4))
|
await patchedGoto(mineflayer.bot, new pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4))
|
||||||
}
|
}
|
||||||
await mineflayer.bot.activateBlock(block)
|
await mineflayer.bot.activateBlock(block)
|
||||||
logger.log(
|
logger.log(
|
||||||
@@ -388,10 +389,7 @@ export async function pickupNearbyItems(
|
|||||||
let pickedUp = 0
|
let pickedUp = 0
|
||||||
while (nearestItem) {
|
while (nearestItem) {
|
||||||
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
||||||
await mineflayer.bot.pathfinder.goto(
|
await patchedGoto(mineflayer.bot, new pathfinder.goals.GoalFollow(nearestItem, 0.8))
|
||||||
new pathfinder.goals.GoalFollow(nearestItem, 0.8),
|
|
||||||
() => { },
|
|
||||||
)
|
|
||||||
await sleep(500)
|
await sleep(500)
|
||||||
const prev = nearestItem
|
const prev = nearestItem
|
||||||
nearestItem = getNearestItem(mineflayer.bot)
|
nearestItem = getNearestItem(mineflayer.bot)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { Vec3 } from 'vec3'
|
|||||||
import { McData } from '../utils/mcdata'
|
import { McData } from '../utils/mcdata'
|
||||||
import { log } from './base'
|
import { log } from './base'
|
||||||
import { goToPosition } from './movement'
|
import { goToPosition } from './movement'
|
||||||
|
import { patchedGoto } from './patched-goto'
|
||||||
import { getNearestBlock, getNearestBlocks, getPosition, shouldPlaceTorch } from './world'
|
import { getNearestBlock, getNearestBlocks, getPosition, shouldPlaceTorch } from './world'
|
||||||
|
|
||||||
const { goals, Movements } = pathfinderModel
|
const { goals, Movements } = pathfinderModel
|
||||||
@@ -81,7 +82,7 @@ async function moveIntoRange(mineflayer: Mineflayer, block: any) {
|
|||||||
movements.allowParkour = false
|
movements.allowParkour = false
|
||||||
movements.allowSprinting = false
|
movements.allowSprinting = false
|
||||||
mineflayer.bot.pathfinder.setMovements(movements)
|
mineflayer.bot.pathfinder.setMovements(movements)
|
||||||
await mineflayer.bot.pathfinder.goto(new goals.GoalNear(pos.x, pos.y, pos.z, 4))
|
await patchedGoto(mineflayer.bot, new goals.GoalNear(pos.x, pos.y, pos.z, 4))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,16 +357,14 @@ async function moveAwayFromBlock(mineflayer: Mineflayer, targetBlock: any) {
|
|||||||
)
|
)
|
||||||
const invertedGoal = new goals.GoalInvert(goal)
|
const invertedGoal = new goals.GoalInvert(goal)
|
||||||
mineflayer.bot.pathfinder.setMovements(new Movements(mineflayer.bot))
|
mineflayer.bot.pathfinder.setMovements(new Movements(mineflayer.bot))
|
||||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
await patchedGoto(mineflayer.bot, invertedGoal)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function moveToBlock(mineflayer: Mineflayer, targetBlock: any) {
|
async function moveToBlock(mineflayer: Mineflayer, targetBlock: any) {
|
||||||
const pos = targetBlock.position
|
const pos = targetBlock.position
|
||||||
const movements = new Movements(mineflayer.bot)
|
const movements = new Movements(mineflayer.bot)
|
||||||
mineflayer.bot.pathfinder.setMovements(movements)
|
mineflayer.bot.pathfinder.setMovements(movements)
|
||||||
await mineflayer.bot.pathfinder.goto(
|
await patchedGoto(mineflayer.bot, new goals.GoalNear(pos.x, pos.y, pos.z, 4))
|
||||||
new goals.GoalNear(pos.x, pos.y, pos.z, 4),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function tryPlaceBlock(
|
async function tryPlaceBlock(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { sleep } from '@moeru/std'
|
|||||||
|
|
||||||
import { isHostile } from '../utils/mcdata'
|
import { isHostile } from '../utils/mcdata'
|
||||||
import { log } from './base'
|
import { log } from './base'
|
||||||
|
import { patchedGoto } from './patched-goto'
|
||||||
import { getNearbyEntities, getNearestEntityWhere } from './world'
|
import { getNearbyEntities, getNearestEntityWhere } from './world'
|
||||||
|
|
||||||
const { goals } = pathfinderModel
|
const { goals } = pathfinderModel
|
||||||
@@ -71,7 +72,7 @@ export async function attackEntity(
|
|||||||
if (!kill) {
|
if (!kill) {
|
||||||
if (mineflayer.bot.entity.position.distanceTo(pos) > 5) {
|
if (mineflayer.bot.entity.position.distanceTo(pos) > 5) {
|
||||||
const goal = new goals.GoalNear(pos.x, pos.y, pos.z, 4)
|
const goal = new goals.GoalNear(pos.x, pos.y, pos.z, 4)
|
||||||
await mineflayer.bot.pathfinder.goto(goal)
|
await patchedGoto(mineflayer.bot, goal)
|
||||||
}
|
}
|
||||||
await mineflayer.bot.attack(entity)
|
await mineflayer.bot.attack(entity)
|
||||||
return true
|
return true
|
||||||
@@ -101,7 +102,7 @@ export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise<boo
|
|||||||
&& enemy.name !== 'creeper' && enemy.name !== 'phantom') {
|
&& enemy.name !== 'creeper' && enemy.name !== 'phantom') {
|
||||||
try {
|
try {
|
||||||
const goal = new goals.GoalFollow(enemy, 3.5)
|
const goal = new goals.GoalFollow(enemy, 3.5)
|
||||||
await mineflayer.bot.pathfinder.goto(goal)
|
await patchedGoto(mineflayer.bot, goal)
|
||||||
}
|
}
|
||||||
catch { /* might error if entity dies, ignore */ }
|
catch { /* might error if entity dies, ignore */ }
|
||||||
}
|
}
|
||||||
@@ -110,7 +111,7 @@ export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise<boo
|
|||||||
try {
|
try {
|
||||||
const followGoal = new goals.GoalFollow(enemy, 2)
|
const followGoal = new goals.GoalFollow(enemy, 2)
|
||||||
const invertedGoal = new goals.GoalInvert(followGoal)
|
const invertedGoal = new goals.GoalInvert(followGoal)
|
||||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
await patchedGoto(mineflayer.bot, invertedGoal)
|
||||||
}
|
}
|
||||||
catch { /* might error if entity dies, ignore */ }
|
catch { /* might error if entity dies, ignore */ }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { Block } from 'prismarine-block'
|
|||||||
import type { Entity } from 'prismarine-entity'
|
import type { Entity } from 'prismarine-entity'
|
||||||
|
|
||||||
import type { Mineflayer } from '../libs/mineflayer'
|
import type { Mineflayer } from '../libs/mineflayer'
|
||||||
|
import type { PathfindProgressInfo, PathfindResult } from './patched-goto'
|
||||||
|
|
||||||
import pathfinder from 'mineflayer-pathfinder'
|
import pathfinder from 'mineflayer-pathfinder'
|
||||||
|
|
||||||
@@ -11,8 +12,11 @@ import { Vec3 } from 'vec3'
|
|||||||
|
|
||||||
import { useLogger } from '../utils/logger'
|
import { useLogger } from '../utils/logger'
|
||||||
import { log } from './base'
|
import { log } from './base'
|
||||||
|
import { patchedGoto } from './patched-goto'
|
||||||
import { getNearestBlock, getNearestEntityWhere } from './world'
|
import { getNearestBlock, getNearestEntityWhere } from './world'
|
||||||
|
|
||||||
|
export type { PathfindProgressInfo, PathfindResult } from './patched-goto'
|
||||||
|
|
||||||
const logger = useLogger()
|
const logger = useLogger()
|
||||||
const { goals, Movements } = pathfinder
|
const { goals, Movements } = pathfinder
|
||||||
|
|
||||||
@@ -22,16 +26,39 @@ export async function goToPosition(
|
|||||||
y: number,
|
y: number,
|
||||||
z: number,
|
z: number,
|
||||||
minDistance = 2,
|
minDistance = 2,
|
||||||
): Promise<boolean> {
|
options: { onProgress?: (info: PathfindProgressInfo) => void } = {},
|
||||||
|
): Promise<PathfindResult> {
|
||||||
if (x == null || y == null || z == null) {
|
if (x == null || y == null || z == null) {
|
||||||
log(mineflayer, `Missing coordinates, given x:${x} y:${y} z:${z}`)
|
log(mineflayer, `Missing coordinates, given x:${x} y:${y} z:${z}`)
|
||||||
return false
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'error',
|
||||||
|
message: `Missing coordinates, given x:${x} y:${y} z:${z}`,
|
||||||
|
startPos: { x: 0, y: 0, z: 0 },
|
||||||
|
endPos: { x: 0, y: 0, z: 0 },
|
||||||
|
distanceTraveled: 0,
|
||||||
|
distanceToTarget: 0,
|
||||||
|
elapsedMs: 0,
|
||||||
|
estimatedTimeMs: 0,
|
||||||
|
pathCost: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mineflayer.allowCheats) {
|
if (mineflayer.allowCheats) {
|
||||||
mineflayer.bot.chat(`/tp @s ${x} ${y} ${z}`)
|
mineflayer.bot.chat(`/tp @s ${x} ${y} ${z}`)
|
||||||
log(mineflayer, `Teleported to ${x}, ${y}, ${z}.`)
|
log(mineflayer, `Teleported to ${x}, ${y}, ${z}.`)
|
||||||
return true
|
return {
|
||||||
|
ok: true,
|
||||||
|
reason: 'success',
|
||||||
|
message: `Teleported to ${x}, ${y}, ${z}.`,
|
||||||
|
startPos: { x: 0, y: 0, z: 0 },
|
||||||
|
endPos: { x, y, z },
|
||||||
|
distanceTraveled: 0,
|
||||||
|
distanceToTarget: 0,
|
||||||
|
elapsedMs: 0,
|
||||||
|
estimatedTimeMs: 0,
|
||||||
|
pathCost: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const targetBlock = mineflayer.bot.blockAt(new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)))
|
const targetBlock = mineflayer.bot.blockAt(new Vec3(Math.floor(x), Math.floor(y), Math.floor(z)))
|
||||||
const blockAbove1 = mineflayer.bot.blockAt(new Vec3(Math.floor(x), Math.floor(y) + 1, Math.floor(z)))
|
const blockAbove1 = mineflayer.bot.blockAt(new Vec3(Math.floor(x), Math.floor(y) + 1, Math.floor(z)))
|
||||||
@@ -42,9 +69,18 @@ export async function goToPosition(
|
|||||||
y += 1
|
y += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
await mineflayer.bot.pathfinder.goto(new goals.GoalNear(x, y, z, minDistance))
|
const result = await patchedGoto(mineflayer.bot, new goals.GoalNear(x, y, z, minDistance), {
|
||||||
log(mineflayer, `You have reached ${x}, ${y}, ${z}.`)
|
onProgress: options.onProgress,
|
||||||
return true
|
})
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
log(mineflayer, `You have reached ${x}, ${y}, ${z}.`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log(mineflayer, `Navigation to ${x}, ${y}, ${z} ended: ${result.reason} — ${result.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function goToNearestBlock(
|
export async function goToNearestBlock(
|
||||||
@@ -65,7 +101,10 @@ export async function goToNearestBlock(
|
|||||||
}
|
}
|
||||||
|
|
||||||
log(mineflayer, `Found ${blockType} at ${block.position}.`)
|
log(mineflayer, `Found ${blockType} at ${block.position}.`)
|
||||||
await goToPosition(mineflayer, block.position.x, block.position.y, block.position.z, minDistance)
|
const result = await goToPosition(mineflayer, block.position.x, block.position.y, block.position.z, minDistance)
|
||||||
|
if (!result.ok) {
|
||||||
|
throw new Error(`Failed to reach ${blockType}: ${result.reason} — ${result.message}`)
|
||||||
|
}
|
||||||
return block
|
return block
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,36 +127,68 @@ export async function goToNearestEntity(
|
|||||||
|
|
||||||
const distance = mineflayer.bot.entity.position.distanceTo(entity.position)
|
const distance = mineflayer.bot.entity.position.distanceTo(entity.position)
|
||||||
log(mineflayer, `Found ${entityType} ${distance} blocks away.`)
|
log(mineflayer, `Found ${entityType} ${distance} blocks away.`)
|
||||||
await goToPosition(
|
const result = await goToPosition(
|
||||||
mineflayer,
|
mineflayer,
|
||||||
entity.position.x,
|
entity.position.x,
|
||||||
entity.position.y,
|
entity.position.y,
|
||||||
entity.position.z,
|
entity.position.z,
|
||||||
minDistance,
|
minDistance,
|
||||||
)
|
)
|
||||||
return true
|
return result.ok
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function goToPlayer(
|
export async function goToPlayer(
|
||||||
mineflayer: Mineflayer,
|
mineflayer: Mineflayer,
|
||||||
username: string,
|
username: string,
|
||||||
distance = 3,
|
distance = 3,
|
||||||
): Promise<boolean> {
|
options: { onProgress?: (info: PathfindProgressInfo) => void } = {},
|
||||||
|
): Promise<PathfindResult> {
|
||||||
if (mineflayer.allowCheats) {
|
if (mineflayer.allowCheats) {
|
||||||
mineflayer.bot.chat(`/tp @s ${username}`)
|
mineflayer.bot.chat(`/tp @s ${username}`)
|
||||||
log(mineflayer, `Teleported to ${username}.`)
|
log(mineflayer, `Teleported to ${username}.`)
|
||||||
return true
|
return {
|
||||||
|
ok: true,
|
||||||
|
reason: 'success',
|
||||||
|
message: `Teleported to ${username}.`,
|
||||||
|
startPos: { x: 0, y: 0, z: 0 },
|
||||||
|
endPos: { x: 0, y: 0, z: 0 },
|
||||||
|
distanceTraveled: 0,
|
||||||
|
distanceToTarget: 0,
|
||||||
|
elapsedMs: 0,
|
||||||
|
estimatedTimeMs: 0,
|
||||||
|
pathCost: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const player = mineflayer.bot.players[username]?.entity
|
const player = mineflayer.bot.players[username]?.entity
|
||||||
if (!player) {
|
if (!player) {
|
||||||
log(mineflayer, `Could not find ${username}.`)
|
log(mineflayer, `Could not find ${username}.`)
|
||||||
return false
|
return {
|
||||||
|
ok: false,
|
||||||
|
reason: 'error',
|
||||||
|
message: `Could not find ${username}.`,
|
||||||
|
startPos: { x: 0, y: 0, z: 0 },
|
||||||
|
endPos: { x: 0, y: 0, z: 0 },
|
||||||
|
distanceTraveled: 0,
|
||||||
|
distanceToTarget: 0,
|
||||||
|
elapsedMs: 0,
|
||||||
|
estimatedTimeMs: 0,
|
||||||
|
pathCost: 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await mineflayer.bot.pathfinder.goto(new goals.GoalFollow(player, distance))
|
const result = await patchedGoto(mineflayer.bot, new goals.GoalFollow(player, distance), {
|
||||||
log(mineflayer, `You have reached ${username}.`)
|
onProgress: options.onProgress,
|
||||||
return true
|
})
|
||||||
|
|
||||||
|
if (result.ok) {
|
||||||
|
log(mineflayer, `You have reached ${username}.`)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log(mineflayer, `Navigation to ${username} ended: ${result.reason} — ${result.message}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function followPlayer(
|
export async function followPlayer(
|
||||||
@@ -172,11 +243,11 @@ export async function moveAway(mineflayer: Mineflayer, distance: number): Promis
|
|||||||
|
|
||||||
const farGoal = new pathfinder.goals.GoalXZ(newX, newZ)
|
const farGoal = new pathfinder.goals.GoalXZ(newX, newZ)
|
||||||
|
|
||||||
await mineflayer.bot.pathfinder.goto(farGoal)
|
const result = await patchedGoto(mineflayer.bot, farGoal)
|
||||||
const newPos = mineflayer.bot.entity.position
|
const newPos = mineflayer.bot.entity.position
|
||||||
logger.log(`I moved away from nearest entity to ${newPos}.`)
|
logger.log(`I moved away from nearest entity to ${newPos}.`)
|
||||||
await sleep(500)
|
await sleep(500)
|
||||||
return true
|
return result.ok
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
logger.log(`I failed to move away: ${(err as Error).message}`)
|
logger.log(`I failed to move away: ${(err as Error).message}`)
|
||||||
@@ -191,8 +262,8 @@ export async function moveAwayFromEntity(
|
|||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const goal = new goals.GoalFollow(entity, distance)
|
const goal = new goals.GoalFollow(entity, distance)
|
||||||
const invertedGoal = new goals.GoalInvert(goal)
|
const invertedGoal = new goals.GoalInvert(goal)
|
||||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
const result = await patchedGoto(mineflayer.bot, invertedGoal)
|
||||||
return true
|
return result.ok
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function stay(mineflayer: Mineflayer, seconds = 30): Promise<boolean> {
|
export async function stay(mineflayer: Mineflayer, seconds = 30): Promise<boolean> {
|
||||||
|
|||||||
@@ -0,0 +1,316 @@
|
|||||||
|
import type { Bot } from 'mineflayer'
|
||||||
|
import type { Vec3 } from 'vec3'
|
||||||
|
|
||||||
|
import { useLogger } from '../utils/logger'
|
||||||
|
|
||||||
|
const logger = useLogger()
|
||||||
|
|
||||||
|
// --- ETA Calibration Constants ---
|
||||||
|
const SPRINT_SPEED = 5.6 // blocks/s
|
||||||
|
// NOTICE: WALK_SPEED kept as reference for non-sprinting ETA calculations
|
||||||
|
|
||||||
|
const _WALK_SPEED = 4.3 // blocks/s
|
||||||
|
const JUMP_TIME = 0.6 // seconds per jump move
|
||||||
|
const PARKOUR_TIME = 1.0 // seconds per parkour move
|
||||||
|
const PLACE_TIME = 0.5 // seconds per block placement
|
||||||
|
const GRACE_FACTOR = 2.0 // multiply ETA by this for timeout
|
||||||
|
const BASE_GRACE_S = 10 // minimum grace seconds
|
||||||
|
const MIN_TIMEOUT_MS = 15_000 // absolute floor 15s
|
||||||
|
const MAX_TIMEOUT_MS = 300_000 // absolute ceiling 5min
|
||||||
|
|
||||||
|
// --- Progress / Stagnation ---
|
||||||
|
const PROGRESS_INTERVAL_MS = 5_000 // check progress every 5s
|
||||||
|
const STAGNATION_THRESHOLD = 1.5 // blocks — less than this = stagnant
|
||||||
|
const MAX_STAGNANT_TICKS = 3 // 3 stagnant ticks (~15s) → cancel
|
||||||
|
|
||||||
|
export interface PathfindResult {
|
||||||
|
ok: boolean
|
||||||
|
reason: 'success' | 'timeout' | 'stagnation' | 'noPath' | 'error' | 'interrupted'
|
||||||
|
message: string
|
||||||
|
startPos: { x: number, y: number, z: number }
|
||||||
|
endPos: { x: number, y: number, z: number }
|
||||||
|
distanceTraveled: number
|
||||||
|
distanceToTarget: number
|
||||||
|
elapsedMs: number
|
||||||
|
estimatedTimeMs: number
|
||||||
|
pathCost: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PathfindProgressInfo {
|
||||||
|
elapsedMs: number
|
||||||
|
estimatedTimeMs: number
|
||||||
|
distanceTraveled: number
|
||||||
|
distanceToTarget: number
|
||||||
|
currentPos: { x: number, y: number, z: number }
|
||||||
|
stagnantTicks: number
|
||||||
|
pathCost: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MoveNode {
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
z: number
|
||||||
|
cost: number
|
||||||
|
toBreak: Array<{ x: number, y: number, z: number }>
|
||||||
|
toPlace: Array<{ x: number, y: number, z: number }>
|
||||||
|
parkour?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PathUpdateResult {
|
||||||
|
status: string
|
||||||
|
cost: number
|
||||||
|
path: MoveNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function vecToCoord(v: Vec3): { x: number, y: number, z: number } {
|
||||||
|
return { x: Math.round(v.x * 10) / 10, y: Math.round(v.y * 10) / 10, z: Math.round(v.z * 10) / 10 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate real-world seconds to traverse a computed A* path.
|
||||||
|
*
|
||||||
|
* Walks each Move node and sums up time for walking, digging, placing, and parkour.
|
||||||
|
* The dig time is estimated from the cost model: `laborCost = (1 + 3 * digTime_ms / 1000) * digCost`.
|
||||||
|
* Since digCost defaults to 1, we can reverse: `digTime_ms ≈ (laborCost - 1) * 1000 / 3`.
|
||||||
|
* But we don't have per-block labor cost separated out. Instead, we use heuristics:
|
||||||
|
* - Each toBreak block: ~1.5s average (conservative; stone with iron pick is ~0.75s, obsidian is 9.4s)
|
||||||
|
* - Each toPlace block: ~0.5s
|
||||||
|
* - Parkour moves: ~1.0s
|
||||||
|
* - Jump moves (cost >= 2 without parkour): ~0.6s
|
||||||
|
* - Normal moves: distance / walk speed
|
||||||
|
*/
|
||||||
|
export function estimatePathTimeMs(path: MoveNode[]): number {
|
||||||
|
if (path.length === 0)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
let totalTimeS = 0
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length; i++) {
|
||||||
|
const node = path[i]
|
||||||
|
|
||||||
|
// Dig time: each block to break
|
||||||
|
totalTimeS += node.toBreak.length * 1.5
|
||||||
|
|
||||||
|
// Place time: each block to place
|
||||||
|
totalTimeS += node.toPlace.length * PLACE_TIME
|
||||||
|
|
||||||
|
if (node.parkour) {
|
||||||
|
totalTimeS += PARKOUR_TIME
|
||||||
|
}
|
||||||
|
else if (node.cost >= 2 && node.toBreak.length === 0 && node.toPlace.length === 0) {
|
||||||
|
// Jump move (cost=2 base for jump-up)
|
||||||
|
totalTimeS += JUMP_TIME
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Normal walking move — estimate from node distance
|
||||||
|
// Diagonal moves have cost √2, forward moves cost 1
|
||||||
|
const walkDistance = node.cost >= 1.4 ? Math.SQRT2 : 1
|
||||||
|
totalTimeS += walkDistance / SPRINT_SPEED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalTimeS * 1000
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute a timeout from the estimated path time.
|
||||||
|
* timeout = ETA * graceFactor + baseGrace, clamped to [MIN, MAX].
|
||||||
|
*/
|
||||||
|
export function computeTimeoutFromEta(estimatedMs: number): number {
|
||||||
|
const timeoutMs = estimatedMs * GRACE_FACTOR + BASE_GRACE_S * 1000
|
||||||
|
return Math.max(MIN_TIMEOUT_MS, Math.min(MAX_TIMEOUT_MS, timeoutMs))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A robust pathfinding wrapper that provides:
|
||||||
|
* - ETA-based dynamic timeout (recalculated on path replanning)
|
||||||
|
* - Periodic progress tracking with stagnation detection
|
||||||
|
* - Structured result with telemetry
|
||||||
|
* - Optional progress callback for LLM feedback
|
||||||
|
*
|
||||||
|
* Uses `bot.pathfinder.setGoal` directly (not `goto`) for full event control.
|
||||||
|
*/
|
||||||
|
export function patchedGoto(
|
||||||
|
bot: Bot,
|
||||||
|
goal: any,
|
||||||
|
options: {
|
||||||
|
onProgress?: (info: PathfindProgressInfo) => void
|
||||||
|
} = {},
|
||||||
|
): Promise<PathfindResult> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const startPos = bot.entity.position.clone()
|
||||||
|
const startTime = Date.now()
|
||||||
|
let lastProgressPos = startPos.clone()
|
||||||
|
let stagnantTicks = 0
|
||||||
|
let currentEstimatedMs = 0
|
||||||
|
let currentTimeoutMs = MIN_TIMEOUT_MS
|
||||||
|
let currentPathCost = 0
|
||||||
|
let timeoutTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let progressTimer: ReturnType<typeof setInterval> | null = null
|
||||||
|
let settled = false
|
||||||
|
|
||||||
|
function getDistanceToTarget(): number {
|
||||||
|
try {
|
||||||
|
// Use the goal's heuristic if available, otherwise euclidean to start target
|
||||||
|
if (goal && typeof goal.heuristic === 'function') {
|
||||||
|
return goal.heuristic(bot.entity.position.floored())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResult(ok: boolean, reason: PathfindResult['reason'], message: string): PathfindResult {
|
||||||
|
const endPos = bot.entity.position.clone()
|
||||||
|
return {
|
||||||
|
ok,
|
||||||
|
reason,
|
||||||
|
message,
|
||||||
|
startPos: vecToCoord(startPos),
|
||||||
|
endPos: vecToCoord(endPos),
|
||||||
|
distanceTraveled: startPos.distanceTo(endPos),
|
||||||
|
distanceToTarget: getDistanceToTarget(),
|
||||||
|
elapsedMs: Date.now() - startTime,
|
||||||
|
estimatedTimeMs: currentEstimatedMs,
|
||||||
|
pathCost: currentPathCost,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanup() {
|
||||||
|
if (timeoutTimer) {
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
timeoutTimer = null
|
||||||
|
}
|
||||||
|
if (progressTimer) {
|
||||||
|
clearInterval(progressTimer)
|
||||||
|
progressTimer = null
|
||||||
|
}
|
||||||
|
bot.removeListener('goal_reached', onGoalReached)
|
||||||
|
bot.removeListener('path_update', onPathUpdate)
|
||||||
|
bot.removeListener('goal_updated', onGoalUpdated)
|
||||||
|
bot.removeListener('path_stop', onPathStop)
|
||||||
|
}
|
||||||
|
|
||||||
|
function settle(result: PathfindResult) {
|
||||||
|
if (settled)
|
||||||
|
return
|
||||||
|
settled = true
|
||||||
|
cleanup()
|
||||||
|
// Resolve on next tick to let pathfinder clean up
|
||||||
|
setTimeout(() => resolve(result), 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetTimeout() {
|
||||||
|
if (timeoutTimer) {
|
||||||
|
clearTimeout(timeoutTimer)
|
||||||
|
}
|
||||||
|
timeoutTimer = setTimeout(() => {
|
||||||
|
logger.withFields({ elapsedMs: Date.now() - startTime, estimatedMs: currentEstimatedMs, timeoutMs: currentTimeoutMs }).log('Pathfinding timeout reached')
|
||||||
|
try {
|
||||||
|
bot.pathfinder.stop()
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
settle(buildResult(false, 'timeout', `Navigation timed out after ${Math.round((Date.now() - startTime) / 1000)}s (ETA was ${Math.round(currentEstimatedMs / 1000)}s)`))
|
||||||
|
}, currentTimeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Event handlers ---
|
||||||
|
|
||||||
|
function onGoalReached() {
|
||||||
|
settle(buildResult(true, 'success', 'Reached the goal'))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPathUpdate(results: PathUpdateResult) {
|
||||||
|
// Recalculate ETA from the new path
|
||||||
|
if (results.path && results.path.length > 0) {
|
||||||
|
currentEstimatedMs = estimatePathTimeMs(results.path)
|
||||||
|
currentTimeoutMs = computeTimeoutFromEta(currentEstimatedMs)
|
||||||
|
currentPathCost = results.cost
|
||||||
|
resetTimeout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for noPath / timeout from A* computation
|
||||||
|
// Only fail when the path is empty AND status indicates failure.
|
||||||
|
// If there's a partial path, the bot should walk it while A* continues.
|
||||||
|
if (results.path.length === 0) {
|
||||||
|
if (results.status === 'noPath') {
|
||||||
|
settle(buildResult(false, 'noPath', 'No path to the goal'))
|
||||||
|
}
|
||||||
|
else if (results.status === 'timeout') {
|
||||||
|
settle(buildResult(false, 'noPath', 'Pathfinding computation timed out (A* could not find a path in time)'))
|
||||||
|
}
|
||||||
|
// else: empty path but status is 'partial' — still computing, don't fail yet
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGoalUpdated(newGoal: any) {
|
||||||
|
if (newGoal !== goal) {
|
||||||
|
settle(buildResult(false, 'interrupted', 'Goal was changed externally'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPathStop() {
|
||||||
|
settle(buildResult(false, 'interrupted', 'Path was stopped'))
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Progress ticker ---
|
||||||
|
function checkProgress() {
|
||||||
|
if (settled)
|
||||||
|
return
|
||||||
|
|
||||||
|
const currentPos = bot.entity.position.clone()
|
||||||
|
const movedSinceLastTick = currentPos.distanceTo(lastProgressPos)
|
||||||
|
|
||||||
|
if (movedSinceLastTick < STAGNATION_THRESHOLD) {
|
||||||
|
stagnantTicks++
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stagnantTicks = 0
|
||||||
|
}
|
||||||
|
lastProgressPos = currentPos
|
||||||
|
|
||||||
|
const progressInfo: PathfindProgressInfo = {
|
||||||
|
elapsedMs: Date.now() - startTime,
|
||||||
|
estimatedTimeMs: currentEstimatedMs,
|
||||||
|
distanceTraveled: startPos.distanceTo(currentPos),
|
||||||
|
distanceToTarget: getDistanceToTarget(),
|
||||||
|
currentPos: vecToCoord(currentPos),
|
||||||
|
stagnantTicks,
|
||||||
|
pathCost: currentPathCost,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify callback
|
||||||
|
options.onProgress?.(progressInfo)
|
||||||
|
|
||||||
|
// Check stagnation limit
|
||||||
|
if (stagnantTicks >= MAX_STAGNANT_TICKS) {
|
||||||
|
logger.withFields({ stagnantTicks, pos: vecToCoord(currentPos) }).log('Pathfinding stagnation detected')
|
||||||
|
try {
|
||||||
|
bot.pathfinder.stop()
|
||||||
|
}
|
||||||
|
catch {}
|
||||||
|
settle(buildResult(false, 'stagnation', `Bot stagnated for ${stagnantTicks * PROGRESS_INTERVAL_MS / 1000}s without meaningful movement`))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Start ---
|
||||||
|
bot.on('goal_reached', onGoalReached)
|
||||||
|
bot.on('path_update', onPathUpdate)
|
||||||
|
bot.on('goal_updated', onGoalUpdated)
|
||||||
|
bot.on('path_stop', onPathStop)
|
||||||
|
|
||||||
|
// Set initial timeout (will be recalculated on first path_update)
|
||||||
|
resetTimeout()
|
||||||
|
|
||||||
|
// Start progress ticker
|
||||||
|
progressTimer = setInterval(checkProgress, PROGRESS_INTERVAL_MS)
|
||||||
|
|
||||||
|
// Kick off pathfinding
|
||||||
|
try {
|
||||||
|
bot.pathfinder.setGoal(goal)
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
settle(buildResult(false, 'error', `Failed to set pathfinding goal: ${(err as Error).message}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user