feat(minecraft): introduce structured ActionError for skill failures, implement fail-fast logic in planning, and remove unused planning code.
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<void> {
|
||||
// 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<void> {
|
||||
// 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<PlanStep[], void, unknown> {
|
||||
// // 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<boolean> {
|
||||
// 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<Plan> {
|
||||
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[] = []
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
@@ -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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
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<boolean> {
|
||||
switch (material) {
|
||||
case 'diamond':
|
||||
|
||||
@@ -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<boolean> {
|
||||
@@ -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<boolean> {
|
||||
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<boole
|
||||
inputItem = await furnace.takeInput()
|
||||
if (furnace.fuelItem())
|
||||
fuelItem = await furnace.takeFuel()
|
||||
logger.log(smeltedItem, inputItem, fuelItem)
|
||||
const smeltedName = smeltedItem
|
||||
? `${smeltedItem.count} ${smeltedItem.name}`
|
||||
: `0 smelted items`
|
||||
const inputName = inputItem
|
||||
? `${inputItem.count} ${inputItem.name}`
|
||||
: `0 input items`
|
||||
const fuelName = fuelItem
|
||||
? `${fuelItem.count} ${fuelItem.name}`
|
||||
: `0 fuel items`
|
||||
logger.log(
|
||||
`Cleared furnace, received ${smeltedName}, ${inputName}, and ${fuelName}.`,
|
||||
)
|
||||
|
||||
await mineflayer.bot.closeWindow(furnace)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
export type ActionErrorCode =
|
||||
| 'RESOURCE_MISSING'
|
||||
| 'CRAFTING_FAILED'
|
||||
| 'NAVIGATION_FAILED'
|
||||
| 'INTERRUPTED'
|
||||
| 'INVENTORY_FULL'
|
||||
| 'UNKNOWN'
|
||||
|
||||
export class ActionError extends Error {
|
||||
public readonly code: ActionErrorCode
|
||||
public readonly context?: Record<string, unknown>
|
||||
|
||||
constructor(code: ActionErrorCode, message: string, context?: Record<string, unknown>) {
|
||||
super(message)
|
||||
this.name = 'ActionError'
|
||||
this.code = code
|
||||
this.context = context
|
||||
}
|
||||
|
||||
public toJSON() {
|
||||
return {
|
||||
message: this.message,
|
||||
code: this.code,
|
||||
context: this.context,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user