Root cause of "bot just stands still": every gather.* skill was using
bot.findBlock({ matching: (b) => names.includes(b.name) }), and under
mineflayer 1.21.4 + ViaBackwards the Block objects fed into the
callback have a wrong .name field (Block.type / numeric id is still
correct — this is mineflayer issue #2347). Every search returned null,
every skill reported "no_target", reflex looped wander → tunnel-out
forever. The bot's logs said "dispatch ok" while the operator watched
it pace in circles.
Proven live with a new diag.match skill on play.xmatic.team:
findBlocks({matching: numericIds}) → 50 hits
findBlocks({matching: (b) => b.name === ...}) → 0 hits ← the bug
findBlock({matching: (b) => b.name === ...}) → null ← the bug
findBlock({matching: numericIds}) → dark_oak_log @ (606,62,110)
After this fix the same bot from the same spawn dispatches gather.logs
and reaches the chop loop ("chop: dark_oak_log at 606,62,110 (tool=fists)")
instead of returning "no reachable log within 64 blocks".
Changes:
- runtime/perception.js (new): findBlocksByName / findNearestBlockByName
centralise the numeric-id workaround for any future skill.
- runtime/actions.js: chopNearestTree, sleepInBed, placeCraftingTable now
use perception. Also load mineflayer-tool plugin alongside collectblock
(collectblock 1.6 hard-requires bot.tool to dispatch a dig).
- gather-stone, gather-wool, deposit-surplus rewritten to numeric-id
search. gather-wool also loads mineflayer-tool.
- diagnose-scan.js (new): two diagnostic skills — diag.scan reports
findBlocks counts per radius for common blocks; diag.match cross-tests
the four matcher styles so this regression can be re-proven on demand.
- runtime/skills/index.js: registers diag.scan + diag.match.
Memory: project_findblock_callback_broken_under_viabackwards.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
125 lines
4.3 KiB
JavaScript
125 lines
4.3 KiB
JavaScript
// village.deposit-surplus — find the nearest placed chest, open it, and
|
||
// transfer any stack the bot is over-carrying (logs, cobble, dirt,
|
||
// seeds). Keeps a small "essentials" reserve in inventory so the bot
|
||
// keeps its tools, food and bed.
|
||
//
|
||
// What counts as surplus:
|
||
// * any item whose count exceeds RESERVE_PER_NAME (default: keep 8 of
|
||
// each named item), UNLESS it's in KEEP_ALWAYS (tools/bed/food).
|
||
// * raw materials that look strictly storable (logs/cobble/dirt/sand).
|
||
|
||
import pathfinderPkg from "mineflayer-pathfinder";
|
||
const { pathfinder, goals, Movements } = pathfinderPkg;
|
||
|
||
import { applyProfile, PROFILES } from "../movement-profiles.js";
|
||
import { info, warn } from "../log.js";
|
||
import { findNearestBlockByName } from "../perception.js";
|
||
|
||
const CHEST_NAMES = ["chest", "trapped_chest"];
|
||
|
||
const KEEP_ALWAYS_NAME_RE = /(_axe|_pickaxe|_sword|_shovel|_hoe|_bed|bread|cooked_|apple|carrot|potato|wheat_seeds)$/;
|
||
const STORABLE_NAME_RE = /(_log$|_stem$|cobblestone|cobbled_deepslate|deepslate|stone$|dirt|sand|gravel|wheat$|_planks$|stick$)/;
|
||
const RESERVE_PER_NAME = 8;
|
||
|
||
let pluginLoaded = new WeakSet();
|
||
function ensurePathfinder(bot) {
|
||
if (pluginLoaded.has(bot)) return;
|
||
bot.loadPlugin(pathfinder);
|
||
pluginLoaded.add(bot);
|
||
}
|
||
|
||
function withTimeout(promise, ms, label) {
|
||
let timer;
|
||
const timeout = new Promise((_, reject) => {
|
||
timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms / 1000}s`)), ms);
|
||
});
|
||
return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
|
||
}
|
||
|
||
function pickSurplus(bot) {
|
||
const out = [];
|
||
// Group inventory items by name, then decide how much to deposit per name.
|
||
const grouped = new Map();
|
||
for (const item of bot.inventory.items()) {
|
||
if (!grouped.has(item.name)) grouped.set(item.name, []);
|
||
grouped.get(item.name).push(item);
|
||
}
|
||
for (const [name, items] of grouped) {
|
||
if (KEEP_ALWAYS_NAME_RE.test(name)) continue;
|
||
const total = items.reduce((s, i) => s + i.count, 0);
|
||
const storable = STORABLE_NAME_RE.test(name);
|
||
const reserve = storable ? Math.min(RESERVE_PER_NAME, total) : 0;
|
||
const surplus = total - reserve;
|
||
if (surplus <= 0) continue;
|
||
out.push({ name, surplus, items });
|
||
}
|
||
return out;
|
||
}
|
||
|
||
export const skill = Object.freeze({
|
||
id: "village.deposit-surplus",
|
||
title: "Deposit surplus items in a chest",
|
||
timeoutMs: 60_000,
|
||
preconditions(ctx) {
|
||
if (!ctx?.bot) return { ok: false, code: "no_bot", detail: "bot missing" };
|
||
const surplus = pickSurplus(ctx.bot);
|
||
if (surplus.length === 0) return { ok: false, code: "nothing_to_deposit", detail: "no surplus stacks" };
|
||
const chest = findNearestBlockByName(ctx.bot, CHEST_NAMES, { maxDistance: 24 });
|
||
if (!chest) return { ok: false, code: "no_chest", detail: "no chest within 24 blocks" };
|
||
return { ok: true };
|
||
},
|
||
async execute(ctx) {
|
||
const bot = ctx.bot;
|
||
const chest = findNearestBlockByName(bot, CHEST_NAMES, { maxDistance: 24 });
|
||
if (!chest) return { ok: false, code: "no_chest", detail: "no chest after move", worldDelta: null };
|
||
|
||
ensurePathfinder(bot);
|
||
applyProfile(PROFILES.TRAVEL, bot);
|
||
try {
|
||
await withTimeout(
|
||
bot.pathfinder.goto(new goals.GoalNear(chest.position.x, chest.position.y, chest.position.z, 1)),
|
||
30_000,
|
||
"goto chest",
|
||
);
|
||
} catch (e) {
|
||
return { ok: false, code: "no_path", detail: e.message, worldDelta: null };
|
||
}
|
||
|
||
let chestHandle;
|
||
try {
|
||
chestHandle = await withTimeout(bot.openContainer(chest), 8_000, "openChest");
|
||
} catch (e) {
|
||
return { ok: false, code: "open_failed", detail: e.message, worldDelta: null };
|
||
}
|
||
|
||
let deposited = 0;
|
||
const detail = [];
|
||
try {
|
||
for (const { name, surplus } of pickSurplus(bot)) {
|
||
const ref = bot.registry?.itemsByName?.[name];
|
||
if (!ref) continue;
|
||
try {
|
||
await withTimeout(chestHandle.deposit(ref.id, null, surplus), 10_000, `deposit ${name}`);
|
||
deposited += surplus;
|
||
detail.push(`${name}×${surplus}`);
|
||
info("action", `village.deposit-surplus: ${name}×${surplus}`);
|
||
} catch (e) {
|
||
warn("action", `village.deposit-surplus: ${name} failed: ${e.message}`);
|
||
}
|
||
}
|
||
} finally {
|
||
try { await chestHandle.close(); } catch {}
|
||
}
|
||
|
||
if (deposited === 0) {
|
||
return { ok: false, code: "deposit_failed", detail: "opened chest but deposited nothing", worldDelta: null };
|
||
}
|
||
return {
|
||
ok: true,
|
||
code: "done",
|
||
detail: { deposited, items: detail },
|
||
worldDelta: { depositedTotal: deposited },
|
||
};
|
||
},
|
||
});
|