Merge pull request #2 from xmatic-squad/feat/mindcraft-skills
feat(perception): mindcraft skills/world + 15 Pi tools (proper perceive→decide→act loop)
This commit was merged in pull request #2.
This commit is contained in:
@@ -30,6 +30,46 @@ The two files that shape your day-to-day choices:
|
||||
|
||||
When no operator task is active and chat is quiet, you work towards the goal. See Operating principle #5 below for the priority order.
|
||||
|
||||
## Perception → decision → action (read FIRST before any world action)
|
||||
|
||||
You used to be blind: you knew your coords but nothing about what was around you. Now you have proper perception. The cycle is:
|
||||
|
||||
1. **Observe.** Call `mc_observe` first. You get a JSON snapshot of position, health, food, time of day, weather, biome, nearby block types, nearby entities (mobs + players + items), inventory counts.
|
||||
2. **Decide.** Based on the snapshot, pick the **single** most relevant next action. Don't plan 5 steps deep; the snapshot will change after you act.
|
||||
3. **Act.** Call ONE high-level tool from the catalog below.
|
||||
4. **Loop.** Observe again. Append one short diary line if a milestone shifted.
|
||||
|
||||
### Tool catalog — mindcraft-skills.ts (use these)
|
||||
|
||||
**Perception (cheap, read-only):**
|
||||
|
||||
- `mc_observe(radius?)` — one-shot full snapshot. **Use this first in autonomous mode.**
|
||||
- `mc_inventory()` — items as `{name: count}`.
|
||||
- `mc_nearby_blocks(radius?)` — distinct block types within radius (default 16).
|
||||
- `mc_nearby_entities(radius?)` — players, mobs, dropped items with approximate distances.
|
||||
|
||||
**Actions (write to world, may take seconds-to-minutes):**
|
||||
|
||||
- `mc_collect_block(blockType, count?)` — walk to a block of that type, equip the right tool, mine N of them, pick them up. The all-in-one "gather wood / stone / iron" primitive.
|
||||
- `mc_place_block(blockType, x, y, z)` — place one block from inventory at exact coords.
|
||||
- `mc_go_to(x, y, z, minDistance?)` — pathfinder navigation with permission to dig soft obstacles (leaves) and jump.
|
||||
- `mc_go_to_block(blockType, minDistance?, range?)` — find nearest block of type within `range` and walk there.
|
||||
- `mc_craft(itemName, num?)` — craft from inventory. Uses a nearby crafting table when needed.
|
||||
- `mc_equip(itemName)` — hold a tool or wear armor.
|
||||
- `mc_consume(itemName?)` — eat food. Empty arg = first food in inventory.
|
||||
- `mc_defend_self(range?)` — attack hostile mobs within range until clear. Uses best weapon.
|
||||
- `mc_avoid_enemies(distance?)` — run away from nearest hostiles by ~N blocks.
|
||||
- `mc_stay(seconds?)` — stand still N seconds (default 30). Use to wait out night or regen.
|
||||
- `mc_pickup_nearby()` — collect dropped items in vicinity.
|
||||
|
||||
### Deprecated (do not use)
|
||||
|
||||
- `mc_build_pyramid_5x5` — narrow-purpose, pre-perception era. Build via `mc_place_block` loops if needed.
|
||||
- `mc_dig(x, y, z)` — too low-level. Use `mc_collect_block(blockType, n)` which handles the whole cycle.
|
||||
- `mc_goto(x, y, z)` — had over-strict safety guards that refused legitimate paths. Replaced by `mc_go_to` from mindcraft-skills.
|
||||
|
||||
The deprecated tools may still appear in your registry for now; ignore them. They will be removed in a follow-up cleanup.
|
||||
|
||||
## Your tools right now
|
||||
|
||||
When you start, you have:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Kolby Nottingham
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,596 @@
|
||||
import minecraftData from 'minecraft-data';
|
||||
import settings from './settings.js';
|
||||
import { createBot } from 'mineflayer';
|
||||
import prismarine_items from 'prismarine-item';
|
||||
import { pathfinder } from 'mineflayer-pathfinder';
|
||||
import { plugin as pvp } from 'mineflayer-pvp';
|
||||
import { plugin as collectblock } from 'mineflayer-collectblock';
|
||||
// mineflayer-auto-eat removed: our installed 5.x exports a default plugin
|
||||
// instead of a named one; we skip it entirely because skills.consume()
|
||||
// works via bot.consume() on plain Mineflayer.
|
||||
// import { plugin as autoEat } from 'mineflayer-auto-eat';
|
||||
import plugin from 'mineflayer-armor-manager';
|
||||
const armorManager = plugin;
|
||||
let mc_version = settings.minecraft_version;
|
||||
let mcdata = null;
|
||||
let Item = null;
|
||||
|
||||
/**
|
||||
* @typedef {string} ItemName
|
||||
* @typedef {string} BlockName
|
||||
*/
|
||||
|
||||
export const WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak', 'mangrove', 'cherry'];
|
||||
export const MATCHING_WOOD_BLOCKS = [
|
||||
'log',
|
||||
'planks',
|
||||
'sign',
|
||||
'boat',
|
||||
'fence_gate',
|
||||
'door',
|
||||
'fence',
|
||||
'slab',
|
||||
'stairs',
|
||||
'button',
|
||||
'pressure_plate',
|
||||
'trapdoor'
|
||||
]
|
||||
export const WOOL_COLORS = [
|
||||
'white',
|
||||
'orange',
|
||||
'magenta',
|
||||
'light_blue',
|
||||
'yellow',
|
||||
'lime',
|
||||
'pink',
|
||||
'gray',
|
||||
'light_gray',
|
||||
'cyan',
|
||||
'purple',
|
||||
'blue',
|
||||
'brown',
|
||||
'green',
|
||||
'red',
|
||||
'black'
|
||||
]
|
||||
|
||||
|
||||
export function initBot(username) {
|
||||
const options = {
|
||||
username: username,
|
||||
host: settings.host,
|
||||
port: settings.port,
|
||||
auth: settings.auth,
|
||||
version: mc_version,
|
||||
checkTimeoutInterval: 60000, // 60s keep-alive check (default 30s) — reduces disconnects on slow servers
|
||||
}
|
||||
if (!mc_version || mc_version === "auto") {
|
||||
delete options.version;
|
||||
}
|
||||
|
||||
const bot = createBot(options);
|
||||
|
||||
// Throttle position packets to avoid kicks on Paper/Spigot servers
|
||||
// Paper enforces stricter packet rate limits than vanilla, causing ECONNRESET
|
||||
// when mineflayer sends position updates faster than 50ms apart
|
||||
let lastPositionUpdate = 0;
|
||||
let pendingPositionPacket = null;
|
||||
const POSITION_THROTTLE_MS = 50;
|
||||
const originalWrite = bot._client.write.bind(bot._client);
|
||||
bot._client.write = function(name, data) {
|
||||
if (name === 'position' || name === 'position_look' || name === 'look') {
|
||||
const now = Date.now();
|
||||
if (now - lastPositionUpdate < POSITION_THROTTLE_MS) {
|
||||
// Queue this packet so the last position update is never lost
|
||||
if (!pendingPositionPacket) {
|
||||
pendingPositionPacket = setTimeout(() => {
|
||||
pendingPositionPacket = null;
|
||||
lastPositionUpdate = Date.now();
|
||||
originalWrite(name, data);
|
||||
}, POSITION_THROTTLE_MS - (now - lastPositionUpdate));
|
||||
}
|
||||
return;
|
||||
}
|
||||
lastPositionUpdate = now;
|
||||
if (pendingPositionPacket) {
|
||||
clearTimeout(pendingPositionPacket);
|
||||
pendingPositionPacket = null;
|
||||
}
|
||||
}
|
||||
return originalWrite(name, data);
|
||||
};
|
||||
|
||||
// Suppress PartialReadError for non-critical packets
|
||||
// Paper servers sometimes send packets that node-minecraft-protocol
|
||||
// can't fully parse (scoreboard, resource_pack, custom_payload, etc.)
|
||||
// These errors crash the bot but the packets aren't needed for gameplay
|
||||
const originalEmit = bot._client.emit.bind(bot._client);
|
||||
bot._client.emit = function(event, ...args) {
|
||||
if (event === 'error' && args[0]) {
|
||||
const err = args[0];
|
||||
const errStr = err instanceof Error ? err.message : String(err);
|
||||
if (errStr.includes('PartialReadError')) {
|
||||
console.warn('[mcdata] Suppressed PartialReadError:', errStr.substring(0, 120));
|
||||
return true; // Swallow the error
|
||||
}
|
||||
}
|
||||
return originalEmit(event, ...args);
|
||||
};
|
||||
|
||||
bot.loadPlugin(pathfinder);
|
||||
bot.loadPlugin(pvp);
|
||||
bot.loadPlugin(collectblock);
|
||||
// bot.loadPlugin(autoEat); // removed — see import comment
|
||||
bot.loadPlugin(armorManager); // auto equip armor
|
||||
bot.once('resourcePack', () => {
|
||||
bot.acceptResourcePack();
|
||||
});
|
||||
|
||||
bot.once('login', () => {
|
||||
mc_version = bot.version;
|
||||
mcdata = minecraftData(mc_version);
|
||||
Item = prismarine_items(mc_version);
|
||||
});
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
// pepa-pi-bot addition: attach pathing/combat/collect plugins to an
|
||||
// EXTERNALLY-created bot (we use our own mineflayer.createBot() in
|
||||
// mineflayer-bridge.ts). Mindcraft's initBot() above creates the bot itself;
|
||||
// this exposes the same plugin+login wiring for our case.
|
||||
//
|
||||
// auto-eat is intentionally NOT loaded — our installed version is 5.x while
|
||||
// Mindcraft's 3.x has diverged. skills.consume() falls back to bot.consume()
|
||||
// which works on plain Mineflayer without the plugin.
|
||||
export function attachPluginsAndInit(bot) {
|
||||
bot.loadPlugin(pathfinder);
|
||||
bot.loadPlugin(pvp);
|
||||
bot.loadPlugin(collectblock);
|
||||
bot.loadPlugin(armorManager);
|
||||
bot.once('login', () => {
|
||||
mc_version = bot.version;
|
||||
mcdata = minecraftData(mc_version);
|
||||
Item = prismarine_items(mc_version);
|
||||
});
|
||||
return bot;
|
||||
}
|
||||
|
||||
export function isHuntable(mob) {
|
||||
if (!mob || !mob.name) return false;
|
||||
const animals = ['chicken', 'cow', 'llama', 'mooshroom', 'pig', 'rabbit', 'sheep'];
|
||||
return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby
|
||||
}
|
||||
|
||||
export function isHostile(mob) {
|
||||
if (!mob || !mob.name) return false;
|
||||
return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem';
|
||||
}
|
||||
|
||||
// blocks that don't work with collectBlock, need to be manually collected
|
||||
export function mustCollectManually(blockName) {
|
||||
// all crops (that aren't normal blocks), torches, buttons, levers, redstone,
|
||||
const full_names = ['wheat', 'carrots', 'potatoes', 'beetroots', 'nether_wart', 'cocoa', 'sugar_cane', 'kelp', 'short_grass', 'fern', 'tall_grass', 'bamboo',
|
||||
'poppy', 'dandelion', 'blue_orchid', 'allium', 'azure_bluet', 'oxeye_daisy', 'cornflower', 'lilac', 'wither_rose', 'lily_of_the_valley', 'wither_rose',
|
||||
'lever', 'redstone_wire', 'lantern']
|
||||
const partial_names = ['sapling', 'torch', 'button', 'carpet', 'pressure_plate', 'mushroom', 'tulip', 'bush', 'vines', 'fern']
|
||||
return full_names.includes(blockName.toLowerCase()) || partial_names.some(partial => blockName.toLowerCase().includes(partial));
|
||||
}
|
||||
|
||||
export function getItemId(itemName) {
|
||||
let item = mcdata.itemsByName[itemName];
|
||||
if (item) {
|
||||
return item.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getItemName(itemId) {
|
||||
let item = mcdata.items[itemId]
|
||||
if (item) {
|
||||
return item.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getBlockId(blockName) {
|
||||
let block = mcdata.blocksByName[blockName];
|
||||
if (block) {
|
||||
return block.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getBlockName(blockId) {
|
||||
let block = mcdata.blocks[blockId]
|
||||
if (block) {
|
||||
return block.name;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getEntityId(entityName) {
|
||||
let entity = mcdata.entitiesByName[entityName];
|
||||
if (entity) {
|
||||
return entity.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getAllItems(ignore) {
|
||||
if (!ignore) {
|
||||
ignore = [];
|
||||
}
|
||||
let items = []
|
||||
for (const itemId in mcdata.items) {
|
||||
const item = mcdata.items[itemId];
|
||||
if (!ignore.includes(item.name)) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
export function getAllItemIds(ignore) {
|
||||
const items = getAllItems(ignore);
|
||||
let itemIds = [];
|
||||
for (const item of items) {
|
||||
itemIds.push(item.id);
|
||||
}
|
||||
return itemIds;
|
||||
}
|
||||
|
||||
export function getAllBlocks(ignore) {
|
||||
if (!ignore) {
|
||||
ignore = [];
|
||||
}
|
||||
let blocks = []
|
||||
for (const blockId in mcdata.blocks) {
|
||||
const block = mcdata.blocks[blockId];
|
||||
if (!ignore.includes(block.name)) {
|
||||
blocks.push(block);
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
}
|
||||
|
||||
export function getAllBlockIds(ignore) {
|
||||
const blocks = getAllBlocks(ignore);
|
||||
let blockIds = [];
|
||||
for (const block of blocks) {
|
||||
blockIds.push(block.id);
|
||||
}
|
||||
return blockIds;
|
||||
}
|
||||
|
||||
export function getAllBiomes() {
|
||||
return mcdata.biomes;
|
||||
}
|
||||
|
||||
export function getItemCraftingRecipes(itemName) {
|
||||
let itemId = getItemId(itemName);
|
||||
if (!mcdata.recipes[itemId]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let recipes = [];
|
||||
for (let r of mcdata.recipes[itemId]) {
|
||||
let recipe = {};
|
||||
let ingredients = [];
|
||||
if (r.ingredients) {
|
||||
ingredients = r.ingredients;
|
||||
} else if (r.inShape) {
|
||||
ingredients = r.inShape.flat();
|
||||
}
|
||||
for (let ingredient of ingredients) {
|
||||
let ingredientName = getItemName(ingredient);
|
||||
if (ingredientName === null) continue;
|
||||
if (!recipe[ingredientName])
|
||||
recipe[ingredientName] = 0;
|
||||
recipe[ingredientName]++;
|
||||
}
|
||||
recipes.push([
|
||||
recipe,
|
||||
{craftedCount : r.result.count}
|
||||
]);
|
||||
}
|
||||
// sort recipes by if their ingredients include common items
|
||||
const commonItems = ['oak_planks', 'oak_log', 'coal', 'cobblestone'];
|
||||
recipes.sort((a, b) => {
|
||||
let commonCountA = Object.keys(a[0]).filter(key => commonItems.includes(key)).reduce((acc, key) => acc + a[0][key], 0);
|
||||
let commonCountB = Object.keys(b[0]).filter(key => commonItems.includes(key)).reduce((acc, key) => acc + b[0][key], 0);
|
||||
return commonCountB - commonCountA;
|
||||
});
|
||||
|
||||
return recipes;
|
||||
}
|
||||
|
||||
export function isSmeltable(itemName) {
|
||||
const misc_smeltables = ['beef', 'chicken', 'cod', 'mutton', 'porkchop', 'rabbit', 'salmon', 'tropical_fish', 'potato', 'kelp', 'sand', 'cobblestone', 'clay_ball'];
|
||||
return itemName.includes('raw') || itemName.includes('log') || misc_smeltables.includes(itemName);
|
||||
}
|
||||
|
||||
export function getSmeltingFuel(bot) {
|
||||
let fuel = bot.inventory.items().find(i => i.name === 'coal' || i.name === 'charcoal' || i.name === 'blaze_rod')
|
||||
if (fuel)
|
||||
return fuel;
|
||||
fuel = bot.inventory.items().find(i => i.name.includes('log') || i.name.includes('planks'))
|
||||
if (fuel)
|
||||
return fuel;
|
||||
return bot.inventory.items().find(i => i.name === 'coal_block' || i.name === 'lava_bucket');
|
||||
}
|
||||
|
||||
export function getFuelSmeltOutput(fuelName) {
|
||||
if (fuelName === 'coal' || fuelName === 'charcoal')
|
||||
return 8;
|
||||
if (fuelName === 'blaze_rod')
|
||||
return 12;
|
||||
if (fuelName.includes('log') || fuelName.includes('planks'))
|
||||
return 1.5
|
||||
if (fuelName === 'coal_block')
|
||||
return 80;
|
||||
if (fuelName === 'lava_bucket')
|
||||
return 100;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function getItemSmeltingIngredient(itemName) {
|
||||
return {
|
||||
baked_potato: 'potato',
|
||||
steak: 'raw_beef',
|
||||
cooked_chicken: 'raw_chicken',
|
||||
cooked_cod: 'raw_cod',
|
||||
cooked_mutton: 'raw_mutton',
|
||||
cooked_porkchop: 'raw_porkchop',
|
||||
cooked_rabbit: 'raw_rabbit',
|
||||
cooked_salmon: 'raw_salmon',
|
||||
dried_kelp: 'kelp',
|
||||
iron_ingot: 'raw_iron',
|
||||
gold_ingot: 'raw_gold',
|
||||
copper_ingot: 'raw_copper',
|
||||
glass: 'sand'
|
||||
}[itemName];
|
||||
}
|
||||
|
||||
export function getItemBlockSources(itemName) {
|
||||
let itemId = getItemId(itemName);
|
||||
let sources = [];
|
||||
for (let block of getAllBlocks()) {
|
||||
if (block.drops.includes(itemId)) {
|
||||
sources.push(block.name);
|
||||
}
|
||||
}
|
||||
return sources;
|
||||
}
|
||||
|
||||
export function getItemAnimalSource(itemName) {
|
||||
return {
|
||||
raw_beef: 'cow',
|
||||
raw_chicken: 'chicken',
|
||||
raw_cod: 'cod',
|
||||
raw_mutton: 'sheep',
|
||||
raw_porkchop: 'pig',
|
||||
raw_rabbit: 'rabbit',
|
||||
raw_salmon: 'salmon',
|
||||
leather: 'cow',
|
||||
wool: 'sheep'
|
||||
}[itemName];
|
||||
}
|
||||
|
||||
export function getBlockTool(blockName) {
|
||||
let block = mcdata.blocksByName[blockName];
|
||||
if (!block || !block.harvestTools) {
|
||||
return null;
|
||||
}
|
||||
return getItemName(Object.keys(block.harvestTools)[0]); // Double check first tool is always simplest
|
||||
}
|
||||
|
||||
export function makeItem(name, amount=1) {
|
||||
return new Item(getItemId(name), amount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of ingredients required to use the recipe once.
|
||||
*
|
||||
* @param {Recipe} recipe
|
||||
* @returns {Object<mc.ItemName, number>} an object describing the number of each ingredient.
|
||||
*/
|
||||
export function ingredientsFromPrismarineRecipe(recipe) {
|
||||
let requiredIngedients = {};
|
||||
if (recipe.inShape)
|
||||
for (const ingredient of recipe.inShape.flat()) {
|
||||
if(ingredient.id<0) continue; //prismarine-recipe uses id -1 as an empty crafting slot
|
||||
const ingredientName = getItemName(ingredient.id);
|
||||
requiredIngedients[ingredientName] ??=0;
|
||||
requiredIngedients[ingredientName] += ingredient.count;
|
||||
}
|
||||
if (recipe.ingredients)
|
||||
for (const ingredient of recipe.ingredients) {
|
||||
if(ingredient.id<0) continue;
|
||||
const ingredientName = getItemName(ingredient.id);
|
||||
requiredIngedients[ingredientName] ??=0;
|
||||
requiredIngedients[ingredientName] -= ingredient.count;
|
||||
//Yes, the `-=` is intended.
|
||||
//prismarine-recipe uses positive numbers for the shaped ingredients but negative for unshaped.
|
||||
//Why this is the case is beyond my understanding.
|
||||
}
|
||||
return requiredIngedients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of times an action, such as a crafing recipe, can be completed before running out of resources.
|
||||
* @template T - doesn't have to be an item. This could be any resource.
|
||||
* @param {Object.<T, number>} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}`
|
||||
* @param {Object.<T, number>} requiredItems - The resources required to complete the action once; e.g, `{'cobble_stone': 3, 'stick': 2}`
|
||||
* @param {boolean} discrete - Is the action discrete?
|
||||
* @returns {{num: number, limitingResource: (T | null)}} the number of times the action can be completed and the limmiting resource; e.g `{num: 2, limitingResource: 'cobble_stone'}`
|
||||
*/
|
||||
export function calculateLimitingResource(availableItems, requiredItems, discrete=true) {
|
||||
let limitingResource = null;
|
||||
let num = Infinity;
|
||||
for (const itemType in requiredItems) {
|
||||
if (availableItems[itemType] < requiredItems[itemType] * num) {
|
||||
limitingResource = itemType;
|
||||
num = availableItems[itemType] / requiredItems[itemType];
|
||||
}
|
||||
}
|
||||
if(discrete) num = Math.floor(num);
|
||||
return {num, limitingResource}
|
||||
}
|
||||
|
||||
let loopingItems = new Set();
|
||||
|
||||
export function initializeLoopingItems() {
|
||||
|
||||
loopingItems = new Set(['coal',
|
||||
'wheat',
|
||||
'bone_meal',
|
||||
'diamond',
|
||||
'emerald',
|
||||
'raw_iron',
|
||||
'raw_gold',
|
||||
'redstone',
|
||||
'blue_wool',
|
||||
'packed_mud',
|
||||
'raw_copper',
|
||||
'iron_ingot',
|
||||
'dried_kelp',
|
||||
'gold_ingot',
|
||||
'slime_ball',
|
||||
'black_wool',
|
||||
'quartz_slab',
|
||||
'copper_ingot',
|
||||
'lapis_lazuli',
|
||||
'honey_bottle',
|
||||
'rib_armor_trim_smithing_template',
|
||||
'eye_armor_trim_smithing_template',
|
||||
'vex_armor_trim_smithing_template',
|
||||
'dune_armor_trim_smithing_template',
|
||||
'host_armor_trim_smithing_template',
|
||||
'tide_armor_trim_smithing_template',
|
||||
'wild_armor_trim_smithing_template',
|
||||
'ward_armor_trim_smithing_template',
|
||||
'coast_armor_trim_smithing_template',
|
||||
'spire_armor_trim_smithing_template',
|
||||
'snout_armor_trim_smithing_template',
|
||||
'shaper_armor_trim_smithing_template',
|
||||
'netherite_upgrade_smithing_template',
|
||||
'raiser_armor_trim_smithing_template',
|
||||
'sentry_armor_trim_smithing_template',
|
||||
'silence_armor_trim_smithing_template',
|
||||
'wayfinder_armor_trim_smithing_template']);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets a detailed plan for crafting an item considering current inventory
|
||||
*/
|
||||
export function getDetailedCraftingPlan(targetItem, count = 1, current_inventory = {}) {
|
||||
initializeLoopingItems();
|
||||
if (!targetItem || count <= 0 || !getItemId(targetItem)) {
|
||||
return "Invalid input. Please provide a valid item name and positive count.";
|
||||
}
|
||||
|
||||
if (isBaseItem(targetItem)) {
|
||||
const available = current_inventory[targetItem] || 0;
|
||||
if (available >= count) return "You have all required items already in your inventory!";
|
||||
return `${targetItem} is a base item, you need to find ${count - available} more in the world`;
|
||||
}
|
||||
|
||||
const inventory = { ...current_inventory };
|
||||
const leftovers = {};
|
||||
const plan = craftItem(targetItem, count, inventory, leftovers);
|
||||
return formatPlan(targetItem, plan);
|
||||
}
|
||||
|
||||
function isBaseItem(item) {
|
||||
return loopingItems.has(item) || getItemCraftingRecipes(item) === null;
|
||||
}
|
||||
|
||||
function craftItem(item, count, inventory, leftovers, crafted = { required: {}, steps: [], leftovers: {} }) {
|
||||
// Check available inventory and leftovers first
|
||||
const availableInv = inventory[item] || 0;
|
||||
const availableLeft = leftovers[item] || 0;
|
||||
const totalAvailable = availableInv + availableLeft;
|
||||
|
||||
if (totalAvailable >= count) {
|
||||
// Use leftovers first, then inventory
|
||||
const useFromLeft = Math.min(availableLeft, count);
|
||||
leftovers[item] = availableLeft - useFromLeft;
|
||||
|
||||
const remainingNeeded = count - useFromLeft;
|
||||
if (remainingNeeded > 0) {
|
||||
inventory[item] = availableInv - remainingNeeded;
|
||||
}
|
||||
return crafted;
|
||||
}
|
||||
|
||||
// Use whatever is available
|
||||
const stillNeeded = count - totalAvailable;
|
||||
if (availableLeft > 0) leftovers[item] = 0;
|
||||
if (availableInv > 0) inventory[item] = 0;
|
||||
|
||||
if (isBaseItem(item)) {
|
||||
crafted.required[item] = (crafted.required[item] || 0) + stillNeeded;
|
||||
return crafted;
|
||||
}
|
||||
|
||||
const recipe = getItemCraftingRecipes(item)?.[0];
|
||||
if (!recipe) {
|
||||
crafted.required[item] = stillNeeded;
|
||||
return crafted;
|
||||
}
|
||||
|
||||
const [ingredients, result] = recipe;
|
||||
const craftedPerRecipe = result.craftedCount;
|
||||
const batchCount = Math.ceil(stillNeeded / craftedPerRecipe);
|
||||
const totalProduced = batchCount * craftedPerRecipe;
|
||||
|
||||
// Add excess to leftovers
|
||||
if (totalProduced > stillNeeded) {
|
||||
leftovers[item] = (leftovers[item] || 0) + (totalProduced - stillNeeded);
|
||||
}
|
||||
|
||||
// Process each ingredient
|
||||
for (const [ingredientName, ingredientCount] of Object.entries(ingredients)) {
|
||||
const totalIngredientNeeded = ingredientCount * batchCount;
|
||||
craftItem(ingredientName, totalIngredientNeeded, inventory, leftovers, crafted);
|
||||
}
|
||||
|
||||
// Add crafting step
|
||||
const stepIngredients = Object.entries(ingredients)
|
||||
.map(([name, amount]) => `${amount * batchCount} ${name}`)
|
||||
.join(' + ');
|
||||
crafted.steps.push(`Craft ${stepIngredients} -> ${totalProduced} ${item}`);
|
||||
|
||||
return crafted;
|
||||
}
|
||||
|
||||
function formatPlan(targetItem, { required, steps, leftovers }) {
|
||||
const lines = [];
|
||||
|
||||
if (Object.keys(required).length > 0) {
|
||||
lines.push('You are missing the following items:');
|
||||
Object.entries(required).forEach(([item, count]) =>
|
||||
lines.push(`- ${count} ${item}`));
|
||||
lines.push('\nOnce you have these items, here\'s your crafting plan:');
|
||||
} else {
|
||||
lines.push('You have all items required to craft this item!');
|
||||
lines.push('Here\'s your crafting plan:');
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push(...steps);
|
||||
|
||||
if (Object.keys(required).some(item => item.includes('oak')) && !targetItem.includes('oak')) {
|
||||
lines.push('Note: Any varient of wood can be used for this recipe.');
|
||||
}
|
||||
|
||||
if (Object.keys(leftovers).length > 0) {
|
||||
lines.push('\nYou will have leftover:');
|
||||
Object.entries(leftovers).forEach(([item, count]) =>
|
||||
lines.push(`- ${count} ${item}`));
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Minimal settings stub adapted from Mindcraft for the pepa-pi-bot vendored
|
||||
// world.js / skills.js / mcdata.js library. Mindcraft expects a settings module
|
||||
// with these defaults; we use sane farmer-bot values.
|
||||
//
|
||||
// Source: github.com/kolbytn/mindcraft (MIT)
|
||||
export default {
|
||||
block_place_delay: 0,
|
||||
minecraft_version: "auto",
|
||||
allow_insecure_coding: false,
|
||||
log_all_prompts: false,
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,431 @@
|
||||
import pf from 'mineflayer-pathfinder';
|
||||
import * as mc from './mcdata.js';
|
||||
|
||||
|
||||
export function getNearestFreeSpace(bot, size=1, distance=8) {
|
||||
/**
|
||||
* Get the nearest empty space with solid blocks beneath it of the given size.
|
||||
* @param {Bot} bot - The bot to get the nearest free space for.
|
||||
* @param {number} size - The (size x size) of the space to find, default 1.
|
||||
* @param {number} distance - The maximum distance to search, default 8.
|
||||
* @returns {Vec3} - The south west corner position of the nearest free space.
|
||||
* @example
|
||||
* let position = world.getNearestFreeSpace(bot, 1, 8);
|
||||
**/
|
||||
let empty_pos = bot.findBlocks({
|
||||
matching: (block) => {
|
||||
return block && block.name == 'air';
|
||||
},
|
||||
maxDistance: distance,
|
||||
count: 1000
|
||||
});
|
||||
for (let i = 0; i < empty_pos.length; i++) {
|
||||
let empty = true;
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let z = 0; z < size; z++) {
|
||||
let top = bot.blockAt(empty_pos[i].offset(x, 0, z));
|
||||
let bottom = bot.blockAt(empty_pos[i].offset(x, -1, z));
|
||||
if (!top || !top.name == 'air' || !bottom || bottom.drops.length == 0 || !bottom.diggable) {
|
||||
empty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!empty) break;
|
||||
}
|
||||
if (empty) {
|
||||
return empty_pos[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function getBlockAtPosition(bot, x=0, y=0, z=0) {
|
||||
/**
|
||||
* Get a block from the bot's relative position
|
||||
* @param {Bot} bot - The bot to get the block for.
|
||||
* @param {number} x - The relative x offset to serach, default 0.
|
||||
* @param {number} y - The relative y offset to serach, default 0.
|
||||
* @param {number} y - The relative z offset to serach, default 0.
|
||||
* @returns {Block} - The nearest block.
|
||||
* @example
|
||||
* let blockBelow = world.getBlockAtPosition(bot, 0, -1, 0);
|
||||
* let blockAbove = world.getBlockAtPosition(bot, 0, 2, 0); since minecraft position is at the feet
|
||||
**/
|
||||
let block = bot.blockAt(bot.entity.position.offset(x, y, z));
|
||||
if (!block) block = {name: 'air'};
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
|
||||
export function getSurroundingBlocks(bot) {
|
||||
/**
|
||||
* Get the surrounding blocks from the bot's environment.
|
||||
* @param {Bot} bot - The bot to get the block for.
|
||||
* @returns {string[]} - A list of block results as strings.
|
||||
* @example
|
||||
**/
|
||||
// Create a list of block position results that can be unpacked.
|
||||
let res = [];
|
||||
res.push(`Block Below: ${getBlockAtPosition(bot, 0, -1, 0).name}`);
|
||||
res.push(`Block at Legs: ${getBlockAtPosition(bot, 0, 0, 0).name}`);
|
||||
res.push(`Block at Head: ${getBlockAtPosition(bot, 0, 1, 0).name}`);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
export function getFirstBlockAboveHead(bot, ignore_types=null, distance=32) {
|
||||
/**
|
||||
* Searches a column from the bot's position for the first solid block above its head
|
||||
* @param {Bot} bot - The bot to get the block for.
|
||||
* @param {string[]} ignore_types - The names of the blocks to ignore.
|
||||
* @param {number} distance - The maximum distance to search, default 32.
|
||||
* @returns {string} - The fist block above head.
|
||||
* @example
|
||||
* let firstBlockAboveHead = world.getFirstBlockAboveHead(bot, null, 32);
|
||||
**/
|
||||
// if ignore_types is not a list, make it a list.
|
||||
let ignore_blocks = [];
|
||||
if (ignore_types === null) ignore_blocks = ['air', 'cave_air'];
|
||||
else {
|
||||
if (!Array.isArray(ignore_types))
|
||||
ignore_types = [ignore_types];
|
||||
for(let ignore_type of ignore_types) {
|
||||
if (mc.getBlockId(ignore_type)) ignore_blocks.push(ignore_type);
|
||||
}
|
||||
}
|
||||
// The block above, stops when it finds a solid block .
|
||||
let block_above = {name: 'air'};
|
||||
let height = 0
|
||||
for (let i = 0; i < distance; i++) {
|
||||
let block = bot.blockAt(bot.entity.position.offset(0, i+2, 0));
|
||||
if (!block) block = {name: 'air'};
|
||||
// Ignore and continue
|
||||
if (ignore_blocks.includes(block.name)) continue;
|
||||
// Defaults to any block
|
||||
block_above = block;
|
||||
height = i;
|
||||
break;
|
||||
}
|
||||
|
||||
if (ignore_blocks.includes(block_above.name)) return 'none';
|
||||
|
||||
return `${block_above.name} (${height} blocks up)`;
|
||||
}
|
||||
|
||||
|
||||
export function getNearestBlocks(bot, block_types=null, distance=8, count=10000) {
|
||||
/**
|
||||
* Get a list of the nearest blocks of the given types.
|
||||
* @param {Bot} bot - The bot to get the nearest block for.
|
||||
* @param {string[]} block_types - The names of the blocks to search for.
|
||||
* @param {number} distance - The maximum distance to search, default 16.
|
||||
* @param {number} count - The maximum number of blocks to find, default 10000.
|
||||
* @returns {Block[]} - The nearest blocks of the given type.
|
||||
* @example
|
||||
* let woodBlocks = world.getNearestBlocks(bot, ['oak_log', 'birch_log'], 16, 1);
|
||||
**/
|
||||
// if blocktypes is not a list, make it a list
|
||||
let block_ids = [];
|
||||
if (block_types === null) {
|
||||
block_ids = mc.getAllBlockIds(['air']);
|
||||
}
|
||||
else {
|
||||
if (!Array.isArray(block_types))
|
||||
block_types = [block_types];
|
||||
for(let block_type of block_types) {
|
||||
block_ids.push(mc.getBlockId(block_type));
|
||||
}
|
||||
}
|
||||
return getNearestBlocksWhere(bot, block_ids, distance, count);
|
||||
}
|
||||
|
||||
export function getNearestBlocksWhere(bot, predicate, distance=8, count=10000) {
|
||||
/**
|
||||
* Get a list of the nearest blocks that satisfy the given predicate.
|
||||
* @param {Bot} bot - The bot to get the nearest blocks for.
|
||||
* @param {function} predicate - The predicate to filter the blocks.
|
||||
* @param {number} distance - The maximum distance to search, default 16.
|
||||
* @param {number} count - The maximum number of blocks to find, default 10000.
|
||||
* @returns {Block[]} - The nearest blocks that satisfy the given predicate.
|
||||
* @example
|
||||
* let waterBlocks = world.getNearestBlocksWhere(bot, block => block.name === 'water', 16, 10);
|
||||
**/
|
||||
let positions = bot.findBlocks({matching: predicate, maxDistance: distance, count: count});
|
||||
let blocks = positions.map(position => bot.blockAt(position));
|
||||
return blocks;
|
||||
}
|
||||
|
||||
|
||||
export function getNearestBlock(bot, block_type, distance=16) {
|
||||
/**
|
||||
* Get the nearest block of the given type.
|
||||
* @param {Bot} bot - The bot to get the nearest block for.
|
||||
* @param {string} block_type - The name of the block to search for.
|
||||
* @param {number} distance - The maximum distance to search, default 16.
|
||||
* @returns {Block} - The nearest block of the given type.
|
||||
* @example
|
||||
* let coalBlock = world.getNearestBlock(bot, 'coal_ore', 16);
|
||||
**/
|
||||
let blocks = getNearestBlocks(bot, block_type, distance, 1);
|
||||
if (blocks.length > 0) {
|
||||
return blocks[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
export function getNearbyEntities(bot, maxDistance=16) {
|
||||
let entities = [];
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
const distance = entity.position.distanceTo(bot.entity.position);
|
||||
if (distance > maxDistance) continue;
|
||||
entities.push({ entity: entity, distance: distance });
|
||||
}
|
||||
entities.sort((a, b) => a.distance - b.distance);
|
||||
let res = [];
|
||||
for (let i = 0; i < entities.length; i++) {
|
||||
res.push(entities[i].entity);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function getNearestEntityWhere(bot, predicate, maxDistance=16) {
|
||||
return bot.nearestEntity(entity => predicate(entity) && bot.entity.position.distanceTo(entity.position) < maxDistance);
|
||||
}
|
||||
|
||||
|
||||
export function getNearbyPlayers(bot, maxDistance) {
|
||||
if (maxDistance == null) maxDistance = 16;
|
||||
let players = [];
|
||||
for (const entity of Object.values(bot.entities)) {
|
||||
const distance = entity.position.distanceTo(bot.entity.position);
|
||||
if (distance > maxDistance) continue;
|
||||
if (entity.type == 'player' && entity.username != bot.username) {
|
||||
players.push({ entity: entity, distance: distance });
|
||||
}
|
||||
}
|
||||
players.sort((a, b) => a.distance - b.distance);
|
||||
let res = [];
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
res.push(players[i].entity);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// Helper function to get villager profession from metadata
|
||||
export function getVillagerProfession(entity) {
|
||||
// Villager profession mapping based on metadata
|
||||
const professions = {
|
||||
0: 'Unemployed',
|
||||
1: 'Armorer',
|
||||
2: 'Butcher',
|
||||
3: 'Cartographer',
|
||||
4: 'Cleric',
|
||||
5: 'Farmer',
|
||||
6: 'Fisherman',
|
||||
7: 'Fletcher',
|
||||
8: 'Leatherworker',
|
||||
9: 'Librarian',
|
||||
10: 'Mason',
|
||||
11: 'Nitwit',
|
||||
12: 'Shepherd',
|
||||
13: 'Toolsmith',
|
||||
14: 'Weaponsmith'
|
||||
};
|
||||
|
||||
if (entity.metadata && entity.metadata[18]) {
|
||||
// Check if metadata[18] is an object with villagerProfession property
|
||||
if (typeof entity.metadata[18] === 'object' && entity.metadata[18].villagerProfession !== undefined) {
|
||||
const professionId = entity.metadata[18].villagerProfession;
|
||||
const level = entity.metadata[18].level || 1;
|
||||
const professionName = professions[professionId] || 'Unknown';
|
||||
return `${professionName} L${level}`;
|
||||
}
|
||||
// Fallback for direct profession ID
|
||||
else if (typeof entity.metadata[18] === 'number') {
|
||||
const professionId = entity.metadata[18];
|
||||
return professions[professionId] || 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
// If we can't determine profession but it's an adult villager
|
||||
if (entity.metadata && entity.metadata[16] !== 1) { // Not a baby
|
||||
return 'Adult';
|
||||
}
|
||||
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
|
||||
export function getInventoryCounts(bot) {
|
||||
/**
|
||||
* Get an object representing the bot's inventory.
|
||||
* @param {Bot} bot - The bot to get the inventory for.
|
||||
* @returns {object} - An object with item names as keys and counts as values.
|
||||
* @example
|
||||
* let inventory = world.getInventoryCounts(bot);
|
||||
* let oakLogCount = inventory['oak_log'];
|
||||
* let hasWoodenPickaxe = inventory['wooden_pickaxe'] > 0;
|
||||
**/
|
||||
let inventory = {};
|
||||
for (const slot of bot.inventory.slots) {
|
||||
if (slot != null && slot.name) {
|
||||
if (inventory[slot.name] == null) {
|
||||
inventory[slot.name] = 0;
|
||||
}
|
||||
inventory[slot.name] += slot.count;
|
||||
}
|
||||
}
|
||||
return inventory;
|
||||
}
|
||||
|
||||
|
||||
export function getCraftableItems(bot) {
|
||||
/**
|
||||
* Get a list of all items that can be crafted with the bot's current inventory.
|
||||
* @param {Bot} bot - The bot to get the craftable items for.
|
||||
* @returns {string[]} - A list of all items that can be crafted.
|
||||
* @example
|
||||
* let craftableItems = world.getCraftableItems(bot);
|
||||
**/
|
||||
let table = getNearestBlock(bot, 'crafting_table');
|
||||
if (!table) {
|
||||
for (const item of bot.inventory.items()) {
|
||||
if (item != null && item.name === 'crafting_table') {
|
||||
table = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let res = [];
|
||||
for (const item of mc.getAllItems()) {
|
||||
let recipes = bot.recipesFor(item.id, null, 1, table);
|
||||
if (recipes.length > 0)
|
||||
res.push(item.name);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
export function getPosition(bot) {
|
||||
/**
|
||||
* Get your position in the world (Note that y is vertical).
|
||||
* @param {Bot} bot - The bot to get the position for.
|
||||
* @returns {Vec3} - An object with x, y, and x attributes representing the position of the bot.
|
||||
* @example
|
||||
* let position = world.getPosition(bot);
|
||||
* let x = position.x;
|
||||
**/
|
||||
return bot.entity.position;
|
||||
}
|
||||
|
||||
|
||||
export function getNearbyEntityTypes(bot) {
|
||||
/**
|
||||
* Get a list of all nearby mob types.
|
||||
* @param {Bot} bot - The bot to get nearby mobs for.
|
||||
* @returns {string[]} - A list of all nearby mobs.
|
||||
* @example
|
||||
* let mobs = world.getNearbyEntityTypes(bot);
|
||||
**/
|
||||
let mobs = getNearbyEntities(bot, 16);
|
||||
let found = [];
|
||||
for (let i = 0; i < mobs.length; i++) {
|
||||
if (!found.includes(mobs[i].name)) {
|
||||
found.push(mobs[i].name);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export function isEntityType(name) {
|
||||
/**
|
||||
* Check if a given name is a valid entity type.
|
||||
* @param {string} name - The name of the entity type to check.
|
||||
* @returns {boolean} - True if the name is a valid entity type, false otherwise.
|
||||
*/
|
||||
return mc.getEntityId(name) !== null;
|
||||
}
|
||||
|
||||
export function getNearbyPlayerNames(bot) {
|
||||
/**
|
||||
* Get a list of all nearby player names.
|
||||
* @param {Bot} bot - The bot to get nearby players for.
|
||||
* @returns {string[]} - A list of all nearby players.
|
||||
* @example
|
||||
* let players = world.getNearbyPlayerNames(bot);
|
||||
**/
|
||||
let players = getNearbyPlayers(bot, 64);
|
||||
let found = [];
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
if (!found.includes(players[i].username) && players[i].username != bot.username) {
|
||||
found.push(players[i].username);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
|
||||
export function getNearbyBlockTypes(bot, distance=16) {
|
||||
/**
|
||||
* Get a list of all nearby block names.
|
||||
* @param {Bot} bot - The bot to get nearby blocks for.
|
||||
* @param {number} distance - The maximum distance to search, default 16.
|
||||
* @returns {string[]} - A list of all nearby blocks.
|
||||
* @example
|
||||
* let blocks = world.getNearbyBlockTypes(bot);
|
||||
**/
|
||||
let blocks = getNearestBlocks(bot, null, distance);
|
||||
let found = [];
|
||||
for (let i = 0; i < blocks.length; i++) {
|
||||
if (!found.includes(blocks[i].name)) {
|
||||
found.push(blocks[i].name);
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
export async function isClearPath(bot, target) {
|
||||
/**
|
||||
* Check if there is a path to the target that requires no digging or placing blocks.
|
||||
* @param {Bot} bot - The bot to get the path for.
|
||||
* @param {Entity} target - The target to path to.
|
||||
* @returns {boolean} - True if there is a clear path, false otherwise.
|
||||
*/
|
||||
let movements = new pf.Movements(bot)
|
||||
movements.canDig = false;
|
||||
movements.canPlaceOn = false;
|
||||
movements.canOpenDoors = false;
|
||||
let goal = new pf.goals.GoalNear(target.position.x, target.position.y, target.position.z, 1);
|
||||
let path = await bot.pathfinder.getPathTo(movements, goal, 100);
|
||||
return path.status === 'success';
|
||||
}
|
||||
|
||||
export function shouldPlaceTorch(bot) {
|
||||
if (!bot.modes.isOn('torch_placing') || bot.interrupt_code) return false;
|
||||
const pos = getPosition(bot);
|
||||
// TODO: check light level instead of nearby torches, block.light is broken
|
||||
let nearest_torch = getNearestBlock(bot, 'torch', 6);
|
||||
if (!nearest_torch)
|
||||
nearest_torch = getNearestBlock(bot, 'wall_torch', 6);
|
||||
if (!nearest_torch) {
|
||||
const block = bot.blockAt(pos);
|
||||
let has_torch = bot.inventory.findInventoryItem('torch');
|
||||
return has_torch && block?.name === 'air';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getBiomeName(bot) {
|
||||
/**
|
||||
* Get the name of the biome the bot is in.
|
||||
* @param {Bot} bot - The bot to get the biome for.
|
||||
* @returns {string} - The name of the biome.
|
||||
* @example
|
||||
* let biome = world.getBiomeName(bot);
|
||||
**/
|
||||
const biomeId = bot.world.getBiome(bot.entity.position);
|
||||
return mc.getAllBiomes()[biomeId].name;
|
||||
}
|
||||
@@ -0,0 +1,406 @@
|
||||
// Mindcraft-backed Pi tools for pepa-pi-bot.
|
||||
//
|
||||
// Wraps battle-tested skill primitives from Mindcraft (MIT) — see
|
||||
// extensions/lib/world.js, extensions/lib/skills.js, extensions/lib/mcdata.js,
|
||||
// and extensions/lib/LICENSE-MINDCRAFT.
|
||||
//
|
||||
// This extension complements mineflayer-bridge.ts (auth, memory, chat,
|
||||
// escalation, trust). It does NOT replace it; both extensions run side-by-side,
|
||||
// registering disjoint Pi tools. Old broken mc_goto / mc_build_pyramid_5x5
|
||||
// in bridge.ts are deprecated in favour of the perception+action set below.
|
||||
|
||||
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||
|
||||
// Lazy access to the Mineflayer bot instance held by mineflayer-bridge.ts.
|
||||
function getBot(): any {
|
||||
const bot = (globalThis as any).__pepaPiBot;
|
||||
if (!bot) throw new Error("Mineflayer bot is not connected (or bridge has not exposed __pepaPiBot yet).");
|
||||
return bot;
|
||||
}
|
||||
|
||||
// ---- parameter schemas ----------------------------------------------------
|
||||
|
||||
const EMPTY_PARAMS = {
|
||||
type: "object",
|
||||
properties: {},
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const SCAN_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
radius: { type: "number", description: "Block radius (default 16, max 48)." },
|
||||
},
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const BLOCK_NAME_COUNT_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
blockType: { type: "string", description: "Minecraft block name, e.g. 'oak_log', 'cobblestone'." },
|
||||
count: { type: "number", description: "How many to collect. Default 1." },
|
||||
},
|
||||
required: ["blockType"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const PLACE_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
blockType: { type: "string", description: "Block name in inventory to place." },
|
||||
x: { type: "number" },
|
||||
y: { type: "number" },
|
||||
z: { type: "number" },
|
||||
},
|
||||
required: ["blockType", "x", "y", "z"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const GOTO_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { type: "number" },
|
||||
y: { type: "number" },
|
||||
z: { type: "number" },
|
||||
minDistance: { type: "number", description: "Stop when within this distance (default 2)." },
|
||||
},
|
||||
required: ["x", "y", "z"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const GOTO_BLOCK_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
blockType: { type: "string" },
|
||||
minDistance: { type: "number" },
|
||||
range: { type: "number", description: "Search radius. Default 64." },
|
||||
},
|
||||
required: ["blockType"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const CONSUME_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
itemName: { type: "string", description: "Food name. Empty = any food in inventory." },
|
||||
},
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const CRAFT_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
itemName: { type: "string" },
|
||||
num: { type: "number" },
|
||||
},
|
||||
required: ["itemName"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const EQUIP_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
itemName: { type: "string" },
|
||||
},
|
||||
required: ["itemName"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const DEFEND_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
range: { type: "number", description: "Reaction range. Default 9." },
|
||||
},
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const AVOID_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
distance: { type: "number", description: "Move this many blocks away. Default 16." },
|
||||
},
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const STAY_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
seconds: { type: "number" },
|
||||
},
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
// ---- helpers --------------------------------------------------------------
|
||||
|
||||
function clampRadius(r: number | undefined): number {
|
||||
const v = typeof r === "number" && isFinite(r) ? r : 16;
|
||||
return Math.max(1, Math.min(48, Math.floor(v)));
|
||||
}
|
||||
|
||||
function textResult(text: string, details?: Record<string, unknown>) {
|
||||
return { content: [{ type: "text", text }], details };
|
||||
}
|
||||
|
||||
// ---- tool registration ----------------------------------------------------
|
||||
|
||||
export default async function mindcraftSkills(pi: ExtensionAPI) {
|
||||
// Load vendored ESM Mindcraft libs at extension init. Pi awaits the default
|
||||
// export, so registerTool calls below see fully-loaded modules.
|
||||
const [skillsMod, worldMod] = await Promise.all([
|
||||
import("./lib/skills.js" as any),
|
||||
import("./lib/world.js" as any),
|
||||
]);
|
||||
const skills = skillsMod as Record<string, (...args: any[]) => any>;
|
||||
const world = worldMod as Record<string, (...args: any[]) => any>;
|
||||
|
||||
const safeCall = async <T>(label: string, fn: () => T | Promise<T>): Promise<T> => {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (e: unknown) {
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
throw new Error(`${label}: ${msg}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 1. mc_observe — one-shot perception snapshot.
|
||||
pi.registerTool({
|
||||
name: "mc_observe",
|
||||
label: "Observe Surroundings",
|
||||
description: "Return a compact JSON summary of nearby blocks, entities, inventory, position, health, food, weather, time of day. Always call before deciding a non-trivial action in autonomous mode.",
|
||||
promptSnippet: "Get a single JSON snapshot of everything around the bot.",
|
||||
parameters: SCAN_PARAMS,
|
||||
async execute(_id, params: { radius?: number }) {
|
||||
const bot = getBot();
|
||||
const radius = clampRadius(params?.radius);
|
||||
const blockTypes = world.getNearbyBlockTypes(bot, radius);
|
||||
const entities = world.getNearbyEntities(bot, radius);
|
||||
const players = world.getNearbyPlayerNames ? world.getNearbyPlayerNames(bot) : [];
|
||||
const inv = world.getInventoryCounts(bot);
|
||||
const pos = world.getPosition(bot);
|
||||
const biome = (() => {
|
||||
try { return world.getBiomeName(bot); } catch { return "unknown"; }
|
||||
})();
|
||||
const summary = {
|
||||
position: { x: Math.round(pos.x), y: Math.round(pos.y), z: Math.round(pos.z) },
|
||||
biome,
|
||||
time: bot.time?.timeOfDay ?? null,
|
||||
isDay: bot.time?.isDay ?? null,
|
||||
weather: { rain: !!bot.isRaining, thunder: !!bot.thunderState },
|
||||
health: bot.health,
|
||||
food: bot.food,
|
||||
saturation: bot.foodSaturation,
|
||||
inventory: inv,
|
||||
nearbyBlocks: blockTypes,
|
||||
nearbyEntityTypes: Array.from(new Set(entities.map((e: any) => e?.name ?? e?.type).filter(Boolean))),
|
||||
nearbyPlayers: players,
|
||||
entityCount: entities.length,
|
||||
};
|
||||
return textResult(JSON.stringify(summary, null, 2), summary);
|
||||
},
|
||||
});
|
||||
|
||||
// 2. mc_inventory
|
||||
pi.registerTool({
|
||||
name: "mc_inventory",
|
||||
label: "Inventory",
|
||||
description: "List items in the bot's inventory as {name: count}.",
|
||||
parameters: EMPTY_PARAMS,
|
||||
async execute() {
|
||||
const bot = getBot();
|
||||
const inv = world.getInventoryCounts(bot);
|
||||
return textResult(JSON.stringify(inv), { inventory: inv });
|
||||
},
|
||||
});
|
||||
|
||||
// 3. mc_nearby_blocks
|
||||
pi.registerTool({
|
||||
name: "mc_nearby_blocks",
|
||||
label: "Nearby Block Types",
|
||||
description: "List distinct nearby block types within radius (default 16). Useful for 'is there water/lava/log close?'.",
|
||||
parameters: SCAN_PARAMS,
|
||||
async execute(_id, params: { radius?: number }) {
|
||||
const bot = getBot();
|
||||
const types = world.getNearbyBlockTypes(bot, clampRadius(params?.radius));
|
||||
return textResult(JSON.stringify(types), { blockTypes: types });
|
||||
},
|
||||
});
|
||||
|
||||
// 4. mc_nearby_entities
|
||||
pi.registerTool({
|
||||
name: "mc_nearby_entities",
|
||||
label: "Nearby Entities",
|
||||
description: "List entities (players, mobs, items, etc.) within radius (default 16). Returns name/type and approximate distance.",
|
||||
parameters: SCAN_PARAMS,
|
||||
async execute(_id, params: { radius?: number }) {
|
||||
const bot = getBot();
|
||||
const radius = clampRadius(params?.radius);
|
||||
const entities = world.getNearbyEntities(bot, radius);
|
||||
const me = bot.entity?.position;
|
||||
const out = entities.map((e: any) => ({
|
||||
type: e.name ?? e.type ?? "unknown",
|
||||
distance: me && e.position ? Math.round(me.distanceTo(e.position)) : null,
|
||||
username: e.username ?? null,
|
||||
position: e.position ? { x: Math.round(e.position.x), y: Math.round(e.position.y), z: Math.round(e.position.z) } : null,
|
||||
}));
|
||||
return textResult(JSON.stringify(out), { entities: out });
|
||||
},
|
||||
});
|
||||
|
||||
// 5. mc_collect_block
|
||||
pi.registerTool({
|
||||
name: "mc_collect_block",
|
||||
label: "Collect Block",
|
||||
description: "Move to and break N blocks of a given type, picking them up. Handles path, tool equip, and dig. Throws if the block can't be found in range.",
|
||||
parameters: BLOCK_NAME_COUNT_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { blockType: string; count?: number }) {
|
||||
const bot = getBot();
|
||||
const count = Math.max(1, Math.min(64, Math.floor(params.count ?? 1)));
|
||||
const ok = await safeCall("collectBlock", () => skills.collectBlock(bot, params.blockType, count));
|
||||
return textResult(ok ? `Collected ${count} of ${params.blockType}.` : `collectBlock returned false for ${params.blockType}.`, { ok, blockType: params.blockType, count });
|
||||
},
|
||||
});
|
||||
|
||||
// 6. mc_place_block
|
||||
pi.registerTool({
|
||||
name: "mc_place_block",
|
||||
label: "Place Block",
|
||||
description: "Place one block from inventory at the given coordinates. Throws if you do not have the block or the target is not placeable.",
|
||||
parameters: PLACE_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { blockType: string; x: number; y: number; z: number }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("placeBlock", () => skills.placeBlock(bot, params.blockType, params.x, params.y, params.z));
|
||||
return textResult(ok ? `Placed ${params.blockType} at ${params.x},${params.y},${params.z}.` : `placeBlock returned false.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 7. mc_go_to
|
||||
pi.registerTool({
|
||||
name: "mc_go_to",
|
||||
label: "Go To Coords",
|
||||
description: "Walk/swim/path to the given coordinates. Will dig through soft obstacles (leaves) and jump as needed. Stop when within minDistance (default 2). Throws on noPath.",
|
||||
parameters: GOTO_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { x: number; y: number; z: number; minDistance?: number }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("goToPosition", () => skills.goToPosition(bot, params.x, params.y, params.z, params.minDistance ?? 2));
|
||||
return textResult(ok ? `Arrived near ${params.x},${params.y},${params.z}.` : `goToPosition returned false.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 8. mc_go_to_block
|
||||
pi.registerTool({
|
||||
name: "mc_go_to_block",
|
||||
label: "Go To Nearest Block",
|
||||
description: "Find the nearest block of a given type within `range` (default 64) and walk to it.",
|
||||
parameters: GOTO_BLOCK_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { blockType: string; minDistance?: number; range?: number }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("goToNearestBlock", () =>
|
||||
skills.goToNearestBlock(bot, params.blockType, params.minDistance ?? 2, params.range ?? 64),
|
||||
);
|
||||
return textResult(ok ? `Arrived near nearest ${params.blockType}.` : `goToNearestBlock returned false for ${params.blockType}.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 9. mc_craft
|
||||
pi.registerTool({
|
||||
name: "mc_craft",
|
||||
label: "Craft",
|
||||
description: "Craft an item by name from inventory. Will use a nearby crafting table when required. Throws if recipe unknown or resources missing.",
|
||||
parameters: CRAFT_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { itemName: string; num?: number }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("craftRecipe", () => skills.craftRecipe(bot, params.itemName, Math.max(1, Math.floor(params.num ?? 1))));
|
||||
return textResult(ok ? `Crafted ${params.itemName}.` : `craftRecipe returned false.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 10. mc_equip
|
||||
pi.registerTool({
|
||||
name: "mc_equip",
|
||||
label: "Equip Item",
|
||||
description: "Equip an item by name (tool, armor, food).",
|
||||
parameters: EQUIP_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { itemName: string }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("equip", () => skills.equip(bot, params.itemName));
|
||||
return textResult(ok ? `Equipped ${params.itemName}.` : `equip returned false for ${params.itemName}.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 11. mc_consume
|
||||
pi.registerTool({
|
||||
name: "mc_consume",
|
||||
label: "Consume Food",
|
||||
description: "Eat food from inventory. If itemName empty, picks first food item.",
|
||||
parameters: CONSUME_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { itemName?: string }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("consume", () => skills.consume(bot, params.itemName ?? ""));
|
||||
return textResult(ok ? `Ate ${params.itemName ?? "food"}.` : `consume returned false.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 12. mc_defend_self
|
||||
pi.registerTool({
|
||||
name: "mc_defend_self",
|
||||
label: "Defend Self",
|
||||
description: "Attack hostile mobs within range until clear. Uses best available weapon. Range default 9.",
|
||||
parameters: DEFEND_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { range?: number }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("defendSelf", () => skills.defendSelf(bot, params.range ?? 9));
|
||||
return textResult(ok ? `Defended against hostiles.` : `defendSelf returned false.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 13. mc_avoid_enemies
|
||||
pi.registerTool({
|
||||
name: "mc_avoid_enemies",
|
||||
label: "Avoid Enemies",
|
||||
description: "Move away from the nearest hostile entities by approx N blocks. Default 16.",
|
||||
parameters: AVOID_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { distance?: number }) {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("avoidEnemies", () => skills.avoidEnemies(bot, params.distance ?? 16));
|
||||
return textResult(ok ? `Avoided enemies.` : `avoidEnemies returned false.`, { ok, ...params });
|
||||
},
|
||||
});
|
||||
|
||||
// 14. mc_stay
|
||||
pi.registerTool({
|
||||
name: "mc_stay",
|
||||
label: "Stay In Place",
|
||||
description: "Stand still for N seconds (default 30). Useful for waiting out night, regen, or letting world state change.",
|
||||
parameters: STAY_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_id, params: { seconds?: number }) {
|
||||
const bot = getBot();
|
||||
const secs = Math.max(1, Math.min(600, Math.floor(params.seconds ?? 30)));
|
||||
await safeCall("stay", () => skills.stay(bot, secs));
|
||||
return textResult(`Stood still for ${secs}s.`, { seconds: secs });
|
||||
},
|
||||
});
|
||||
|
||||
// 15. mc_pickup_nearby
|
||||
pi.registerTool({
|
||||
name: "mc_pickup_nearby",
|
||||
label: "Pickup Nearby Items",
|
||||
description: "Walk over and pick up any dropped item entities within ~8 blocks.",
|
||||
parameters: EMPTY_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute() {
|
||||
const bot = getBot();
|
||||
const ok = await safeCall("pickupNearbyItems", () => skills.pickupNearbyItems(bot));
|
||||
return textResult(ok ? `Picked up nearby items.` : `pickupNearbyItems returned false.`);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -8,6 +8,15 @@ import type { Bot } from "mineflayer";
|
||||
const require = createRequire(import.meta.url);
|
||||
const dotenv = require("dotenv") as typeof import("dotenv");
|
||||
const mineflayer = require("mineflayer") as typeof import("mineflayer");
|
||||
|
||||
// mcdata is an ES module; load it lazily via dynamic import on first use to
|
||||
// avoid the "Cannot require() ES Module ... not yet fully loaded" race when
|
||||
// Pi loads multiple extensions in parallel.
|
||||
let mcdataPromise: Promise<{ attachPluginsAndInit: (bot: any) => any }> | null = null;
|
||||
function loadMcdata() {
|
||||
if (!mcdataPromise) mcdataPromise = import("./lib/mcdata.js" as any) as Promise<{ attachPluginsAndInit: (bot: any) => any }>;
|
||||
return mcdataPromise;
|
||||
}
|
||||
const pathfinderModule = require("mineflayer-pathfinder") as {
|
||||
pathfinder: (bot: Bot) => void;
|
||||
Movements: new (bot: Bot) => any;
|
||||
@@ -90,6 +99,12 @@ interface BuildPyramidInput {
|
||||
dry_run?: boolean;
|
||||
}
|
||||
|
||||
interface DigInput {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
|
||||
type MemoryAction = "set_current_task" | "clear_current_task" | "append_diary" | "register_location";
|
||||
|
||||
interface MemoryInput {
|
||||
@@ -228,6 +243,17 @@ const BUILD_PYRAMID_PARAMS = {
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const DIG_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
x: { type: "number", description: "Block X coordinate to dig." },
|
||||
y: { type: "number", description: "Block Y coordinate to dig." },
|
||||
z: { type: "number", description: "Block Z coordinate to dig." },
|
||||
},
|
||||
required: ["x", "y", "z"],
|
||||
additionalProperties: false,
|
||||
} as const;
|
||||
|
||||
const MEMORY_PARAMS = {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -1081,6 +1107,13 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
}
|
||||
|
||||
bot = nextBot;
|
||||
(globalThis as any).__pepaPiBot = nextBot;
|
||||
// Attach Mindcraft-required plugins (pathfinder, pvp, collectblock,
|
||||
// armorManager) and prime mc_version once login completes. Skill calls
|
||||
// from mindcraft-skills.ts rely on this. Loaded lazily as ESM.
|
||||
loadMcdata()
|
||||
.then((m) => { try { m.attachPluginsAndInit(nextBot); } catch (e) { log("plugin-init-error", e); } })
|
||||
.catch((e) => log("mcdata-load-error", e));
|
||||
nextBot.once("login", () => {
|
||||
setConnectionState("connected");
|
||||
});
|
||||
@@ -1122,7 +1155,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
lastDisconnectReason = `error: ${truncate(redact(stringifyUnknown(error), current), 200)}`;
|
||||
activeWorldTask = undefined;
|
||||
stopAuthTimer();
|
||||
if (bot === nextBot) bot = undefined;
|
||||
if (bot === nextBot) { bot = undefined; (globalThis as any).__pepaPiBot = undefined; }
|
||||
setConnectionState("disconnected");
|
||||
try {
|
||||
nextBot.end("connection error");
|
||||
@@ -1135,7 +1168,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
nextBot.on("end", (reasonText) => {
|
||||
activeWorldTask = undefined;
|
||||
stopAuthTimer();
|
||||
if (bot === nextBot) bot = undefined;
|
||||
if (bot === nextBot) { bot = undefined; (globalThis as any).__pepaPiBot = undefined; }
|
||||
const reasonString = stringifyUnknown(reasonText || lastDisconnectReason || "end");
|
||||
lastDisconnectReason = reasonString;
|
||||
if (manualDisconnectRequested || shuttingDown) {
|
||||
@@ -1161,6 +1194,7 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
}
|
||||
const currentBot = bot as Bot & { quit?: (reason?: string) => void; end?: (reason?: string) => void };
|
||||
bot = undefined;
|
||||
(globalThis as any).__pepaPiBot = undefined;
|
||||
setConnectionState("disconnected");
|
||||
if (typeof currentBot.quit === "function") {
|
||||
currentBot.quit();
|
||||
@@ -1866,6 +1900,31 @@ export default function mineflayerBridge(pi: ExtensionAPI) {
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "mc_dig",
|
||||
label: "Minecraft Dig Block",
|
||||
description: "Dig one block at exact coordinates using Mineflayer bot.dig(bot.blockAt(new Vec3(x,y,z))).",
|
||||
promptSnippet: "Dig one block at exact Minecraft coordinates.",
|
||||
parameters: DIG_PARAMS,
|
||||
executionMode: "sequential",
|
||||
async execute(_toolCallId, params: DigInput) {
|
||||
const currentBot = activeBot();
|
||||
const pos = new Vec3(
|
||||
Math.floor(finiteNumber(params.x, "x")),
|
||||
Math.floor(finiteNumber(params.y, "y")),
|
||||
Math.floor(finiteNumber(params.z, "z")),
|
||||
);
|
||||
const block = currentBot.blockAt(pos);
|
||||
if (!block) throw new Error(`No loaded block at ${pos.x},${pos.y},${pos.z}.`);
|
||||
if (block.name === "air") throw new Error(`Block at ${pos.x},${pos.y},${pos.z} is air.`);
|
||||
await currentBot.dig(block);
|
||||
return {
|
||||
content: [{ type: "text", text: `Dug ${block.name} at x=${pos.x}, y=${pos.y}, z=${pos.z}.` }],
|
||||
details: { x: pos.x, y: pos.y, z: pos.z, block: block.name },
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
pi.registerTool({
|
||||
name: "mc_goto",
|
||||
label: "Minecraft Guarded Go To",
|
||||
|
||||
Generated
+152
-4
@@ -10,12 +10,16 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"minecraft-data": "^3.110.2",
|
||||
"mineflayer": "^4.37.1",
|
||||
"mineflayer-armor-manager": "^2.0.1",
|
||||
"mineflayer-auto-eat": "^5.0.3",
|
||||
"mineflayer-collectblock": "^1.6.0",
|
||||
"mineflayer-pathfinder": "^2.4.5",
|
||||
"mineflayer-tool": "^1.2.0"
|
||||
"mineflayer-pvp": "^1.3.2",
|
||||
"mineflayer-tool": "^1.2.0",
|
||||
"prismarine-item": "^1.18.0",
|
||||
"vec3": "^0.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -526,6 +530,23 @@
|
||||
"vec3": "^0.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer-pathfinder/node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/mineflayer-pvp": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer-pvp/-/mineflayer-pvp-1.3.2.tgz",
|
||||
"integrity": "sha512-CI2T5w4ceiQdQRCFdrCs3OYmvO9G0cOx2qovUt6f27gknpVI90Ihe1qYhf+zBZJCg9uP1oJ1Pemlzw46Mm3wcA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mineflayer": "^4.0.0",
|
||||
"mineflayer-pathfinder": "^2.0.0",
|
||||
"mineflayer-utils": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer-tool": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer-tool/-/mineflayer-tool-1.2.0.tgz",
|
||||
@@ -537,6 +558,87 @@
|
||||
"prismarine-nbt": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer-utils": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer-utils/-/mineflayer-utils-0.1.4.tgz",
|
||||
"integrity": "sha512-8+0dbGAjA6FO62/W80v5k44AuSAcwJUMdpMAAGhjI9AdCDz+UuyTnNkPBncF4EcLY7yUUdtA7m/OXQZKFdWOlg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "^14.0.27",
|
||||
"mineflayer": "^2.27.0",
|
||||
"prismarine-entity": "^1.0.0",
|
||||
"require-self": "^0.2.3",
|
||||
"typescript": "^3.9.7"
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer-utils/node_modules/@types/node": {
|
||||
"version": "14.18.63",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.18.63.tgz",
|
||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mineflayer-utils/node_modules/minecraft-data": {
|
||||
"version": "2.221.0",
|
||||
"resolved": "https://registry.npmjs.org/minecraft-data/-/minecraft-data-2.221.0.tgz",
|
||||
"integrity": "sha512-0AhqzbIKb6WqPSF6qBevaPryeWOz545hLxt6q+gfJF8YIQX/YfkyX/nXWhl+pSIS2rTBcQ0RJkRCtTeRzQwHDA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mineflayer-utils/node_modules/mineflayer": {
|
||||
"version": "2.41.0",
|
||||
"resolved": "https://registry.npmjs.org/mineflayer/-/mineflayer-2.41.0.tgz",
|
||||
"integrity": "sha512-IFFy4NgF24FU2PkAwazJphl2F+3gpbpN578ex0sq1XfcBBRge3kCz1UC2KDMjKI+V/8vffOL+OEnug9jt3f7Vw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minecraft-data": "^2.70.0",
|
||||
"minecraft-protocol": "^1.17.0",
|
||||
"prismarine-biome": "^1.1.0",
|
||||
"prismarine-block": "^1.6.0",
|
||||
"prismarine-chat": "^1.0.0",
|
||||
"prismarine-chunk": "^1.20.3",
|
||||
"prismarine-entity": "^1.0.0",
|
||||
"prismarine-item": "^1.5.0",
|
||||
"prismarine-physics": "^1.0.4",
|
||||
"prismarine-recipe": "^1.1.0",
|
||||
"prismarine-windows": "^1.5.0",
|
||||
"prismarine-world": "^3.2.0",
|
||||
"protodef": "^1.8.0",
|
||||
"typed-emitter": "^1.2.0",
|
||||
"vec3": "^0.1.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer-utils/node_modules/prismarine-entity": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-entity/-/prismarine-entity-1.2.0.tgz",
|
||||
"integrity": "sha512-4dQ9LYl6HDJQrwZHjSKU4D5VNyHRnfrjcw7eVLlbRPkuR50utW5mmfPi4ys9U7tHNmGWHC/cwjH9xzT75LUovQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"vec3": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer-utils/node_modules/prismarine-windows": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-windows/-/prismarine-windows-1.6.0.tgz",
|
||||
"integrity": "sha512-026LG1yR76Xb62kM+W83IWT7Wy2yKplllbXNFBF2m0Lr4k4YpYKnpLb8tRft8MLOLRbYAt/KnxE/YKvRZul7kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prismarine-item": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mineflayer-utils/node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/mineflayer/node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/mojangson": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mojangson/-/mojangson-2.0.4.tgz",
|
||||
@@ -676,6 +778,12 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-chunk/node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/prismarine-entity": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-entity/-/prismarine-entity-2.6.0.tgz",
|
||||
@@ -688,6 +796,12 @@
|
||||
"vec3": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-entity/node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/prismarine-item": {
|
||||
"version": "1.18.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-item/-/prismarine-item-1.18.0.tgz",
|
||||
@@ -718,6 +832,12 @@
|
||||
"vec3": "^0.1.7"
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-physics/node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/prismarine-realms": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/prismarine-realms/-/prismarine-realms-1.6.0.tgz",
|
||||
@@ -780,6 +900,12 @@
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prismarine-world/node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/process": {
|
||||
"version": "0.11.10",
|
||||
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
|
||||
@@ -859,6 +985,15 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-self": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/require-self/-/require-self-0.2.3.tgz",
|
||||
"integrity": "sha512-keGBWkK0PWJGFAd6IznpjM5zZzySbsrvzq0ElXpZ4G8hzymV9I+/OnC916MF5mRxHRWSZKGIyCFJ73BZFz3xaA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"require-self": "bin/require-self"
|
||||
}
|
||||
},
|
||||
"node_modules/ret": {
|
||||
"version": "0.1.15",
|
||||
"resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz",
|
||||
@@ -948,6 +1083,19 @@
|
||||
"integrity": "sha512-weBmoo3HhpKGgLBOYwe8EB31CzDFuaK7CCL+axXhUYhn4jo6DSkHnbefboCF5i4DQ2aMFe0C/FdTWcPdObgHyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "3.9.10",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
|
||||
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uint4": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/uint4/-/uint4-0.1.2.tgz",
|
||||
@@ -989,9 +1137,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vec3": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.1.10.tgz",
|
||||
"integrity": "sha512-Sr1U3mYtMqCOonGd3LAN9iqy0qF6C+Gjil92awyK/i2OwiUo9bm7PnLgFpafymun50mOjnDcg4ToTgRssrlTcw==",
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vec3/-/vec3-0.2.0.tgz",
|
||||
"integrity": "sha512-jOjU4zbCNWOKOGLHop1I0J2Ar4h5Zz76455LJhJKhiOJwI21QP6d5TnKklNZDKG+c5h71zWtPAbd9wh+Va57OQ==",
|
||||
"license": "BSD"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
|
||||
+5
-1
@@ -14,12 +14,16 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.5",
|
||||
"minecraft-data": "^3.110.2",
|
||||
"mineflayer": "^4.37.1",
|
||||
"mineflayer-armor-manager": "^2.0.1",
|
||||
"mineflayer-auto-eat": "^5.0.3",
|
||||
"mineflayer-collectblock": "^1.6.0",
|
||||
"mineflayer-pathfinder": "^2.4.5",
|
||||
"mineflayer-tool": "^1.2.0"
|
||||
"mineflayer-pvp": "^1.3.2",
|
||||
"mineflayer-tool": "^1.2.0",
|
||||
"prismarine-item": "^1.18.0",
|
||||
"vec3": "^0.2.0"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
Reference in New Issue
Block a user