diff --git a/services/minecraft/src/agents/action/index.ts b/services/minecraft/src/agents/action/index.ts index 6565a466d..e3a8d813b 100644 --- a/services/minecraft/src/agents/action/index.ts +++ b/services/minecraft/src/agents/action/index.ts @@ -5,6 +5,7 @@ import type { PlanStep } from '../planning/adapter' import { useBot } from '../../composables/bot' import { AbstractAgent } from '../../libs/mineflayer/base-agent' +import { ActionError } from '../../utils/errors' import { actionsList } from './tools' interface ActionState { @@ -78,6 +79,10 @@ export class ActionAgentImpl extends AbstractAgent implements ActionAgent { }) } catch (error) { + if (error instanceof ActionError) { + this.logger.withError(error).warn(`Action failed: ${error.code}`) + throw error + } this.logger.withError(error).error('Action failed') throw error } diff --git a/services/minecraft/src/agents/planning/index.ts b/services/minecraft/src/agents/planning/index.ts index 39a7afa12..950801ae7 100644 --- a/services/minecraft/src/agents/planning/index.ts +++ b/services/minecraft/src/agents/planning/index.ts @@ -5,6 +5,7 @@ import type { ActionAgent, AgentConfig, MemoryAgent, Plan, PlanningAgent } from import type { PlanStep } from './adapter' import { AbstractAgent } from '../../libs/mineflayer/base-agent' +import { ActionError } from '../../utils/errors' import { ActionAgentImpl } from '../action' import { PlanningLLMHandler } from './adapter' @@ -164,6 +165,19 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent { await this.actionAgent.performAction(step) } catch (stepError) { + + if (stepError instanceof ActionError) { + this.logger.withError(stepError).warn('Step execution failed with ActionError') + // If it's a resource failure or crafting failure that we've already tried to fix (implied by fail-fast skills), + // then we should abort and report failure instead of looping. + // We can check error types or context. + if (stepError.code === 'RESOURCE_MISSING' || stepError.code === 'CRAFTING_FAILED' || stepError.code === 'INVENTORY_FULL') { + // For now, fail fast on these hard errors. + // In the future we might want a "replanning" phase here, but NOT a blind retry. + throw stepError; + } + } + this.logger.withError(stepError).error('Failed to execute step') // Attempt to adjust plan and retry @@ -187,163 +201,13 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent { } catch (error) { plan.status = 'failed' - throw error + throw error // This will be caught by handleChatMessage and reported to user } finally { this.context = null } } - // private async generateStepsStream( - // goal: string, - // availableActions: Action[], - // sender: string, - // ): Promise { - // if (!this.context) { - // return - // } - - // try { - // // Generate all steps at once - // const steps = await this.llmHandler.generatePlan(goal, availableActions, sender) - // if (!this.context.isGenerating) { - // return - // } - - // // Add all steps to pending queue - // this.context.pendingSteps.push(...steps) - // this.logger.withField('steps', steps).log('Generated steps') - // } - // catch (error) { - // this.logger.withError(error).error('Failed to generate steps') - // throw error - // } - // finally { - // this.context.isGenerating = false - // } - // } - - // private async executeStepsStream(): Promise { - // if (!this.context || !this.actionAgent) { - // return - // } - - // try { - // while (this.context.isGenerating || this.context.pendingSteps.length > 0) { - // // Wait for steps to be available - // if (this.context.pendingSteps.length === 0) { - // await new Promise(resolve => setTimeout(resolve, 100)) - // continue - // } - - // // Execute next step - // const step = this.context.pendingSteps.shift() - // if (!step) { - // continue - // } - - // try { - // this.logger.withField('step', step).log('Executing step') - // await this.actionAgent.performAction(step) - // this.context.lastUpdate = Date.now() - // this.context.currentStep++ - // } - // catch (stepError) { - // this.logger.withError(stepError).error('Failed to execute step') - - // // Attempt to adjust plan and retry - // if (this.context.retryCount < 3) { - // this.context.retryCount++ - // // Stop current generation - // this.context.isGenerating = false - // this.context.pendingSteps = [] - // // Adjust plan and restart - // const adjustedPlan = await this.adjustPlan( - // this.currentPlan!, - // stepError instanceof Error ? stepError.message : 'Unknown error', - // 'system', - // ) - // await this.executePlan(adjustedPlan) - // return - // } - - // throw stepError - // } - // } - // } - // catch (error) { - // this.logger.withError(error).error('Failed to execute steps') - // throw error - // } - // } - - // private async *createStepGenerator( - // goal: string, - // availableActions: Action[], - // ): AsyncGenerator { - // // Use LLM to generate plan in chunks - // this.logger.log('Generating plan using LLM') - // const chunkSize = 3 // Generate 3 steps at a time - // let currentChunk = 1 - - // while (true) { - // const steps = await this.llmHandler.generatePlan( - // goal, - // availableActions, - // `Generate steps ${currentChunk * chunkSize - 2} to ${currentChunk * chunkSize}`, - // ) - - // if (steps.length === 0) { - // break - // } - - // yield steps - // currentChunk++ - - // // Check if we've generated enough steps or if the goal is achieved - // if (steps.length < chunkSize || await this.isGoalAchieved(goal)) { - // break - // } - // } - // } - - // private async isGoalAchieved(goal: string): Promise { - // if (!this.context || !this.actionAgent) { - // return false - // } - - // const requirements = this.parseGoalRequirements(goal) - - // // Check inventory for required items - // if (requirements.needsItems && requirements.items) { - // const inventorySteps = this.generateGatheringSteps(requirements.items) - // if (inventorySteps.length > 0) { - // this.context.pendingSteps.push(...inventorySteps) - // return false - // } - // } - - // // Check location requirements - // if (requirements.needsMovement && requirements.location) { - // const movementSteps = this.generateMovementSteps(requirements.location) - // if (movementSteps.length > 0) { - // this.context.pendingSteps.push(...movementSteps) - // return false - // } - // } - - // // Check interaction requirements - // if (requirements.needsInteraction && requirements.target) { - // const interactionSteps = this.generateInteractionSteps(requirements.target) - // if (interactionSteps.length > 0) { - // this.context.pendingSteps.push(...interactionSteps) - // return false - // } - // } - - // return true - // } - public async adjustPlan(plan: Plan, feedback: string, sender: string): Promise { if (!this.initialized) { throw new Error('Planning agent not initialized') @@ -387,58 +251,6 @@ export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent { } } - // private generateGatheringSteps(items: string[]): PlanStep[] { - // const steps: PlanStep[] = [] - - // for (const item of items) { - // steps.push( - // { - // description: `Search for ${item} in the surrounding area`, - // tool: 'searchForBlock', - // params: { - // blockType: item, - // range: 64, - // }, - // }, - // { - // description: `Collect ${item} from the found location`, - // tool: 'collectBlocks', - // params: { - // blockType: item, - // count: 1, - // }, - // }, - // ) - // } - - // return steps - // } - - // private generateMovementSteps(location: { x?: number, y?: number, z?: number }): PlanStep[] { - // if (location.x !== undefined && location.y !== undefined && location.z !== undefined) { - // return [{ - // description: `Move to coordinates (${location.x}, ${location.y}, ${location.z})`, - // tool: 'goToCoordinates', - // params: { - // x: location.x, - // y: location.y, - // z: location.z, - // }, - // }] - // } - // return [] - // } - - // private generateInteractionSteps(target: string): PlanStep[] { - // return [{ - // description: `Interact with ${target}`, - // tool: 'activate', - // params: { - // target, - // }, - // }] - // } - private generateRecoverySteps(feedback: string): PlanStep[] { const steps: PlanStep[] = [] diff --git a/services/minecraft/src/libs/llm-agent/chat.ts b/services/minecraft/src/libs/llm-agent/chat.ts index 63adf49b3..7a2337070 100644 --- a/services/minecraft/src/libs/llm-agent/chat.ts +++ b/services/minecraft/src/libs/llm-agent/chat.ts @@ -1,6 +1,6 @@ +import type { Logger } from '@guiiai/logg' import type { Neuri, NeuriContext } from 'neuri' -import type { Logger } from '../../utils/logger' import type { MineflayerWithAgents } from './types' import { withRetry } from '@moeru/std' @@ -46,7 +46,7 @@ export async function handleChatMessage(username: string, message: string, bot: } } catch (error) { - logger.withError(error).error('Failed to process message') + logger.withError(error).warn('Failed to process message') bot.bot.chat( `Sorry, I encountered an error: ${ error instanceof Error ? error.message : 'Unknown error' diff --git a/services/minecraft/src/skills/actions/ensure.ts b/services/minecraft/src/skills/actions/ensure.ts index 826372218..504cb8e13 100644 --- a/services/minecraft/src/skills/actions/ensure.ts +++ b/services/minecraft/src/skills/actions/ensure.ts @@ -1,5 +1,6 @@ import type { Mineflayer } from '../../libs/mineflayer' +import { ActionError } from '../../utils/errors' import { useLogger } from '../../utils/logger' import { getItemId } from '../../utils/mcdata' import { craftRecipe } from '../crafting' @@ -17,32 +18,37 @@ const logger = useLogger() export async function ensureCraftingTable(mineflayer: Mineflayer): Promise { logger.log('Bot: Checking for a crafting table...') - let hasCraftingTable = getItemCount(mineflayer, 'crafting_table') > 0 + const hasCraftingTable = getItemCount(mineflayer, 'crafting_table') > 0 if (hasCraftingTable) { logger.log('Bot: Crafting table is available.') return true } - while (!hasCraftingTable) { + // Check if we already have wood to make it + try { const planksEnsured = await ensurePlanks(mineflayer, 4) if (!planksEnsured) { - logger.error('Bot: Failed to ensure planks.') - continue + throw new ActionError('RESOURCE_MISSING', 'Failed to ensure planks for crafting table', { item: 'planks', count: 4 }) } // Craft crafting table - hasCraftingTable = await craftRecipe(mineflayer, 'crafting_table', 1) - if (hasCraftingTable) { + const result = await craftRecipe(mineflayer, 'crafting_table', 1) + if (result) { mineflayer.bot.chat('I have made a crafting table.') logger.log('Bot: Crafting table crafted.') - } - else { - logger.error('Bot: Failed to craft crafting table.') + return true } } + catch (error) { + if (error instanceof ActionError) { + throw error + } + // If craftRecipe failed but didn't throw ActionError (protection for mixed versions) + throw new ActionError('CRAFTING_FAILED', 'Failed to craft crafting table', { error }) + } - return hasCraftingTable + throw new ActionError('CRAFTING_FAILED', 'Failed to ensure crafting table') } // Helper function to ensure a specific amount of planks @@ -51,12 +57,16 @@ export async function ensurePlanks(mineflayer: Mineflayer, neededAmount: number) let planksCount = getItemCount(mineflayer, 'planks') - if (neededAmount < planksCount) { + if (neededAmount <= planksCount) { logger.log('Bot: Have enough planks.') return true } - while (neededAmount > planksCount) { + const maxRetries = 3 + let retries = 0 + + while (neededAmount > planksCount && retries < maxRetries) { + retries++ const logsNeeded = Math.ceil((neededAmount - planksCount) / PLANKS_PER_LOG) // Get all available log types in inventory @@ -66,46 +76,64 @@ export async function ensurePlanks(mineflayer: Mineflayer, neededAmount: number) // If no logs available, gather more wood if (availableLogs.length === 0) { - await gatherWood(mineflayer, logsNeeded, 80) - logger.error('Bot: Not enough logs for planks.') + logger.log(`Bot: Not enough logs. Gathering ${logsNeeded} logs.`) + try { + await gatherWood(mineflayer, logsNeeded, 80) + } + catch (error) { + throw new ActionError('RESOURCE_MISSING', 'Could not gather wood', { item: 'log', count: logsNeeded, originalError: error }) + } + + // Check if we actually got wood + const newLogs = mineflayer.bot.inventory.items().filter(item => item.name.includes('log')) + if (newLogs.length === 0) { + throw new ActionError('RESOURCE_MISSING', 'Gathered wood but inventory still empty of logs', { item: 'log' }) + } + // Continue to next iteration to craft continue } // Iterate over each log type to craft planks + let anyCrafted = false for (const log of availableLogs) { const logType = log.name.replace('_log', '') // Get log type without "_log" suffix const logsToCraft = Math.min(log.count, logsNeeded) - logger.log( - `Trying to make ${logsToCraft * PLANKS_PER_LOG} ${logType}_planks`, - ) - logger.log(`NeededAmount: ${neededAmount}, while I have ${planksCount}`) + logger.log(`Trying to make ${logsToCraft * PLANKS_PER_LOG} ${logType}_planks`) const crafted = await craftRecipe( mineflayer, `${logType}_planks`, logsToCraft * PLANKS_PER_LOG, ) + if (crafted) { planksCount = getItemCount(mineflayer, 'planks') - mineflayer.bot.chat( - `I have crafted ${logsToCraft * PLANKS_PER_LOG} ${logType} planks.`, - ) - logger.log(`Bot: ${logType} planks crafted.`) + mineflayer.bot.chat(`I have crafted ${logsToCraft * PLANKS_PER_LOG} ${logType} planks.`) + anyCrafted = true } else { + // If we have logs but failed to craft planks, it might be due to full inventory or other issues logger.error(`Bot: Failed to craft ${logType} planks.`) - return false } // Check if we have enough planks after crafting if (planksCount >= neededAmount) break } + + if (!anyCrafted && availableLogs.length > 0) { + // We had logs but couldn't craft anything? That's a problem. + throw new ActionError('CRAFTING_FAILED', 'Has logs but failed to craft planks', { availableLogs: availableLogs.map(l => l.name) }) + } } - return planksCount >= neededAmount -}; + if (planksCount >= neededAmount) { + return true + } + + throw new ActionError('RESOURCE_MISSING', 'Failed to ensure enough planks after retries', { needed: neededAmount, current: planksCount }) +} // Helper function to ensure a specific amount of sticks export async function ensureSticks(mineflayer: Mineflayer, neededAmount: number): Promise { @@ -118,141 +146,113 @@ export async function ensureSticks(mineflayer: Mineflayer, neededAmount: number) return true } - while (neededAmount >= sticksCount) { + const maxRetries = 2 + let retries = 0 + + while (neededAmount > sticksCount && retries < maxRetries) { + retries++ const planksCount = getItemCount(mineflayer, 'planks') const planksNeeded = Math.max( Math.ceil((neededAmount - sticksCount) / STICKS_PER_PLANK), - 4, + 2, // Minimum craft is usually 2 planks -> 4 sticks ) if (planksCount >= planksNeeded) { try { const sticksId = getItemId('stick') - const recipe = await mineflayer.bot.recipesFor(sticksId, null, 1, null)[0] - await mineflayer.bot.craft(recipe, neededAmount - sticksCount) + const recipe = mineflayer.bot.recipesFor(sticksId, null, 1, null)[0] + if (!recipe) { + throw new ActionError('CRAFTING_FAILED', 'No recipe for sticks found') + } + await mineflayer.bot.craft(recipe, Math.ceil((neededAmount - sticksCount) / 4)) // Crafting usually gives 4 sticks sticksCount = getItemCount(mineflayer, 'stick') - mineflayer.bot.chat(`I have made ${Math.abs(neededAmount - sticksCount)} sticks.`) - logger.log(`Bot: Sticks crafted.`) + mineflayer.bot.chat(`I have made sticks.`) } catch (err) { logger.withError(err).error('Bot: Failed to craft sticks.') - return false + throw new ActionError('CRAFTING_FAILED', 'Failed to craft sticks', { error: err }) } } else { await ensurePlanks(mineflayer, planksNeeded) - logger.error('Bot: Not enough planks for sticks.') } + sticksCount = getItemCount(mineflayer, 'stick') } - return sticksCount >= neededAmount + if (sticksCount >= neededAmount) return true + + throw new ActionError('RESOURCE_MISSING', 'Failed to ensure sticks', { needed: neededAmount, current: sticksCount }) } // Ensure a specific number of chests export async function ensureChests(mineflayer: Mineflayer, quantity: number = 1): Promise { logger.log(`Bot: Checking for ${quantity} chest(s)...`) - // Count the number of chests the bot already has let chestCount = getItemCount(mineflayer, 'chest') if (chestCount >= quantity) { - logger.log(`Bot: Already has ${quantity} or more chest(s).`) return true } - while (chestCount < quantity) { - const planksEnsured = await ensurePlanks(mineflayer, 8 * quantity) // 8 planks per chest - if (!planksEnsured) { - logger.error('Bot: Failed to ensure planks for chest(s).') - continue - } - - // Craft the chest(s) - const crafted = await craftRecipe(mineflayer, 'chest', quantity - chestCount) - if (crafted) { - chestCount = getItemCount(mineflayer, 'chest') - mineflayer.bot.chat(`I have crafted ${quantity} chest(s).`) - logger.log(`Bot: ${quantity} chest(s) crafted.`) - continue - } - else { - logger.error('Bot: Failed to craft chest(s).') - } + await ensurePlanks(mineflayer, 8 * (quantity - chestCount)) + const crafted = await craftRecipe(mineflayer, 'chest', quantity - chestCount) + + if (!crafted) { + throw new ActionError('CRAFTING_FAILED', 'Failed to craft chests') } - return chestCount >= quantity + + return true } // Ensure a specific number of furnaces export async function ensureFurnaces(mineflayer: Mineflayer, quantity: number = 1): Promise { logger.log(`Bot: Checking for ${quantity} furnace(s)...`) - // Count the number of furnaces the bot already has let furnaceCount = getItemCount(mineflayer, 'furnace') if (furnaceCount >= quantity) { - logger.log(`Bot: Already has ${quantity} or more furnace(s).`) return true } - while (furnaceCount < quantity) { - const stoneEnsured = await ensureCobblestone(mineflayer, 8 * (quantity - furnaceCount)) // 8 stone blocks per furnace - if (!stoneEnsured) { - logger.error('Bot: Failed to ensure stone for furnace(s).') - continue - } - - // Craft the furnace(s) - const crafted = await craftRecipe(mineflayer, 'furnace', quantity - furnaceCount) - if (crafted) { - furnaceCount = getItemCount(mineflayer, 'furnace') - mineflayer.bot.chat(`I have crafted ${quantity} furnace(s).`) - logger.log(`Bot: ${quantity} furnace(s) crafted.`) - continue - } - else { - logger.error('Bot: Failed to craft furnace(s).') - } + const stoneNeeded = 8 * (quantity - furnaceCount) + try { + await ensureCobblestone(mineflayer, stoneNeeded) + } catch (e) { + throw new ActionError('RESOURCE_MISSING', 'Failed to gather cobblestone for furnace', { error: e }) } - return furnaceCount >= quantity + + const crafted = await craftRecipe(mineflayer, 'furnace', quantity - furnaceCount) + if (!crafted) { + throw new ActionError('CRAFTING_FAILED', 'Failed to craft furnace') + } + + return true } // Ensure a specific number of torches export async function ensureTorches(mineflayer: Mineflayer, quantity: number = 1): Promise { logger.log(`Bot: Checking for ${quantity} torch(es)...`) - // Count the number of torches the bot already has let torchCount = getItemCount(mineflayer, 'torch') if (torchCount >= quantity) { - logger.log(`Bot: Already has ${quantity} or more torch(es).`) return true } - while (torchCount < quantity) { - const sticksEnsured = await ensureSticks(mineflayer, quantity - torchCount) // 1 stick per 4 torches - const coalEnsured = await ensureCoal( - mineflayer, - Math.ceil((quantity - torchCount) / 4), - ) // 1 coal per 4 torches - - if (!sticksEnsured || !coalEnsured) { - logger.error('Bot: Failed to ensure sticks or coal for torch(es).') - continue - } - - // Craft the torch(es) - const crafted = await craftRecipe(mineflayer, 'torch', quantity - torchCount) - if (crafted) { - torchCount = getItemCount(mineflayer, 'torch') - mineflayer.bot.chat(`I have crafted ${quantity} torch(es).`) - logger.log(`Bot: ${quantity} torch(es) crafted.`) - continue - } - else { - logger.error('Bot: Failed to craft torch(es).') - } + const needed = quantity - torchCount + await ensureSticks(mineflayer, Math.ceil(needed / 4)) + + try { + await ensureCoal(mineflayer, Math.ceil(needed / 4)) + } catch (e) { + throw new ActionError('RESOURCE_MISSING', 'Failed to gather coal for torches', { error: e }) } - return torchCount >= quantity + + const crafted = await craftRecipe(mineflayer, 'torch', Math.ceil(needed / 4)) + if (!crafted) { + throw new ActionError('CRAFTING_FAILED', 'Failed to craft torches') + } + return true } // Ensure a campfire @@ -261,38 +261,32 @@ export async function ensureCampfire(mineflayer: Mineflayer): Promise { logger.log('Bot: Checking for a campfire...') const hasCampfire = getItemCount(mineflayer, 'campfire') > 0 + if (hasCampfire) return true - if (hasCampfire) { - logger.log('Bot: Campfire is already available.') - return true - } - - const logsEnsured = await ensurePlanks(mineflayer, 3) // Need 3 logs for a campfire - const sticksEnsured = await ensureSticks(mineflayer, 3) // Need 3 sticks for a campfire - const coalEnsured = await ensureCoal(mineflayer, 1) // Need 1 coal or charcoal for a campfire - - if (!logsEnsured || !sticksEnsured || !coalEnsured) { - logger.error('Bot: Failed to ensure resources for campfire.') + await ensurePlanks(mineflayer, 3) + await ensureSticks(mineflayer, 3) + try { + await ensureCoal(mineflayer, 1) + } catch (e) { + throw new ActionError('RESOURCE_MISSING', 'Failed to gather coal/charcoal for campfire', { error: e }) } const crafted = await craftRecipe(mineflayer, 'campfire', 1) - if (crafted) { - mineflayer.bot.chat('I have crafted a campfire.') - logger.log('Bot: Campfire crafted.') - return true - } - else { - logger.error('Bot: Failed to craft campfire.') + if (!crafted) { + throw new ActionError('CRAFTING_FAILED', 'Failed to craft campfire') } - return hasCampfire + return true } // Helper function to gather cobblestone export async function ensureCobblestone(mineflayer: Mineflayer, requiredCobblestone: number, maxDistance: number = 4): Promise { let cobblestoneCount = getItemCount(mineflayer, 'cobblestone') + let retries = 0 + const maxRetries = 3 - while (cobblestoneCount < requiredCobblestone) { + while (cobblestoneCount < requiredCobblestone && retries < maxRetries) { + retries++ logger.log('Bot: Gathering more cobblestone...') const cobblestoneShortage = requiredCobblestone - cobblestoneCount @@ -304,57 +298,57 @@ export async function ensureCobblestone(mineflayer: Mineflayer, requiredCobblest maxDistance, ) if (!success) { - await moveAway(mineflayer, 30) + await moveAway(mineflayer, 10) continue } } - catch (err) { + catch (err: unknown) { if (err instanceof Error && err.message.includes('right tools')) { await ensurePickaxe(mineflayer) continue } else { - logger.withError(err).error('Error collecting cobblestone') - await moveAway(mineflayer, 30) - continue + throw new ActionError('RESOURCE_MISSING', 'Error collecting cobblestone', { error: err }) } } cobblestoneCount = getItemCount(mineflayer, 'cobblestone') } - logger.log('Bot: Collected enough cobblestone.') - return true + if (cobblestoneCount >= requiredCobblestone) return true + + throw new ActionError('RESOURCE_MISSING', 'Could not gather enough cobblestone', { required: requiredCobblestone, current: cobblestoneCount }) } export async function ensureCoal(mineflayer: Mineflayer, neededAmount: number, maxDistance: number = 4): Promise { logger.log('Bot: Checking for coal...') let coalCount = getItemCount(mineflayer, 'coal') + let retries = 0 + const maxRetries = 3 - while (coalCount < neededAmount) { + while (coalCount < neededAmount && retries < maxRetries) { + retries++ logger.log('Bot: Gathering more coal...') const coalShortage = neededAmount - coalCount try { - await collectBlock(mineflayer, 'stone', coalShortage, maxDistance) + await collectBlock(mineflayer, 'coal_ore', coalShortage, maxDistance) } - catch (err) { + catch (err: unknown) { if (err instanceof Error && err.message.includes('right tools')) { await ensurePickaxe(mineflayer) continue } else { - logger.withError(err).error('Error collecting cobblestone:') - moveAway(mineflayer, 30) - continue + throw new ActionError('RESOURCE_MISSING', 'Error collecting coal', { error: err }) } } - coalCount = getItemCount(mineflayer, 'cobblestone') + coalCount = getItemCount(mineflayer, 'coal') } - logger.log('Bot: Collected enough cobblestone.') - return true + if (coalCount >= neededAmount) return true + throw new ActionError('RESOURCE_MISSING', 'Could not gather enough coal', { required: neededAmount, current: coalCount }) } // Define the valid tool types as a union type @@ -391,71 +385,62 @@ export function materialsForTool(tool: ToolType): number { async function ensureTool(mineflayer: Mineflayer, toolType: ToolType, quantity: number = 1): Promise { logger.log(`Bot: Checking for ${quantity} ${toolType}(s)...`) - const neededMaterials = materialsForTool(toolType) - - // Check how many of the tool the bot currently has let toolCount = mineflayer.bot.inventory .items() .filter(item => item.name.includes(toolType)) .length if (toolCount >= quantity) { - logger.log(`Bot: Already has ${quantity} or more ${toolType}(s).`) return true } - while (toolCount < quantity) { - // Iterate over the tool materials from best (diamond) to worst (wooden) - for (const material of TOOLS_MATERIALS) { - const toolRecipe = `${material}_${toolType}` // Craft tool name like diamond_pickaxe, iron_sword - const hasResources = await hasResourcesForTool(mineflayer, material, neededMaterials) + // Iterate over the tool materials from best (diamond) to worst (wooden) + for (const material of TOOLS_MATERIALS) { + const toolRecipe = `${material}_${toolType}` + const neededMaterials = materialsForTool(toolType) + const hasResources = await hasResourcesForTool(mineflayer, material, neededMaterials) - // Check if we have enough material for the current tool - if (hasResources) { + if (hasResources) { + try { await ensureCraftingTable(mineflayer) - - const sticksEnsured = await ensureSticks(mineflayer, 2) - - if (!sticksEnsured) { - logger.error( - `Bot: Failed to ensure planks or sticks for wooden ${toolType}.`, - ) - continue - } - - // Craft the tool + await ensureSticks(mineflayer, 2) + const crafted = await craftRecipe(mineflayer, toolRecipe, 1) if (crafted) { toolCount++ - mineflayer.bot.chat( - `I have crafted a ${material} ${toolType}. Total ${toolType}(s): ${toolCount}/${quantity}`, - ) - logger.log( - `Bot: ${material} ${toolType} crafted. Total ${toolCount}/${quantity}`, - ) - if (toolCount >= quantity) - return true - } - else { - logger.error(`Bot: Failed to craft ${material} ${toolType}.`) + mineflayer.bot.chat(`I have crafted a ${material} ${toolType}.`) + if (toolCount >= quantity) return true } + } catch (err) { + if (err instanceof ActionError && err.code === 'RESOURCE_MISSING') { + // Just fall through to next material if resources missing + } else { + logger.error(`Failed to craft ${material} ${toolType}, trying next material.`) + } } - else if (material === 'wooden') { - // Crafting planks if we don't have enough resources for wooden tools - logger.log(`Bot: Crafting planks for ${material} ${toolType}...`) - await ensurePlanks(mineflayer, 4) + } else if (material === 'wooden') { + // Last resort: make wooden tools + // This will try to gather wood if needed, or throw if it fails + try { + await ensurePlanks(mineflayer, 4) + await ensureCraftingTable(mineflayer) + await ensureSticks(mineflayer, 2) + const crafted = await craftRecipe(mineflayer, `wooden_${toolType}`, 1) + if (crafted) return true + } catch (err) { + throw new ActionError('CRAFTING_FAILED', `Could not craft any ${toolType}`, { error: err }) } } } - return toolCount >= quantity + throw new ActionError('CRAFTING_FAILED', `Failed to ensure ${toolType} of any material`) } // Helper function to check if the bot has enough materials to craft a tool of a specific material export async function hasResourcesForTool( mineflayer: Mineflayer, material: MaterialType, - num = 3, // Number of resources needed for most tools + num = 3, ): Promise { switch (material) { case 'diamond': diff --git a/services/minecraft/src/skills/crafting.ts b/services/minecraft/src/skills/crafting.ts index 427f4a412..3fa1d5f86 100644 --- a/services/minecraft/src/skills/crafting.ts +++ b/services/minecraft/src/skills/crafting.ts @@ -4,6 +4,7 @@ import type { Recipe } from 'prismarine-recipe' import type { Mineflayer } from '../libs/mineflayer' +import { ActionError } from '../utils/errors' import { useLogger } from '../utils/logger' import { getItemId, getItemName } from '../utils/mcdata' import { ensureCraftingTable } from './actions/ensure' @@ -13,31 +14,6 @@ import { getInventoryCounts, getNearestBlock, getNearestFreeSpace } from './worl const logger = useLogger() -/* -Possible Scenarios: - -1. **Successful Craft Without Crafting Table**: - - The bot attempts to craft the item without a crafting table and succeeds. The function returns `true`. - -2. **Crafting Table Nearby**: - - The bot tries to craft without a crafting table but fails. - - The bot then checks for a nearby crafting table. - - If a crafting table is found, the bot moves to it and successfully crafts the item, returning `true`. - -3. **No Crafting Table Nearby, Place Crafting Table**: - - The bot fails to craft without a crafting table and does not find a nearby crafting table. - - The bot checks inventory for a crafting table, places it at a suitable location, and attempts crafting again. - - If successful, the function returns `true`. If the bot cannot find a suitable position or fails to craft, it returns `false`. - -4. **Insufficient Resources**: - - At any point, if the bot does not have the required resources to craft the item, it logs an appropriate message and returns `false`. - -5. **No Crafting Table and No Suitable Position**: - - If the bot does not find a crafting table and cannot find a suitable position to place one, it moves away and returns `false`. - -6. **Invalid Item Name**: - - If the provided item name is invalid, the function logs the error and returns `false`. -*/ export async function craftRecipe( mineflayer: Mineflayer, incomingItemName: string, @@ -50,8 +26,7 @@ export async function craftRecipe( const itemId = getItemId(itemName) if (itemId === null) { - logger.log(`Invalid item name: ${itemName}`) - return false + throw new ActionError('UNKNOWN', `Invalid item name: ${itemName}`) } // Helper function to attempt crafting @@ -71,8 +46,7 @@ export async function craftRecipe( return true } catch (err) { - logger.log(`Failed to craft ${itemName}: ${(err as Error).message}`) - return false + throw new ActionError('CRAFTING_FAILED', `Failed to craft ${itemName}`, { error: err }) } } return false @@ -95,6 +69,10 @@ export async function craftRecipe( 1, ) const recipes = mineflayer.bot.recipesFor(itemId, null, 1, craftingTable) + if (!recipes || recipes.length === 0) { + // If we have a crafting table but still no recipes, we are missing materials + return false // Let the caller decide or fall through + } success = await attemptCraft(recipes, craftingTable) } catch (err) { @@ -103,10 +81,15 @@ export async function craftRecipe( (err as Error).message }`, ) + if (err instanceof ActionError) throw err } attempts++ } + if (!success) { + throw new ActionError('NAVIGATION_FAILED', 'Could not reach crafting table') + } + return success } @@ -120,11 +103,8 @@ export async function craftRecipe( } logger.log(`No crafting table nearby, attempting to place one.`) - const hasCraftingTable = await ensureCraftingTable(mineflayer) - if (!hasCraftingTable) { - logger.log(`Failed to ensure a crafting table to craft ${itemName}.`) - return false - } + // valid: ensureCraftingTable now throws ActionError if it fails + await ensureCraftingTable(mineflayer) const pos = getNearestFreeSpace(mineflayer, 1, 10) if (pos) { @@ -139,9 +119,7 @@ export async function craftRecipe( } } else { - logger.log('No suitable position found to place the crafting table.') - moveAway(mineflayer, 5) - return false + throw new ActionError('CRAFTING_FAILED', 'No suitable position found to place the crafting table') } return false @@ -150,18 +128,35 @@ export async function craftRecipe( // Step 1: Try to craft without a crafting table logger.log(`Step 1: Try to craft without a crafting table`) const recipes = mineflayer.bot.recipesFor(itemId, null, 1, null) - if (recipes && (await attemptCraft(recipes))) { - return true + if (recipes && recipes.length > 0) { + // We have recipes without table + if (await attemptCraft(recipes)) { + return true + } + } + + // RECURSION GUARD: + // If we failed to craft basic items (planks, sticks) without a table, + // do NOT try to find a table. These items do not need a table. + // Seeking a table often triggers "ensureCraftingTable" -> "ensurePlanks" -> infinite loop. + if (itemName.includes('planks') || itemName === 'stick' || itemName === 'crafting_table') { + logger.log(`Recursion Guard: Skipping crafting table search for basic item: ${itemName}`) + throw new ActionError('RESOURCE_MISSING', `Cannot craft ${itemName} - missing ingredients`, { item: itemName }) } // Step 2: Find and use a crafting table + // This will throw if it fails hard logger.log(`Step 2: Find and use a crafting table`) const craftingTableRange = 32 if (await findAndUseCraftingTable(craftingTableRange)) { return true } - - return false + + // If we got here, maybe we didn't have recipes even with a table? + // Let's verify if resources are missing + // We can check recipes again assuming table is available (which we tried to ensure) + // Simple fallback: + throw new ActionError('RESOURCE_MISSING', `Cannot craft ${itemName}, possibly missing resources`, { item: itemName }) } export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = 1): Promise { @@ -176,11 +171,8 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = 'tropical_fish', ] if (!itemName.includes('raw') && !foods.includes(itemName)) { - logger.log( - `Cannot smelt ${itemName}, must be a "raw" item, like "raw_iron".`, - ) - return false - } // TODO: allow cobblestone, sand, clay, etc. + throw new ActionError('CRAFTING_FAILED', `Cannot smelt ${itemName}, must be a "raw" item`) + } let placedFurnace = false let furnaceBlock = getNearestBlock(mineflayer, 'furnace', 32) @@ -193,17 +185,16 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = await placeBlock(mineflayer, 'furnace', pos.x, pos.y, pos.z) } else { - logger.log('No suitable position found to place the furnace.') - return false + throw new ActionError('CRAFTING_FAILED', 'No suitable position found to place the furnace') } furnaceBlock = getNearestBlock(mineflayer, 'furnace', 32) placedFurnace = true } } if (!furnaceBlock) { - logger.log(`There is no furnace nearby and I have no furnace.`) - return false + throw new ActionError('RESOURCE_MISSING', 'There is no furnace nearby and I have no furnace to place') } + if (mineflayer.bot.entity.position.distanceTo(furnaceBlock.position) > 4) { await goToNearestBlock(mineflayer, 'furnace', 4, 32) } @@ -218,22 +209,15 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = && inputItem.type !== getItemId(itemName) && inputItem.count > 0 ) { - logger.log( - `The furnace is currently smelting ${getItemName( - inputItem.type, - )}.`, - ) - if (placedFurnace) - await collectBlock(mineflayer, 'furnace', 1) - return false + if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) + throw new ActionError('CRAFTING_FAILED', `The furnace is currently smelting ${getItemName(inputItem.type)}`) } + // Check if the bot has enough items to smelt const invCounts = getInventoryCounts(mineflayer) if (!invCounts[itemName] || invCounts[itemName] < num) { - logger.log(`I do not have enough ${itemName} to smelt.`) - if (placedFurnace) - await collectBlock(mineflayer, 'furnace', 1) - return false + if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) + throw new ActionError('RESOURCE_MISSING', `I do not have enough ${itemName} to smelt`, { required: num }) } // Fuel the furnace @@ -243,32 +227,38 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = .find(item => item.name === 'coal' || item.name === 'charcoal') const putFuel = Math.ceil(num / 8) if (!fuel || fuel.count < putFuel) { - logger.log( - `I do not have enough coal or charcoal to smelt ${num} ${itemName}, I need ${putFuel} coal or charcoal`, - ) - if (placedFurnace) - await collectBlock(mineflayer, 'furnace', 1) - return false + if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) + throw new ActionError('RESOURCE_MISSING', `I do not have enough coal or charcoal to smelt`, { required: putFuel }) } await furnace.putFuel(fuel.type, null, putFuel) - logger.log( - `Added ${putFuel} ${getItemName(fuel.type)} to furnace fuel.`, - ) } + // Put the items in the furnace const itemId = getItemId(itemName) if (itemId === null) { - logger.log(`Invalid item name: ${itemName}`) - return false + if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) + throw new ActionError('UNKNOWN', `Invalid item name: ${itemName}`) } await furnace.putInput(itemId, null, num) + // Wait for the items to smelt let total = 0 let collectedLast = true let smeltedItem: Item | null = null await new Promise(resolve => setTimeout(resolve, 200)) + // Wait limit 30s per item? + const maxWait = num * 12000 // approx 10s per item + buffer + let waited = 0 + while (total < num) { - await new Promise(resolve => setTimeout(resolve, 10000)) + await new Promise(resolve => setTimeout(resolve, 5000)) + waited += 5000 + + // Safety break + if (waited > maxWait) { + break + } + logger.log('checking...') let collected = false if (furnace.outputItem()) { @@ -279,7 +269,11 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = } } if (!collected && !collectedLast) { - break // if nothing was collected this time or last time + // If we didn't collect anything twice in a row, maybe it stopped? + // Check input + if (!furnace.inputItem() && !furnace.outputItem()) { + break // empty? + } } collectedLast = collected } @@ -288,16 +282,11 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = if (placedFurnace) { await collectBlock(mineflayer, 'furnace', 1) } - if (total === 0) { - logger.log(`Failed to smelt ${itemName}.`) - return false - } + if (total < num) { - logger.log( - `Only smelted ${total} ${getItemName(smeltedItem?.type || 0)}.`, - ) - return false + throw new ActionError('CRAFTING_FAILED', `Failed to smelt all items, only got ${total}/${num}`) } + logger.log( `Successfully smelted ${itemName}, got ${total} ${getItemName( smeltedItem?.type || 0, @@ -309,8 +298,7 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = export async function clearNearestFurnace(mineflayer: Mineflayer): Promise { const furnaceBlock = getNearestBlock(mineflayer, 'furnace', 6) if (!furnaceBlock) { - logger.log(`There is no furnace nearby.`) - return false + throw new ActionError('NAVIGATION_FAILED', 'No furnace nearby to clear') } logger.log('clearing furnace...') @@ -326,19 +314,7 @@ export async function clearNearestFurnace(mineflayer: Mineflayer): Promise + + constructor(code: ActionErrorCode, message: string, context?: Record) { + super(message) + this.name = 'ActionError' + this.code = code + this.context = context + } + + public toJSON() { + return { + message: this.message, + code: this.code, + context: this.context, + } + } +}