MCPMC (Minecraft MCP)

by gerred
Verified
import { createBot } from "mineflayer"; import type { Bot, Furnace } from "mineflayer"; import { Vec3 } from "vec3"; import { pathfinder, Movements, goals } from "mineflayer-pathfinder"; import type { Pathfinder } from "mineflayer-pathfinder"; import type { Position, MinecraftBot, ToolResponse, Player, InventoryItem, Entity as CustomEntity, Block, HealthStatus, Weather, Recipe, Container, } from "../types/minecraft"; import { TypeConverters } from "../types/minecraft"; import { Block as PrismarineBlock } from "prismarine-block"; import { Item } from "prismarine-item"; import { EventEmitter } from "events"; interface PrismarineBlockWithBoundingBox extends PrismarineBlock { boundingBox: string; } type EquipmentDestination = | "hand" | "off-hand" | "head" | "torso" | "legs" | "feet"; interface ExtendedBot extends Bot { pathfinder: Pathfinder & { setMovements(movements: Movements): void; goto(goal: goals.Goal): Promise<void>; }; } interface ConnectionParams { host: string; port: number; username: string; version?: string; hideErrors?: boolean; } export class MineflayerBot extends EventEmitter implements MinecraftBot { private bot: ExtendedBot | null = null; private isConnected: boolean = false; private isConnecting: boolean = false; private reconnectAttempts: number = 0; private readonly maxReconnectAttempts: number = 3; private lastConnectionParams: ConnectionParams; private movements: Movements | null = null; constructor(connectionParams: ConnectionParams) { super(); this.lastConnectionParams = connectionParams; } async connect(host: string, port: number, username: string): Promise<void> { if (this.isConnecting) { return; } this.isConnecting = true; try { const params: ConnectionParams = { host, port, username }; this.lastConnectionParams = params; await this.setupBot(); } finally { this.isConnecting = false; } } private setupBot(): Promise<void> { return new Promise<void>((resolve, reject) => { try { if (this.isConnecting) { reject(new Error("Already connecting")); return; } this.isConnecting = true; if (this.bot) { this.bot.end(); this.bot = null; } this.bot = createBot({ ...this.lastConnectionParams, hideErrors: false, }); this.bot.loadPlugin(pathfinder); this.bot.on("error", (error: Error) => { this.logError("Bot error", error); this.isConnecting = false; reject(error); }); this.bot.on("kicked", (reason: string, loggedIn: boolean) => { this.logError("Bot kicked", { reason, loggedIn }); this.isConnecting = false; this.handleDisconnect(); }); this.bot.once("spawn", () => { this.logDebug("Bot spawned successfully"); this.isConnected = true; this.isConnecting = false; this.reconnectAttempts = 0; this.setupMovements(); resolve(); }); this.bot.on("end", (reason: string) => { this.logError("Bot connection ended", { reason }); this.isConnecting = false; this.handleDisconnect(); }); } catch (error) { this.logError("Bot setup error", error); this.isConnecting = false; this.sendJSONRPCError(-32001, "Failed to create bot", { error: error instanceof Error ? error.message : String(error), }); reject(error); } }); } private setupMovements(): void { if (!this.bot) return; try { this.movements = new Movements(this.bot); this.movements.allowParkour = true; this.movements.allowSprinting = true; this.bot.pathfinder.setMovements(this.movements); } catch (error) { this.sendJSONRPCError(-32002, "Error setting up movements", { error: error instanceof Error ? error.message : String(error), }); } } private handleDisconnect(): void { this.isConnected = false; this.movements = null; // Send a notification that the bot has disconnected this.sendJsonRpcNotification("bot.disconnected", { message: "Bot disconnected from server", }); } private sendJsonRpcNotification(method: string, params: any) { process.stdout.write( JSON.stringify({ jsonrpc: "2.0", method, params, id: null, }) + "\n" ); } private sendJSONRPCError(code: number, message: string, data?: any) { process.stdout.write( JSON.stringify({ jsonrpc: "2.0", id: null, error: { code, message, data, }, }) + "\n" ); } private logDebug(message: string, data?: any) { this.sendJsonRpcNotification("bot.debug", { message, data }); } private logWarning(message: string, data?: any) { this.sendJsonRpcNotification("bot.warning", { message, data }); } private logError(message: string, error?: any) { this.sendJsonRpcNotification("bot.error", { message, error: String(error), }); } disconnect(): void { if (this.bot) { this.bot.end(); this.bot = null; } } chat(message: string): void { if (!this.bot) { return this.wrapError("Not connected"); } this.bot.chat(message); } getPosition(): Position | null { if (!this.bot?.entity?.position) return null; const pos = this.bot.entity.position; return { x: pos.x, y: pos.y, z: pos.z }; } getHealth(): number { if (!this.bot) { return this.wrapError("Not connected"); } return this.bot.health; } getInventory(): InventoryItem[] { if (!this.bot) { return this.wrapError("Not connected"); } return this.bot.inventory.items().map(TypeConverters.item); } getPlayers(): Player[] { if (!this.bot) { return this.wrapError("Not connected"); } return Object.values(this.bot.players).map((player) => ({ username: player.username, uuid: player.uuid, ping: player.ping, })); } async navigateTo( x: number, y: number, z: number, progressCallback?: (progress: number) => void ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); const goal = new goals.GoalNear(x, y, z, 1); try { const startPos = this.bot.entity.position; const targetPos = new Vec3(x, y, z); const totalDistance = startPos.distanceTo(targetPos); // Set up progress monitoring const checkProgress = () => { if (!this.bot) return; const currentPos = this.bot.entity.position; const remainingDistance = currentPos.distanceTo(targetPos); const progress = Math.min( 100, ((totalDistance - remainingDistance) / totalDistance) * 100 ); progressCallback?.(progress); }; const progressInterval = setInterval(checkProgress, 500); try { await this.bot.pathfinder.goto(goal); } finally { clearInterval(progressInterval); // Send final progress progressCallback?.(100); } } catch (error) { return this.wrapError( `Failed to navigate: ${ error instanceof Error ? error.message : String(error) }` ); } } async digBlock(x: number, y: number, z: number): Promise<void> { if (!this.bot) { return this.wrapError("Not connected"); } const targetPos = new Vec3(x, y, z); // Try to move close enough to dig if needed try { const goal = new goals.GoalNear(x, y, z, 3); // Stay within 3 blocks await this.bot.pathfinder.goto(goal); } catch (error) { this.logWarning("Could not move closer to block for digging", error); // Continue anyway - the block might still be reachable } while (true) { const block = this.bot.blockAt(targetPos); if (!block) { // No block at all, so we're done return; } if (block.name === "air") { // The target is now air, so we're done return; } // Skip bedrock and other indestructible blocks if (block.hardness < 0) { this.logWarning( `Cannot dig indestructible block ${block.name} at ${x}, ${y}, ${z}` ); return; } // Attempt to dig try { await this.bot.dig(block); } catch (err) { const error = err as Error; // If it's a known "cannot dig" error, skip if ( error.message?.includes("cannot be broken") || error.message?.includes("cannot dig") || error.message?.includes("unreachable") ) { this.logWarning( `Failed to dig block ${block.name} at ${x}, ${y}, ${z}: ${error.message}` ); return; } // For other errors, wrap them return this.wrapError(error.message || String(error)); } // Small delay to avoid server spam await new Promise((resolve) => setTimeout(resolve, 150)); } } async digArea( start: Position, end: Position, progressCallback?: ( progress: number, blocksDug: number, totalBlocks: number ) => void ): Promise<void> { if (!this.bot) { return this.wrapError("Not connected"); } const minX = Math.min(start.x, end.x); const maxX = Math.max(start.x, end.x); const minY = Math.min(start.y, end.y); const maxY = Math.max(start.y, end.y); const minZ = Math.min(start.z, end.z); const maxZ = Math.max(start.z, end.z); // Pre-scan the area to identify diggable blocks and create an efficient digging plan const diggableBlocks: Vec3[] = []; const undiggableBlocks: Vec3[] = []; // Helper to check if a block is diggable const isDiggable = (block: PrismarineBlock | null): boolean => { if (!block) return false; if (block.name === "air") return false; if (block.hardness < 0) return false; // Bedrock and other unbreakable blocks // Skip fluid blocks if ( block.name.includes("water") || block.name.includes("lava") || block.name.includes("flowing") ) { return false; } // Skip blocks that are known to be unbreakable or special const unbreakableBlocks = [ "barrier", "bedrock", "end_portal", "end_portal_frame", ]; if (unbreakableBlocks.includes(block.name)) return false; return true; }; // First pass: identify all diggable blocks for (let y = maxY; y >= minY; y--) { for (let x = minX; x <= maxX; x++) { for (let z = minZ; z <= maxZ; z++) { const pos = new Vec3(x, y, z); const block = this.bot.blockAt(pos); if (isDiggable(block)) { diggableBlocks.push(pos); } else if (block && block.name !== "air") { undiggableBlocks.push(pos); } } } } const totalBlocks = diggableBlocks.length; let blocksDug = 0; let lastProgressUpdate = Date.now(); // Set up disconnect handler let disconnected = false; const disconnectHandler = () => { disconnected = true; }; this.bot.once("end", disconnectHandler); try { // Group blocks into "slices" for more efficient digging const sliceSize = 4; // Size of each work area const slices: Vec3[][] = []; // Group blocks into nearby clusters for efficient movement for (let x = minX; x <= maxX; x += sliceSize) { for (let z = minZ; z <= maxZ; z += sliceSize) { const slice: Vec3[] = diggableBlocks.filter( (pos) => pos.x >= x && pos.x < x + sliceSize && pos.z >= z && pos.z < z + sliceSize ); if (slice.length > 0) { // Sort the slice from top to bottom for safer digging slice.sort((a, b) => b.y - a.y); slices.push(slice); } } } // Process each slice for (const slice of slices) { if (disconnected) { return this.wrapError("Disconnected while digging area"); } // Find optimal position to dig this slice const sliceCenter = slice .reduce((acc, pos) => acc.plus(pos), new Vec3(0, 0, 0)) .scaled(1 / slice.length); // Try to move to a good position for this slice try { // Position ourselves at a good vantage point for the slice const standingPos = new Vec3( sliceCenter.x - 1, Math.max(sliceCenter.y, minY), sliceCenter.z - 1 ); await this.navigateTo(standingPos.x, standingPos.y, standingPos.z); } catch (error) { this.logWarning( "Could not reach optimal digging position for slice", error ); // Continue anyway - some blocks might still be reachable } // Process blocks in the slice from top to bottom for (const pos of slice) { if (disconnected) { return this.wrapError("Disconnected while digging area"); } try { const block = this.bot.blockAt(pos); if (!block || !isDiggable(block)) { continue; // Skip if block changed or became undiggable } // Check if we need to move closer const distance = pos.distanceTo(this.bot.entity.position); if (distance > 4) { try { const goal = new goals.GoalNear(pos.x, pos.y, pos.z, 3); await this.bot.pathfinder.goto(goal); } catch (error) { this.logWarning( `Could not move closer to block at ${pos.x}, ${pos.y}, ${pos.z}:`, error ); continue; // Skip this block if we can't reach it } } await this.digBlock(pos.x, pos.y, pos.z); blocksDug++; // Update progress every 500ms const now = Date.now(); if (progressCallback && now - lastProgressUpdate >= 500) { const progress = Math.floor((blocksDug / totalBlocks) * 100); progressCallback(progress, blocksDug, totalBlocks); lastProgressUpdate = now; } } catch (error) { // Log the error but continue with other blocks this.logWarning( `Failed to dig block at ${pos.x}, ${pos.y}, ${pos.z}:`, error ); continue; } } } // Final progress update if (progressCallback) { progressCallback(100, blocksDug, totalBlocks); } // Log summary of undiggable blocks if any if (undiggableBlocks.length > 0) { this.logWarning( `Completed digging with ${undiggableBlocks.length} undiggable blocks`, undiggableBlocks.map((pos) => ({ position: pos, type: this.bot?.blockAt(pos)?.name || "unknown", })) ); } } finally { // Clean up the disconnect handler this.bot.removeListener("end", disconnectHandler); } } async placeBlock( x: number, y: number, z: number, blockName: string ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); const item = this.bot.inventory.items().find((i) => i.name === blockName); if (!item) return this.wrapError(`No ${blockName} in inventory`); try { await this.bot.equip(item, "hand"); const targetPos = new Vec3(x, y, z); const targetBlock = this.bot.blockAt(targetPos); if (!targetBlock) return this.wrapError("Invalid target position for placing block"); const faceVector = new Vec3(0, 1, 0); await this.bot.placeBlock(targetBlock, faceVector); } catch (error) { return this.wrapError( `Failed to place block: ${ error instanceof Error ? error.message : String(error) }` ); } } async followPlayer(username: string, distance: number = 2): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); const target = this.bot.players[username]?.entity; if (!target) return this.wrapError(`Player ${username} not found`); const goal = new goals.GoalFollow(target, distance); try { await this.bot.pathfinder.goto(goal); } catch (error) { return this.wrapError( `Failed to follow player: ${ error instanceof Error ? error.message : String(error) }` ); } } async attackEntity( entityName: string, maxDistance: number = 5 ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); const entity = Object.values(this.bot.entities).find( (e) => e.name === entityName && e.position.distanceTo(this.bot!.entity.position) <= maxDistance ); if (!entity) return this.wrapError( `No ${entityName} found within ${maxDistance} blocks` ); try { await this.bot.attack(entity as any); } catch (error) { return this.wrapError( `Failed to attack entity: ${ error instanceof Error ? error.message : String(error) }` ); } } getEntitiesNearby(maxDistance: number = 10): CustomEntity[] { if (!this.bot) return this.wrapError("Not connected"); return Object.values(this.bot.entities) .filter( (e) => e.position.distanceTo(this.bot!.entity.position) <= maxDistance ) .map(TypeConverters.entity); } getBlocksNearby(maxDistance: number = 10, count: number = 100): Block[] { if (!this.bot) return this.wrapError("Not connected"); return this.bot .findBlocks({ matching: () => true, maxDistance, count, }) .map((pos) => { const block = this.bot?.blockAt(pos); return block ? TypeConverters.block(block) : null; }) .filter((b): b is Block => b !== null); } getHealthStatus(): HealthStatus { if (!this.bot) return this.wrapError("Not connected"); return { health: this.bot.health, food: this.bot.food, saturation: this.bot.foodSaturation, armor: this.bot.game.gameMode === "creative" ? 20 : 0, }; } getWeather(): Weather { if (!this.bot) return this.wrapError("Not connected"); return { isRaining: this.bot.isRaining, rainState: this.bot.isRaining ? "raining" : "clear", thunderState: this.bot.thunderState, }; } async navigateRelative( dx: number, dy: number, dz: number, progressCallback?: (progress: number) => void ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); const currentPos = this.bot.entity.position; const yaw = this.bot.entity.yaw; const sin = Math.sin(yaw); const cos = Math.cos(yaw); const worldDx = dx * cos - dz * sin; const worldDz = dx * sin + dz * cos; try { await this.navigateTo( currentPos.x + worldDx, currentPos.y + dy, currentPos.z + worldDz, progressCallback ); } catch (error) { return this.wrapError( `Failed to navigate relatively: ${ error instanceof Error ? error.message : String(error) }` ); } } private relativeToAbsolute( origin: Vec3, dx: number, dy: number, dz: number ): Position { const yaw = this.bot!.entity.yaw; const sin = Math.sin(yaw); const cos = Math.cos(yaw); // For "forward/back" as +Z, "left/right" as ±X const worldDx = dx * cos - dz * sin; const worldDz = dx * sin + dz * cos; return { x: Math.floor(origin.x + worldDx), y: Math.floor(origin.y + dy), z: Math.floor(origin.z + worldDz), }; } async digBlockRelative(dx: number, dy: number, dz: number): Promise<void> { if (!this.bot) throw new Error("Not connected"); const currentPos = this.bot.entity.position; const { x, y, z } = this.relativeToAbsolute(currentPos, dx, dy, dz); await this.digBlock(x, y, z); } async digAreaRelative( start: { dx: number; dy: number; dz: number }, end: { dx: number; dy: number; dz: number }, progressCallback?: ( progress: number, blocksDug: number, totalBlocks: number ) => void ): Promise<void> { if (!this.bot) throw new Error("Not connected"); const currentPos = this.bot.entity.position; // Convert both corners to absolute coordinates const absStart = this.relativeToAbsolute( currentPos, start.dx, start.dy, start.dz ); const absEnd = this.relativeToAbsolute(currentPos, end.dx, end.dy, end.dz); // Use the absolute digArea method await this.digArea(absStart, absEnd, progressCallback); } get entity() { if (!this.bot?.entity) return this.wrapError("Not connected"); return { position: this.bot.entity.position, velocity: this.bot.entity.velocity, yaw: this.bot.entity.yaw, pitch: this.bot.entity.pitch, }; } get entities() { if (!this.bot) return this.wrapError("Not connected"); const converted: { [id: string]: CustomEntity } = {}; for (const [id, e] of Object.entries(this.bot.entities)) { converted[id] = TypeConverters.entity(e); } return converted; } get inventory() { if (!this.bot) return this.wrapError("Not connected"); return { items: () => this.bot!.inventory.items().map(TypeConverters.item), slots: Object.fromEntries( Object.entries(this.bot!.inventory.slots).map(([slot, item]) => [ slot, item ? TypeConverters.item(item) : null, ]) ), }; } get pathfinder() { if (!this.bot) return this.wrapError("Not connected"); if (!this.movements) { this.movements = new Movements(this.bot as unknown as Bot); } const pf = this.bot.pathfinder; const currentMovements = this.movements; return { setMovements: (movements: Movements) => { this.movements = movements; pf.setMovements(movements); }, goto: (goal: goals.Goal) => pf.goto(goal), getPathTo: async (goal: goals.Goal, timeout?: number) => { if (!this.movements) return this.wrapError("Movements not initialized"); const path = await pf.getPathTo(this.movements, goal, timeout); if (!path) return null; return { path: path.path.map((pos: any) => new Vec3(pos.x, pos.y, pos.z)), }; }, }; } blockAt(position: Vec3): Block | null { if (!this.bot) return this.wrapError("Not connected"); const block = this.bot.blockAt(position); return block ? TypeConverters.block(block) : null; } findBlocks(options: { matching: ((block: Block) => boolean) | string | string[]; maxDistance: number; count: number; point?: Vec3; }): Vec3[] { if (!this.bot) return this.wrapError("Not connected"); // Convert string or string[] to matching function let matchingFn: (block: PrismarineBlock) => boolean; if (typeof options.matching === "string") { const blockName = options.matching; matchingFn = (b: PrismarineBlock) => b.name === blockName; } else if (Array.isArray(options.matching)) { const blockNames = options.matching; matchingFn = (b: PrismarineBlock) => blockNames.includes(b.name); } else { const matchingFunc = options.matching; matchingFn = (b: PrismarineBlock) => matchingFunc(TypeConverters.block(b)); } return this.bot.findBlocks({ ...options, matching: matchingFn, }); } getEquipmentDestSlot(destination: string): number { if (!this.bot) return this.wrapError("Not connected"); return this.bot.getEquipmentDestSlot(destination); } canSeeEntity(entity: CustomEntity): boolean { if (!this.bot) return false; const prismarineEntity = Object.values(this.bot.entities).find( (e) => e.name === entity.name && e.position.equals( new Vec3(entity.position.x, entity.position.y, entity.position.z) ) ); if (!prismarineEntity) return false; // Simple line-of-sight check const distance = prismarineEntity.position.distanceTo( this.bot.entity.position ); return ( distance <= 32 && this.hasLineOfSight(this.bot.entity.position, prismarineEntity.position) ); } private hasLineOfSight(start: Vec3, end: Vec3): boolean { if (!this.bot) return false; const direction = end.minus(start).normalize(); const distance = start.distanceTo(end); const steps = Math.ceil(distance); for (let i = 1; i < steps; i++) { const point = start.plus(direction.scaled(i)); const block = this.getPrismarineBlock(point); if (block?.boundingBox !== "empty") { return false; } } return true; } private getPrismarineBlock( position: Vec3 ): PrismarineBlockWithBoundingBox | undefined { if (!this.bot) return undefined; const block = this.bot.blockAt(position); if (!block) return undefined; return block as PrismarineBlockWithBoundingBox; } async craftItem( itemName: string, quantity: number = 1, useCraftingTable: boolean = false ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); try { // Find all available recipes const itemById = this.bot.registry.itemsByName[itemName]; if (!itemById) return this.wrapError(`Unknown item: ${itemName}`); const recipes = this.bot.recipesFor(itemById.id, 1, null, true); const recipe = recipes[0]; // First matching recipe if (!recipe) { return this.wrapError(`No recipe found for ${itemName}`); } if (recipe.requiresTable && !useCraftingTable) { return this.wrapError(`${itemName} requires a crafting table`); } // If we need a crafting table, find one nearby or place one let craftingTableBlock = null; if (useCraftingTable) { const nearbyBlocks = this.findBlocks({ matching: (block) => block.name === "crafting_table", maxDistance: 4, count: 1, }); if (nearbyBlocks.length > 0) { craftingTableBlock = this.bot.blockAt(nearbyBlocks[0]); } else { // Try to place a crafting table const tableItem = this.bot.inventory .items() .find((i) => i.name === "crafting_table"); if (!tableItem) { return this.wrapError("No crafting table in inventory"); } // Find a suitable position to place the table const pos = this.bot.entity.position.offset(0, 0, 1); await this.placeBlock(pos.x, pos.y, pos.z, "crafting_table"); craftingTableBlock = this.bot.blockAt(pos); } } await this.bot.craft(recipe, quantity, craftingTableBlock || undefined); } catch (error) { return this.wrapError( `Failed to craft ${itemName}: ${ error instanceof Error ? error.message : String(error) }` ); } } async equipItem( itemName: string, destination: EquipmentDestination ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); const item = this.bot.inventory.items().find((i) => i.name === itemName); if (!item) return this.wrapError(`No ${itemName} in inventory`); try { await this.bot.equip(item, destination); } catch (error) { return this.wrapError( `Failed to equip ${itemName}: ${ error instanceof Error ? error.message : String(error) }` ); } } async dropItem(itemName: string, quantity: number = 1): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); const item = this.bot.inventory.items().find((i) => i.name === itemName); if (!item) return this.wrapError(`No ${itemName} in inventory`); try { await this.bot.toss(item.type, quantity, null); } catch (error) { return this.wrapError( `Failed to drop ${itemName}: ${ error instanceof Error ? error.message : String(error) }` ); } } async openContainer(position: Position): Promise<Container> { if (!this.bot) return this.wrapError("Not connected"); const block = this.bot.blockAt( new Vec3(position.x, position.y, position.z) ); if (!block) return this.wrapError("No block at specified position"); try { const container = await this.bot.openContainer(block); return { type: block.name as "chest" | "furnace" | "crafting_table", position, slots: Object.fromEntries( Object.entries(container.slots).map(([slot, item]) => [ slot, item ? TypeConverters.item(item as Item) : null, ]) ), }; } catch (error) { return this.wrapError( `Failed to open container: ${ error instanceof Error ? error.message : String(error) }` ); } } closeContainer(): void { if (!this.bot?.currentWindow) return; this.bot.closeWindow(this.bot.currentWindow); } getRecipe(itemName: string): Recipe | null { if (!this.bot) return null; const itemById = this.bot.registry.itemsByName[itemName]; if (!itemById) return null; const recipes = this.bot.recipesFor(itemById.id, 1, null, true); const recipe = recipes[0]; if (!recipe) return null; return { name: itemName, ingredients: (recipe.ingredients as any[]) .filter((item) => item != null) .reduce((acc: { [key: string]: number }, item) => { const name = Object.entries(this.bot!.registry.itemsByName).find( ([_, v]) => v.id === item.id )?.[0]; if (name) { acc[name] = (acc[name] || 0) + 1; } return acc; }, {}), requiresCraftingTable: recipe.requiresTable, }; } listAvailableRecipes(): Recipe[] { if (!this.bot) return []; const recipes = new Set<string>(); // Get all item names from registry Object.keys(this.bot.registry.itemsByName).forEach((name) => { const recipe = this.getRecipe(name); if (recipe) { recipes.add(name); } }); return Array.from(recipes) .map((name) => this.getRecipe(name)) .filter((recipe): recipe is Recipe => recipe !== null); } canCraft(recipe: Recipe): boolean { if (!this.bot) return false; // Check if we have all required ingredients for (const [itemName, count] of Object.entries(recipe.ingredients)) { const available = this.bot.inventory .items() .filter((item) => item.name === itemName) .reduce((sum, item) => sum + item.count, 0); if (available < count) return false; } // If it needs a crafting table, check if we have one or can reach one if (recipe.requiresCraftingTable) { const hasCraftingTable = this.bot.inventory .items() .some((item) => item.name === "crafting_table"); if (!hasCraftingTable) { const nearbyCraftingTable = this.findBlocks({ matching: (block) => block.name === "crafting_table", maxDistance: 4, count: 1, }); if (nearbyCraftingTable.length === 0) return false; } } return true; } async smeltItem( itemName: string, fuelName: string, quantity: number = 1 ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); try { // Find a nearby furnace or place one const nearbyBlocks = this.findBlocks({ matching: (block) => block.name === "furnace", maxDistance: 4, count: 1, }); let furnaceBlock; if (nearbyBlocks.length > 0) { furnaceBlock = this.bot.blockAt(nearbyBlocks[0]); } else { // Try to place a furnace const furnaceItem = this.bot.inventory .items() .find((i) => i.name === "furnace"); if (!furnaceItem) { return this.wrapError("No furnace in inventory"); } const pos = this.bot.entity.position.offset(0, 0, 1); await this.placeBlock(pos.x, pos.y, pos.z, "furnace"); furnaceBlock = this.bot.blockAt(pos); } if (!furnaceBlock) return this.wrapError("Could not find or place furnace"); // Open the furnace const furnace = (await this.bot.openContainer( furnaceBlock )) as unknown as Furnace; try { // Add the item to smelt const itemToSmelt = this.bot.inventory .items() .find((i) => i.name === itemName); if (!itemToSmelt) return this.wrapError(`No ${itemName} in inventory`); // Add the fuel const fuelItem = this.bot.inventory .items() .find((i) => i.name === fuelName); if (!fuelItem) return this.wrapError(`No ${fuelName} in inventory`); // Put items in the furnace await furnace.putInput(itemToSmelt.type, null, quantity); await furnace.putFuel(fuelItem.type, null, quantity); // Wait for smelting to complete await new Promise((resolve) => { const checkInterval = setInterval(() => { if (furnace.fuel === 0 && furnace.progress === 0) { clearInterval(checkInterval); resolve(null); } }, 1000); }); } finally { // Always close the furnace when done this.bot.closeWindow(furnace); } } catch (error) { return this.wrapError( `Failed to smelt ${itemName}: ${ error instanceof Error ? error.message : String(error) }` ); } } async depositItem( containerPosition: Position, itemName: string, quantity: number = 1 ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); try { const block = this.bot.blockAt( new Vec3(containerPosition.x, containerPosition.y, containerPosition.z) ); if (!block) return this.wrapError("No container at position"); const window = await this.bot.openContainer(block); if (!window) return this.wrapError("Failed to open container"); try { const item = this.bot.inventory.slots.find((i) => i?.name === itemName); if (!item) return this.wrapError(`No ${itemName} in inventory`); const emptySlot = window.slots.findIndex( (slot: Item | null) => slot === null ); if (emptySlot === -1) return this.wrapError("Container is full"); await this.bot.moveSlotItem(item.slot, emptySlot); } finally { this.bot.closeWindow(window); } } catch (error) { return this.wrapError( `Failed to deposit ${itemName}: ${ error instanceof Error ? error.message : String(error) }` ); } } async withdrawItem( containerPosition: Position, itemName: string, quantity: number = 1 ): Promise<void> { if (!this.bot) return this.wrapError("Not connected"); try { const block = this.bot.blockAt( new Vec3(containerPosition.x, containerPosition.y, containerPosition.z) ); if (!block) return this.wrapError("No container at position"); const window = await this.bot.openContainer(block); if (!window) return this.wrapError("Failed to open container"); try { const containerSlot = window.slots.findIndex( (item: Item | null) => item?.name === itemName ); if (containerSlot === -1) return this.wrapError(`No ${itemName} in container`); const emptySlot = this.bot.inventory.slots.findIndex( (slot) => slot === null ); if (emptySlot === -1) return this.wrapError("Inventory is full"); await this.bot.moveSlotItem(containerSlot, emptySlot); } finally { this.bot.closeWindow(window); } } catch (error) { return this.wrapError( `Failed to withdraw ${itemName}: ${ error instanceof Error ? error.message : String(error) }` ); } } private wrapError(message: string): never { throw { code: -32603, message, data: null, }; } }