From 4bbdb5cafde339e080f186d889899daf4aa9d627 Mon Sep 17 00:00:00 2001 From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com> Date: Fri, 19 Jun 2026 12:58:33 +0200 Subject: [PATCH 1/2] fix(executor): unstick the bot when it wedges on a wall corner When walking along a wall (e.g. detouring around it) the bot could wedge its hitbox against the wall's corner block: it kept pressing forward along the path (into the wall) and stopped moving, oscillating in place forever. The forward executor's stall safety net was commented out and recovery is disabled, so the move never ended and goto() never returned. NewForwardExecutor now tracks XZ progress per move. After 12 ticks of no progress it enters an unstick: stop sprinting and steer at the target block's centre, which adds the sideways push needed to slide off the corner instead of ramming the wall. If that still makes no progress after 40 ticks it throws CancelError so the path executor can give up cleanly. The detector sits after the mining/placing early-returns, so it never fires while interacting, and it keys on real position progress (not a tick count), so slow legit moves are fine. Verified in-game on a real offline 1.20.4 server: - wall-detour now completes; full movement suite 5/5 (flat, diagonal, staircase, drop, wall) on two runs, no regression. - wall stress 10/10 across 5 geometries x 2 rounds (standard, long, north-side detour, 5-tall, L-shaped corner). - build + ts-standard clean, 31/31 unit tests pass. Pre-existing bug (reproduces on 2026-rewrite before this change). Touches only the forward executor; recovery and cost subsystems are untouched. --- .../movements/movementExecutors.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/mineflayer-specific/movements/movementExecutors.ts b/src/mineflayer-specific/movements/movementExecutors.ts index 3c0cda2..7f303d1 100644 --- a/src/mineflayer-specific/movements/movementExecutors.ts +++ b/src/mineflayer-specific/movements/movementExecutors.ts @@ -35,6 +35,16 @@ export class IdleMovementExecutor extends MovementExecutor { } export class NewForwardExecutor extends MovementExecutor { + // Stall guard state: how many ticks in a row the bot has barely moved, and the + // position we last measured from. Used to detect being wedged against a block + // corner (see performPerTick) so the path executor can replan instead of + // grinding forever. Number of "no progress" ticks before we give up. + private stuckTicks = 0 + private stuckLastPos: Vec3 | null = null + private static readonly STUCK_UNSTICK_AT = 12 // ticks of no progress before we try to unstick + private static readonly STUCK_TICK_LIMIT = 40 // ticks of no progress before we give up and replan + private static readonly STUCK_MIN_STEP = 0.05 // XZ blocks/tick below which we count as "no progress" + protected isComplete (startMove: Move, endMove?: Move, opts?: CompleteOpts): boolean { return super.isComplete(startMove, endMove, { ticks: 2 }) } @@ -201,6 +211,43 @@ export class NewForwardExecutor extends MovementExecutor { throw new CancelError(`ForwardMove: not on ground. Target pos: ${thisMove.exitPos}, us: ${this.bot.entity.position}`) } + // Stall guard. Walking along a wall, the bot can wedge its hitbox against a + // block corner: it keeps pressing forward (mostly along the path, into the + // wall) and stops moving. Mining/placing returned above, so reaching here + // means we expect to be travelling. + // + // When we notice no XZ progress, first try to UNSTICK: aim straight at the + // centre of the target block and stop sprinting. Slowing down and steering + // at the centre adds the sideways push needed to slide off the corner, + // instead of ramming the wall. Only if that fails for a whole second do we + // give up with a CancelError so the path executor can replan from here. + if (tickCount === 0) { + this.stuckTicks = 0 + this.stuckLastPos = null + } + const here = this.bot.entity.position + if (this.stuckLastPos != null && here.xzDistanceTo(this.stuckLastPos) < NewForwardExecutor.STUCK_MIN_STEP) { + this.stuckTicks++ + } else if (this.stuckTicks > 0) { + this.stuckTicks-- // made progress: ease back out of unstick mode + } + this.stuckLastPos = here.clone() + + if (this.stuckTicks >= NewForwardExecutor.STUCK_TICK_LIMIT) { + this.stuckTicks = 0 + this.stuckLastPos = null + throw new CancelError(`ForwardMove: stuck (no progress) near ${thisMove.exitPos}`) + } + + if (this.stuckTicks >= NewForwardExecutor.STUCK_UNSTICK_AT) { + const centre = thisMove.exitPos.floored().translate(0.5, 0, 0.5) + this.bot.setControlState('jump', false) + this.bot.setControlState('sprint', false) + botSmartMovement(this.bot, centre, false) + botStrafeMovement(this.bot, centre) + return this.isComplete(thisMove) + } + const faceForward = await this.faceForward() if (faceForward) { From 20f99a7b4943b9eeb25657e4646e5e41ffecf3c3 Mon Sep 17 00:00:00 2001 From: XaXayo12 <257781493+XaXayo12@users.noreply.github.com> Date: Fri, 19 Jun 2026 13:14:36 +0200 Subject: [PATCH 2/2] fix(executor): don't let the stall guard abort an in-progress dig Review catch (chatgpt-codex): `allowExternalInfluence` returns true for a reachable block mid-dig, so a Forward/Diagonal move that breaks a slow block (stone by hand, obsidian, ...) falls through to the stall guard. The bot stands still against the still-solid block during the dig, so XZ progress stays under the threshold and the guard CancelError'd valid mining after 40 ticks. Now the no-progress counter is skipped whenever an interaction is in progress (this.cI != null): mining/placing is not a movement stall. The corner-unstick behaviour for real travel is unchanged. --- src/mineflayer-specific/movements/movementExecutors.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/mineflayer-specific/movements/movementExecutors.ts b/src/mineflayer-specific/movements/movementExecutors.ts index 7f303d1..36651ef 100644 --- a/src/mineflayer-specific/movements/movementExecutors.ts +++ b/src/mineflayer-specific/movements/movementExecutors.ts @@ -226,7 +226,13 @@ export class NewForwardExecutor extends MovementExecutor { this.stuckLastPos = null } const here = this.bot.entity.position - if (this.stuckLastPos != null && here.xzDistanceTo(this.stuckLastPos) < NewForwardExecutor.STUCK_MIN_STEP) { + // Mining/placing legitimately keeps the bot still, and a slow dig can outlast + // the stall window. An interaction in progress (this.cI != null) is NOT a + // movement stall — the allowExternalInfluence check above lets a reachable dig + // fall through to here — so don't count those ticks against the stall budget. + if (this.cI != null) { + this.stuckTicks = 0 + } else if (this.stuckLastPos != null && here.xzDistanceTo(this.stuckLastPos) < NewForwardExecutor.STUCK_MIN_STEP) { this.stuckTicks++ } else if (this.stuckTicks > 0) { this.stuckTicks-- // made progress: ease back out of unstick mode