mirror of
https://github.com/moeru-ai/airi.git
synced 2026-08-14 00:48:06 +00:00
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
|
||||
# SOFTWARE.
|
||||
diff --git a/index.js b/index.js
|
||||
index b38bd30..2ca8205 100644
|
||||
index b38bd30..patched 100644
|
||||
--- a/index.js
|
||||
+++ b/index.js
|
||||
@@ -14,6 +14,7 @@ const interactableBlocks = require('./lib/interactable.json')
|
||||
@@ -14,6 +14,7 @@
|
||||
|
||||
function inject (bot) {
|
||||
const waterType = bot.registry.blocksByName.water.id
|
||||
@@ -31,7 +31,16 @@ index b38bd30..2ca8205 100644
|
||||
const ladderId = bot.registry.blocksByName.ladder.id
|
||||
const vineId = bot.registry.blocksByName.vine.id
|
||||
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) => {
|
||||
@@ -42,7 +51,19 @@ index b38bd30..2ca8205 100644
|
||||
const generator = bot.pathfinder.getPathFromTo(movements, bot.entity.position, goal, { timeout })
|
||||
const { value: { result, astarContext: context } } = generator.next()
|
||||
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]
|
||||
if (curPoint.toBreak.length > 0 || curPoint.toPlace.length > 0) break
|
||||
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))) {
|
||||
curPoint.x = Math.floor(curPoint.x) + 0.5
|
||||
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(() => {
|
||||
lockUseBlock.release()
|
||||
placingBlock = nextPoint.toPlace.shift()
|
||||
@@ -69,7 +90,7 @@ index b38bd30..2ca8205 100644
|
||||
}, err => {
|
||||
console.error(err)
|
||||
lockUseBlock.release()
|
||||
@@ -550,6 +568,7 @@ function inject (bot) {
|
||||
@@ -550,6 +575,7 @@
|
||||
lockEquipItem.release()
|
||||
const refBlock = bot.blockAt(new Vec3(placingBlock.x, placingBlock.y, placingBlock.z), false)
|
||||
if (!lockPlaceBlock.tryAcquire()) return
|
||||
@@ -77,7 +98,7 @@ index b38bd30..2ca8205 100644
|
||||
if (interactableBlocks.includes(refBlock.name)) {
|
||||
bot.setControlState('sneak', true)
|
||||
}
|
||||
@@ -557,6 +576,7 @@ function inject (bot) {
|
||||
@@ -557,6 +583,7 @@
|
||||
.then(function () {
|
||||
// Dont release Sneak if the block placement was not successful
|
||||
bot.setControlState('sneak', false)
|
||||
@@ -85,7 +106,7 @@ index b38bd30..2ca8205 100644
|
||||
if (bot.pathfinder.LOSWhenPlacingBlocks && placingBlock.returnPos) returningPos = placingBlock.returnPos.clone()
|
||||
})
|
||||
.catch(_ignoreError => {
|
||||
@@ -576,7 +596,7 @@ function inject (bot) {
|
||||
@@ -576,7 +603,7 @@
|
||||
let dx = nextPoint.x - p.x
|
||||
const dy = nextPoint.y - p.y
|
||||
let dz = nextPoint.z - p.z
|
||||
@@ -94,7 +115,7 @@ index b38bd30..2ca8205 100644
|
||||
// arrived at next point
|
||||
lastNodeTime = performance.now()
|
||||
if (stopPathing) {
|
||||
@@ -611,6 +631,9 @@ function inject (bot) {
|
||||
@@ -611,6 +638,9 @@
|
||||
if (bot.entity.isInWater) {
|
||||
bot.setControlState('jump', true)
|
||||
bot.setControlState('sprint', false)
|
||||
@@ -104,11 +125,36 @@ index b38bd30..2ca8205 100644
|
||||
} 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..84a0841 100644
|
||||
index a7e3505..patched 100644
|
||||
--- a/lib/movements.js
|
||||
+++ b/lib/movements.js
|
||||
@@ -50,7 +50,12 @@ class Movements {
|
||||
@@ -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)
|
||||
@@ -122,7 +168,7 @@ index a7e3505..84a0841 100644
|
||||
|
||||
this.liquids = new Set()
|
||||
this.liquids.add(registry.blocksByName.water.id)
|
||||
@@ -62,7 +67,13 @@ class Movements {
|
||||
@@ -62,7 +67,13 @@
|
||||
|
||||
this.climbables = new Set()
|
||||
this.climbables.add(registry.blocksByName.ladder.id)
|
||||
@@ -137,7 +183,7 @@ index a7e3505..84a0841 100644
|
||||
this.emptyBlocks = new Set()
|
||||
|
||||
this.replaceables = new Set()
|
||||
@@ -92,13 +103,15 @@ class Movements {
|
||||
@@ -92,13 +103,15 @@
|
||||
}
|
||||
})
|
||||
registry.blocksArray.forEach(block => {
|
||||
@@ -155,7 +201,7 @@ index a7e3505..84a0841 100644
|
||||
|
||||
this.exclusionAreasStep = []
|
||||
this.exclusionAreasBreak = []
|
||||
@@ -230,8 +243,13 @@ class Movements {
|
||||
@@ -230,8 +243,13 @@
|
||||
}
|
||||
}
|
||||
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.liquid = this.liquids.has(b.type)
|
||||
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.getNumEntitiesAt(block.position, 0, 0, 0) * this.entityCost
|
||||
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
|
||||
toBreak.push(block.position)
|
||||
|
||||
@@ -387,8 +417,8 @@ class Movements {
|
||||
@@ -387,8 +417,8 @@
|
||||
cost += this.safeOrBreak(blockB, toBreak)
|
||||
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
|
||||
} else {
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -256,7 +302,7 @@ index a7e3505..84a0841 100644
|
||||
// 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 @@ class Movements {
|
||||
@@ -656,8 +734,27 @@
|
||||
this.getMoveDown(node, neighbors)
|
||||
this.getMoveUp(node, neighbors)
|
||||
|
||||
|
||||
Generated
+9
-9
@@ -250,7 +250,7 @@ overrides:
|
||||
|
||||
patchedDependencies:
|
||||
mineflayer-pathfinder:
|
||||
hash: 4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1
|
||||
hash: 4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797
|
||||
path: patches/mineflayer-pathfinder.patch
|
||||
mineflayer@4.33.0:
|
||||
hash: 03a5a80439dece74f880cf754f7317b3c9e4c5dbc6488eba67c36ab4fbfbea3a
|
||||
@@ -3069,7 +3069,7 @@ importers:
|
||||
version: 14.1.0(vue@3.5.26(typescript@5.9.3))
|
||||
'@wxt-dev/module-vue':
|
||||
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:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
@@ -3189,7 +3189,7 @@ importers:
|
||||
version: 1.6.0(encoding@0.1.13)
|
||||
mineflayer-pathfinder:
|
||||
specifier: ^2.4.5
|
||||
version: 2.4.5(patch_hash=4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1)
|
||||
version: 2.4.5(patch_hash=4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797)
|
||||
mineflayer-pvp:
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2(encoding@0.1.13)(prismarine-registry@1.11.0)
|
||||
@@ -23309,9 +23309,9 @@ snapshots:
|
||||
'@types/filesystem': 0.0.36
|
||||
'@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:
|
||||
'@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)
|
||||
transitivePeerDependencies:
|
||||
- vite
|
||||
@@ -27880,13 +27880,13 @@ snapshots:
|
||||
mineflayer-collectblock@1.6.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
mineflayer-pathfinder@2.4.5(patch_hash=4e932c106617efe7663216fc5ed42556761146633ebae88a5c831dc253abafa1):
|
||||
mineflayer-pathfinder@2.4.5(patch_hash=4bbfdca823ab48b74086e6e7d4b2f9baf5ee7c0ba9aee0c279b3c91c50bfd797):
|
||||
dependencies:
|
||||
minecraft-data: 3.102.3
|
||||
prismarine-block: 1.22.0
|
||||
@@ -27899,7 +27899,7 @@ snapshots:
|
||||
mineflayer-pvp@1.3.2(encoding@0.1.13)(prismarine-registry@1.11.0):
|
||||
dependencies:
|
||||
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)
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
@@ -27909,7 +27909,7 @@ snapshots:
|
||||
mineflayer-tool@1.2.0(encoding@0.1.13):
|
||||
dependencies:
|
||||
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
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
@@ -82,21 +82,24 @@ export const actionsList: Action[] = [
|
||||
const targetStart = getPlayerPos()
|
||||
const distanceToTargetBefore = targetStart ? selfStart.distanceTo(targetStart) : null
|
||||
|
||||
// TODO estimate time cost based on distance, trigger failure if time runs out
|
||||
const ok = await skills.goToPlayer(mineflayer, player_name, closeness)
|
||||
const result = await skills.goToPlayer(mineflayer, player_name, closeness)
|
||||
|
||||
const selfEnd = cloneVec3(mineflayer.bot.entity.position)
|
||||
const targetEnd = getPlayerPos()
|
||||
const distanceToTargetAfter = targetEnd ? selfEnd.distanceTo(targetEnd) : null
|
||||
|
||||
return {
|
||||
ok,
|
||||
ok: result.ok,
|
||||
reason: result.reason,
|
||||
target: { player_name, closeness },
|
||||
startPos: toCoord(selfStart),
|
||||
endPos: toCoord(selfEnd),
|
||||
movedDistance: selfStart.distanceTo(selfEnd),
|
||||
distanceToTargetBefore,
|
||||
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 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 distanceToTargetAfter = selfEnd.distanceTo(targetVec)
|
||||
|
||||
return {
|
||||
ok,
|
||||
ok: result.ok,
|
||||
reason: result.reason,
|
||||
target: { x, y, z, closeness },
|
||||
startPos: toCoord(selfStart),
|
||||
endPos: toCoord(selfEnd),
|
||||
@@ -163,6 +167,9 @@ export const actionsList: Action[] = [
|
||||
distanceToTargetBefore,
|
||||
distanceToTargetAfter,
|
||||
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.
|
||||
- **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.
|
||||
- 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)
|
||||
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 { useLogger } from '../../utils/logger'
|
||||
import { breakBlockAt } from '../blocks'
|
||||
import { patchedGoto } from '../patched-goto'
|
||||
import { getNearestBlocks } from '../world'
|
||||
import { expandBlockAliases } from './block-type-normalizer'
|
||||
import { ensurePickaxe } from './ensure'
|
||||
@@ -105,7 +106,11 @@ export async function collectBlock(
|
||||
veinBlock.position.y,
|
||||
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
|
||||
await mineAndCollect(mineflayer, veinBlock)
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ActionError } from '../../utils/errors'
|
||||
import { useLogger } from '../../utils/logger'
|
||||
import { McData } from '../../utils/mcdata'
|
||||
import { goToPosition } from '../movement'
|
||||
import { patchedGoto } from '../patched-goto'
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
@@ -161,7 +162,7 @@ export async function placeBlock(
|
||||
),
|
||||
)
|
||||
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
||||
await mineflayer.bot.pathfinder.goto(goal)
|
||||
await patchedGoto(mineflayer.bot, goal)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const pos = block.position
|
||||
// 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)
|
||||
logger.log(
|
||||
@@ -388,10 +389,7 @@ export async function pickupNearbyItems(
|
||||
let pickedUp = 0
|
||||
while (nearestItem) {
|
||||
// bot.pathfinder.setMovements(new pf.Movements(bot));
|
||||
await mineflayer.bot.pathfinder.goto(
|
||||
new pathfinder.goals.GoalFollow(nearestItem, 0.8),
|
||||
() => { },
|
||||
)
|
||||
await patchedGoto(mineflayer.bot, new pathfinder.goals.GoalFollow(nearestItem, 0.8))
|
||||
await sleep(500)
|
||||
const prev = nearestItem
|
||||
nearestItem = getNearestItem(mineflayer.bot)
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Vec3 } from 'vec3'
|
||||
import { McData } from '../utils/mcdata'
|
||||
import { log } from './base'
|
||||
import { goToPosition } from './movement'
|
||||
import { patchedGoto } from './patched-goto'
|
||||
import { getNearestBlock, getNearestBlocks, getPosition, shouldPlaceTorch } from './world'
|
||||
|
||||
const { goals, Movements } = pathfinderModel
|
||||
@@ -81,7 +82,7 @@ async function moveIntoRange(mineflayer: Mineflayer, block: any) {
|
||||
movements.allowParkour = false
|
||||
movements.allowSprinting = false
|
||||
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)
|
||||
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) {
|
||||
const pos = targetBlock.position
|
||||
const movements = new Movements(mineflayer.bot)
|
||||
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))
|
||||
}
|
||||
|
||||
async function tryPlaceBlock(
|
||||
|
||||
@@ -9,6 +9,7 @@ import { sleep } from '@moeru/std'
|
||||
|
||||
import { isHostile } from '../utils/mcdata'
|
||||
import { log } from './base'
|
||||
import { patchedGoto } from './patched-goto'
|
||||
import { getNearbyEntities, getNearestEntityWhere } from './world'
|
||||
|
||||
const { goals } = pathfinderModel
|
||||
@@ -71,7 +72,7 @@ export async function attackEntity(
|
||||
if (!kill) {
|
||||
if (mineflayer.bot.entity.position.distanceTo(pos) > 5) {
|
||||
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)
|
||||
return true
|
||||
@@ -101,7 +102,7 @@ export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise<boo
|
||||
&& enemy.name !== 'creeper' && enemy.name !== 'phantom') {
|
||||
try {
|
||||
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 */ }
|
||||
}
|
||||
@@ -110,7 +111,7 @@ export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise<boo
|
||||
try {
|
||||
const followGoal = new goals.GoalFollow(enemy, 2)
|
||||
const invertedGoal = new goals.GoalInvert(followGoal)
|
||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
||||
await patchedGoto(mineflayer.bot, invertedGoal)
|
||||
}
|
||||
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 { Mineflayer } from '../libs/mineflayer'
|
||||
import type { PathfindProgressInfo, PathfindResult } from './patched-goto'
|
||||
|
||||
import pathfinder from 'mineflayer-pathfinder'
|
||||
|
||||
@@ -11,8 +12,11 @@ import { Vec3 } from 'vec3'
|
||||
|
||||
import { useLogger } from '../utils/logger'
|
||||
import { log } from './base'
|
||||
import { patchedGoto } from './patched-goto'
|
||||
import { getNearestBlock, getNearestEntityWhere } from './world'
|
||||
|
||||
export type { PathfindProgressInfo, PathfindResult } from './patched-goto'
|
||||
|
||||
const logger = useLogger()
|
||||
const { goals, Movements } = pathfinder
|
||||
|
||||
@@ -22,16 +26,39 @@ export async function goToPosition(
|
||||
y: number,
|
||||
z: number,
|
||||
minDistance = 2,
|
||||
): Promise<boolean> {
|
||||
options: { onProgress?: (info: PathfindProgressInfo) => void } = {},
|
||||
): Promise<PathfindResult> {
|
||||
if (x == null || y == null || z == null) {
|
||||
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) {
|
||||
mineflayer.bot.chat(`/tp @s ${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 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
|
||||
}
|
||||
|
||||
await mineflayer.bot.pathfinder.goto(new goals.GoalNear(x, y, z, minDistance))
|
||||
log(mineflayer, `You have reached ${x}, ${y}, ${z}.`)
|
||||
return true
|
||||
const result = await patchedGoto(mineflayer.bot, new goals.GoalNear(x, y, z, minDistance), {
|
||||
onProgress: options.onProgress,
|
||||
})
|
||||
|
||||
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(
|
||||
@@ -65,7 +101,10 @@ export async function goToNearestBlock(
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -88,36 +127,68 @@ export async function goToNearestEntity(
|
||||
|
||||
const distance = mineflayer.bot.entity.position.distanceTo(entity.position)
|
||||
log(mineflayer, `Found ${entityType} ${distance} blocks away.`)
|
||||
await goToPosition(
|
||||
const result = await goToPosition(
|
||||
mineflayer,
|
||||
entity.position.x,
|
||||
entity.position.y,
|
||||
entity.position.z,
|
||||
minDistance,
|
||||
)
|
||||
return true
|
||||
return result.ok
|
||||
}
|
||||
|
||||
export async function goToPlayer(
|
||||
mineflayer: Mineflayer,
|
||||
username: string,
|
||||
distance = 3,
|
||||
): Promise<boolean> {
|
||||
options: { onProgress?: (info: PathfindProgressInfo) => void } = {},
|
||||
): Promise<PathfindResult> {
|
||||
if (mineflayer.allowCheats) {
|
||||
mineflayer.bot.chat(`/tp @s ${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
|
||||
if (!player) {
|
||||
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))
|
||||
log(mineflayer, `You have reached ${username}.`)
|
||||
return true
|
||||
const result = await patchedGoto(mineflayer.bot, new goals.GoalFollow(player, distance), {
|
||||
onProgress: options.onProgress,
|
||||
})
|
||||
|
||||
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(
|
||||
@@ -172,11 +243,11 @@ export async function moveAway(mineflayer: Mineflayer, distance: number): Promis
|
||||
|
||||
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
|
||||
logger.log(`I moved away from nearest entity to ${newPos}.`)
|
||||
await sleep(500)
|
||||
return true
|
||||
return result.ok
|
||||
}
|
||||
catch (err) {
|
||||
logger.log(`I failed to move away: ${(err as Error).message}`)
|
||||
@@ -191,8 +262,8 @@ export async function moveAwayFromEntity(
|
||||
): Promise<boolean> {
|
||||
const goal = new goals.GoalFollow(entity, distance)
|
||||
const invertedGoal = new goals.GoalInvert(goal)
|
||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
||||
return true
|
||||
const result = await patchedGoto(mineflayer.bot, invertedGoal)
|
||||
return result.ok
|
||||
}
|
||||
|
||||
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