Co-authored-by: RainbowBird <44741987+luoling8192@users.noreply.github.com>
This commit is contained in:
Neko Ayaka
2025-02-14 17:17:17 +08:00
co-authored by RainbowBird
57 changed files with 7220 additions and 0 deletions
@@ -0,0 +1,40 @@
import { messages, system, user } from 'neuri/openai'
import { beforeAll, describe, expect, it } from 'vitest'
import { initBot, useBot } from '../../composables/bot'
import { config, initEnv } from '../../composables/config'
import { createNeuriAgent } from '../../composables/neuri'
import { generateSystemBasicPrompt } from '../../libs/llm-agent/prompt'
import { initLogger } from '../../utils/logger'
describe('openAI agent', { timeout: 0 }, () => {
beforeAll(() => {
initLogger()
initEnv()
initBot({ botConfig: config.bot })
})
it('should initialize the agent', async () => {
const { bot } = useBot()
const agent = await createNeuriAgent(bot)
await new Promise<void>((resolve) => {
bot.bot.once('spawn', async () => {
const text = await agent.handle(
messages(
system(generateSystemBasicPrompt('airi')),
user('Hello, who are you?'),
),
async (c) => {
const completion = await c.reroute('query', c.messages, { model: config.openai.model })
return await completion?.firstContent()
},
)
expect(text?.toLowerCase()).toContain('airi')
resolve()
})
})
})
})
@@ -0,0 +1,84 @@
import type { Agent } from 'neuri'
import type { Message } from 'neuri/openai'
import type { Mineflayer } from '../../libs/mineflayer'
import type { PlanStep } from '../planning/adapter'
import { agent } from 'neuri'
import { system, user } from 'neuri/openai'
import { BaseLLMHandler } from '../../libs/llm-agent/handler'
import { useLogger } from '../../utils/logger'
import { actionsList } from './tools'
export async function createActionNeuriAgent(mineflayer: Mineflayer): Promise<Agent> {
const logger = useLogger()
logger.log('Initializing action agent')
let actionAgent = agent('action')
Object.values(actionsList).forEach((action) => {
actionAgent = actionAgent.tool(
action.name,
action.schema,
async ({ parameters }) => {
logger.withFields({ name: action.name, parameters }).log('Calling action')
mineflayer.memory.actions.push(action)
const fn = action.perform(mineflayer)
return await fn(...Object.values(parameters))
},
{ description: action.description },
)
})
return actionAgent.build()
}
export class ActionLLMHandler extends BaseLLMHandler {
public async executeStep(step: PlanStep): Promise<string> {
const systemPrompt = this.generateActionSystemPrompt()
const userPrompt = this.generateActionUserPrompt(step)
const messages = [system(systemPrompt), user(userPrompt)]
const result = await this.handleAction(messages)
return result
}
private generateActionSystemPrompt(): string {
return `You are a Minecraft bot action executor. Your task is to execute a given step using available tools.
You have access to various tools that can help you accomplish tasks.
When using a tool:
1. Choose the most appropriate tool for the task
2. Determine the correct parameters based on the context
3. Handle any errors or unexpected situations
Remember to:
- Be precise with tool parameters
- Consider the current state of the bot
- Handle failures gracefully`
}
private generateActionUserPrompt(step: PlanStep): string {
return `Execute this step: ${step.description}
Suggested tool: ${step.tool}
Params: ${JSON.stringify(step.params)}
Please use the appropriate tool with the correct parameters to accomplish this step.
If the suggested tool is not appropriate, you may choose a different one.`
}
public async handleAction(messages: Message[]): Promise<string> {
const result = await this.config.agent.handleStateless(messages, async (context) => {
this.logger.log('Processing action...')
const retryHandler = this.createRetryHandler(
async ctx => (await this.handleCompletion(ctx, 'action', ctx.messages)).content,
)
return await retryHandler(context)
})
if (!result) {
throw new Error('Failed to process action')
}
return result
}
}
@@ -0,0 +1,118 @@
import type { Mineflayer } from '../../libs/mineflayer'
import type { Action } from '../../libs/mineflayer/action'
import type { ActionAgent, AgentConfig } from '../../libs/mineflayer/base-agent'
import type { PlanStep } from '../planning/adapter'
import { useBot } from '../../composables/bot'
import { AbstractAgent } from '../../libs/mineflayer/base-agent'
import { actionsList } from './tools'
interface ActionState {
executing: boolean
label: string
startTime: number
}
/**
* ActionAgentImpl implements the ActionAgent interface to handle action execution
* Manages action lifecycle, state tracking and error handling
*/
export class ActionAgentImpl extends AbstractAgent implements ActionAgent {
public readonly type = 'action' as const
private actions: Map<string, Action>
private mineflayer: Mineflayer
private currentActionState: ActionState
constructor(config: AgentConfig) {
super(config)
this.actions = new Map()
this.mineflayer = useBot().bot
this.currentActionState = {
executing: false,
label: '',
startTime: 0,
}
}
protected async initializeAgent(): Promise<void> {
this.logger.log('Initializing action agent')
actionsList.forEach(action => this.actions.set(action.name, action))
// Set up event listeners
this.on('message', async ({ sender, message }) => {
await this.handleAgentMessage(sender, message)
})
}
protected async destroyAgent(): Promise<void> {
this.actions.clear()
this.removeAllListeners()
}
public async performAction(step: PlanStep): Promise<string> {
if (!this.initialized) {
throw new Error('Action agent not initialized')
}
const action = this.actions.get(step.tool)
if (!action) {
throw new Error(`Unknown action: ${step.tool}`)
}
this.logger.withFields({
action: step.tool,
description: step.description,
params: step.params,
}).log('Performing action')
// Update action state
this.updateActionState(true, step.description)
try {
// Execute action with provided parameters
const result = await action.perform(this.mineflayer)(...Object.values(step.params))
return this.formatActionOutput({
message: result,
timedout: false,
interrupted: false,
})
}
catch (error) {
this.logger.withError(error).error('Action failed')
throw error
}
finally {
this.updateActionState(false)
}
}
public getAvailableActions(): Action[] {
return Array.from(this.actions.values())
}
private async handleAgentMessage(sender: string, message: string): Promise<void> {
if (sender === 'system' && message.includes('interrupt') && this.currentActionState.executing) {
// Handle interruption
this.logger.log('Received interrupt request')
// Additional interrupt handling logic here
}
}
private updateActionState(executing: boolean, label = ''): void {
this.currentActionState = {
executing,
label,
startTime: executing ? Date.now() : this.currentActionState.startTime,
}
}
private formatActionOutput(result: { message: string | null, timedout: boolean, interrupted: boolean }): string {
if (result.timedout) {
return 'Action timed out'
}
if (result.interrupted) {
return 'Action was interrupted'
}
return result.message || 'Action completed successfully'
}
}
@@ -0,0 +1,61 @@
import { messages, system, user } from 'neuri/openai'
import { beforeAll, describe, expect, it } from 'vitest'
import { initBot, useBot } from '../../composables/bot'
import { config, initEnv } from '../../composables/config'
import { createNeuriAgent } from '../../composables/neuri'
import { generateActionAgentPrompt } from '../../libs/llm-agent/prompt'
import { sleep } from '../../utils/helper'
import { initLogger } from '../../utils/logger'
describe('actions agent', { timeout: 0 }, () => {
beforeAll(() => {
initLogger()
initEnv()
initBot({ botConfig: config.bot })
})
it('should choose right query command', async () => {
const { bot } = useBot()
const agent = await createNeuriAgent(bot)
await new Promise<void>((resolve) => {
bot.bot.once('spawn', async () => {
const text = await agent.handle(messages(
system(generateActionAgentPrompt(bot)),
user('What\'s your status?'),
), async (c) => {
const completion = await c.reroute('query', c.messages, { model: config.openai.model })
return await completion?.firstContent()
})
expect(text?.toLowerCase()).toContain('position')
resolve()
})
})
})
it('should choose right action command', async () => {
const { bot } = useBot()
const agent = await createNeuriAgent(bot)
await new Promise<void>((resolve) => {
bot.bot.on('spawn', async () => {
const text = await agent.handle(messages(
system(generateActionAgentPrompt(bot)),
user('goToPlayer: luoling8192'),
), async (c) => {
const completion = await c.reroute('action', c.messages, { model: config.openai.model })
return await completion?.firstContent()
})
expect(text).toContain('goToPlayer')
await sleep(10000)
resolve()
})
})
})
})
@@ -0,0 +1,540 @@
import type { Action } from '../../libs/mineflayer'
import { z } from 'zod'
import * as skills from '../../skills'
import { collectBlock } from '../../skills/actions/collect-block'
import { discard, equip, putInChest, takeFromChest, viewChest } from '../../skills/actions/inventory'
import { activateNearestBlock, placeBlock } from '../../skills/actions/world-interactions'
import * as world from '../../skills/world'
import { useLogger } from '../../utils/logger'
// Utils
const pad = (str: string): string => `\n${str}\n`
function formatInventoryItem(item: string, count: number): string {
return count > 0 ? `\n- ${item}: ${count}` : ''
}
function formatWearingItem(slot: string, item: string | undefined): string {
return item ? `\n${slot}: ${item}` : ''
}
export const actionsList: Action[] = [
{
name: 'stats',
description: 'Get your bot\'s location, health, hunger, and time of day.',
schema: z.object({}),
perform: mineflayer => (): string => {
const status = mineflayer.status.toOneLiner()
return status
},
},
{
name: 'inventory',
description: 'Get your bot\'s inventory.',
schema: z.object({}),
perform: mineflayer => (): string => {
const inventory = world.getInventoryCounts(mineflayer)
const items = Object.entries(inventory)
.map(([item, count]) => formatInventoryItem(item, count))
.join('')
const wearing = [
formatWearingItem('Head', mineflayer.bot.inventory.slots[5]?.name),
formatWearingItem('Torso', mineflayer.bot.inventory.slots[6]?.name),
formatWearingItem('Legs', mineflayer.bot.inventory.slots[7]?.name),
formatWearingItem('Feet', mineflayer.bot.inventory.slots[8]?.name),
].filter(Boolean).join('')
return pad(`INVENTORY${items || ': Nothing'}
${mineflayer.bot.game.gameMode === 'creative' ? '\n(You have infinite items in creative mode. You do not need to gather resources!!)' : ''}
WEARING: ${wearing || 'Nothing'}`)
},
},
{
name: 'nearbyBlocks',
description: 'Get the blocks near the bot.',
schema: z.object({}),
perform: mineflayer => (): string => {
const blocks = world.getNearbyBlockTypes(mineflayer)
useLogger().withFields({ blocks }).log('nearbyBlocks')
return pad(`NEARBY_BLOCKS${blocks.map((b: string) => `\n- ${b}`).join('') || ': none'}`)
},
},
{
name: 'craftable',
description: 'Get the craftable items with the bot\'s inventory.',
schema: z.object({}),
perform: mineflayer => (): string => {
const craftable = world.getCraftableItems(mineflayer)
return pad(`CRAFTABLE_ITEMS${craftable.map((i: string) => `\n- ${i}`).join('') || ': none'}`)
},
},
{
name: 'entities',
description: 'Get the nearby players and entities.',
schema: z.object({}),
perform: mineflayer => (): string => {
const players = world.getNearbyPlayerNames(mineflayer)
const entities = world.getNearbyEntityTypes(mineflayer)
.filter((e: string) => e !== 'player' && e !== 'item')
const result = [
...players.map((p: string) => `- Human player: ${p}`),
...entities.map((e: string) => `- entities: ${e}`),
]
return pad(`NEARBY_ENTITIES${result.length ? `\n${result.join('\n')}` : ': none'}`)
},
},
// getNewAction(): Action {
// return {
// name: 'newAction',
// description: 'Perform new and unknown custom behaviors that are not available as a command.',
// schema: z.object({
// prompt: z.string().describe('A natural language prompt to guide code generation. Make a detailed step-by-step plan.'),
// }),
// perform: (mineflayer: BotContext) => async (prompt: string) => {
// if (!settings.allow_insecure_coding)
// return 'newAction not allowed! Code writing is disabled in settings. Notify the user.'
// return await ctx.coder.generateCode(mineflayer.history)
// },
// }
// },
// todo: must 'stop now' can be used to stop the agent
{
name: 'stop',
description: 'Force stop all actions and commands that are currently executing.',
schema: z.object({}),
perform: mineflayer => async () => {
// await ctx.actions.stop()
// ctx.clearBotLogs()
// ctx.actions.cancelResume()
// ctx.bot.emit('idle')
mineflayer.emit('interrupt')
const msg = 'Agent stopped.'
// if (mineflayer.self_prompter.on)
// msg += ' Self-prompting still active.'
return msg
},
},
// getStfuAction(): Action {
// return {
// name: 'stfu',
// description: 'Stop all chatting and self prompting, but continue current action.',
// schema: z.object({}),
// perform: (mineflayer: BotContext) => async () => {
// ctx.openChat('Shutting up.')
// ctx.shutUp()
// return 'Shutting up.'
// },
// }
// },
// getRestartAction(): Action {
// return {
// name: 'restart',
// description: 'Restart the agent process.',
// schema: z.object({}),
// perform: (mineflayer: BotContext) => async () => {
// ctx.cleanKill()
// return 'Restarting agent...'
// },
// }
// },
// getClearChatAction(): Action {
// return {
// name: 'clearChat',
// description: 'Clear the chat history.',
// schema: z.object({}),
// perform: (mineflayer: BotContext) => async () => {
// ctx.history.clear()
// return `${ctx.name}'s chat history was cleared, starting new conversation from scratch.`
// },
// }
// },
{
name: 'goToPlayer',
description: 'Go to the given player.',
schema: z.object({
player_name: z.string().describe('The name of the player to go to.'),
closeness: z.number().describe('How close to get to the player.').min(0),
}),
perform: mineflayer => async (player_name: string, closeness: number) => {
await skills.goToPlayer(mineflayer, player_name, closeness)
return 'Moving to player...'
},
},
{
name: 'followPlayer',
description: 'Endlessly follow the given player.',
schema: z.object({
player_name: z.string().describe('name of the player to follow.'),
follow_dist: z.number().describe('The distance to follow from.').min(0),
}),
perform: mineflayer => async (player_name: string, follow_dist: number) => {
await skills.followPlayer(mineflayer, player_name, follow_dist)
return 'Following player...'
},
},
{
name: 'goToCoordinates',
description: 'Go to the given x, y, z location.',
schema: z.object({
x: z.number().describe('The x coordinate.'),
y: z.number().describe('The y coordinate.').min(-64).max(320),
z: z.number().describe('The z coordinate.'),
closeness: z.number().describe('How close to get to the location.').min(0),
}),
perform: mineflayer => async (x: number, y: number, z: number, closeness: number) => {
await skills.goToPosition(mineflayer, x, y, z, closeness)
return 'Moving to coordinates...'
},
},
{
name: 'searchForBlock',
description: 'Find and go to the nearest block of a given type in a given range.',
schema: z.object({
type: z.string().describe('The block type to go to.'),
search_range: z.number().describe('The range to search for the block.').min(32).max(512),
}),
perform: mineflayer => async (block_type: string, range: number) => {
await skills.goToNearestBlock(mineflayer, block_type, 4, range)
return 'Searching for block...'
},
},
{
name: 'searchForEntity',
description: 'Find and go to the nearest entity of a given type in a given range.',
schema: z.object({
type: z.string().describe('The type of entity to go to.'),
search_range: z.number().describe('The range to search for the entity.').min(32).max(512),
}),
perform: mineflayer => async (entity_type: string, range: number) => {
await skills.goToNearestEntity(mineflayer, entity_type, 4, range)
return 'Searching for entity...'
},
},
{
name: 'moveAway',
description: 'Move away from the current location in any direction by a given distance.',
schema: z.object({
distance: z.number().describe('The distance to move away.').min(0),
}),
perform: mineflayer => async (distance: number) => {
await skills.moveAway(mineflayer, distance)
return 'Moving away...'
},
},
{
name: 'givePlayer',
description: 'Give the specified item to the given player.',
schema: z.object({
player_name: z.string().describe('The name of the player to give the item to.'),
item_name: z.string().describe('The name of the item to give.'),
num: z.number().int().describe('The number of items to give.').min(1),
}),
perform: mineflayer => async (player_name: string, item_name: string, num: number) => {
await skills.giveToPlayer(mineflayer, item_name, player_name, num)
return 'Giving items to player...'
},
},
{
name: 'consume',
description: 'Eat/drink the given item.',
schema: z.object({
item_name: z.string().describe('The name of the item to consume.'),
}),
perform: mineflayer => async (item_name: string) => {
await skills.consume(mineflayer, item_name)
return 'Consuming item...'
},
},
{
name: 'equip',
description: 'Equip the given item.',
schema: z.object({
item_name: z.string().describe('The name of the item to equip.'),
}),
perform: mineflayer => async (item_name: string) => {
await equip(mineflayer, item_name)
return 'Equipping item...'
},
},
{
name: 'putInChest',
description: 'Put the given item in the nearest chest.',
schema: z.object({
item_name: z.string().describe('The name of the item to put in the chest.'),
num: z.number().int().describe('The number of items to put in the chest.').min(1),
}),
perform: mineflayer => async (item_name: string, num: number) => {
await putInChest(mineflayer, item_name, num)
return 'Putting items in chest...'
},
},
{
name: 'takeFromChest',
description: 'Take the given items from the nearest chest.',
schema: z.object({
item_name: z.string().describe('The name of the item to take.'),
num: z.number().int().describe('The number of items to take.').min(1),
}),
perform: mineflayer => async (item_name: string, num: number) => {
await takeFromChest(mineflayer, item_name, num)
return 'Taking items from chest...'
},
},
{
name: 'viewChest',
description: 'View the items/counts of the nearest chest.',
schema: z.object({}),
perform: mineflayer => async () => {
await viewChest(mineflayer)
return 'Viewing chest contents...'
},
},
{
name: 'discard',
description: 'Discard the given item from the inventory.',
schema: z.object({
item_name: z.string().describe('The name of the item to discard.'),
num: z.number().int().describe('The number of items to discard.').min(1),
}),
perform: mineflayer => async (item_name: string, num: number) => {
await discard(mineflayer, item_name, num)
return 'Discarding items...'
},
},
{
name: 'collectBlocks',
description: 'Collect the nearest blocks of a given type.',
schema: z.object({
type: z.string().describe('The block type to collect.'),
num: z.number().int().describe('The number of blocks to collect.').min(1),
}),
perform: mineflayer => async (type: string, num: number) => {
await collectBlock(mineflayer, type, num)
return 'Collecting blocks...'
},
},
{
name: 'craftRecipe',
description: 'Craft the given recipe a given number of times.',
schema: z.object({
recipe_name: z.string().describe('The name of the output item to craft.'),
num: z.number().int().describe('The number of times to craft the recipe. This is NOT the number of output items, as it may craft many more items depending on the recipe.').min(1),
}),
perform: mineflayer => async (recipe_name: string, num: number) => {
await skills.craftRecipe(mineflayer, recipe_name, num)
return 'Crafting items...'
},
},
{
name: 'smeltItem',
description: 'Smelt the given item the given number of times.',
schema: z.object({
item_name: z.string().describe('The name of the input item to smelt.'),
num: z.number().int().describe('The number of times to smelt the item.').min(1),
}),
perform: mineflayer => async (item_name: string, num: number) => {
await skills.smeltItem(mineflayer, item_name, num)
return 'Smelting items...'
},
},
{
name: 'clearFurnace',
description: 'Take all items out of the nearest furnace.',
schema: z.object({}),
perform: mineflayer => async () => {
await skills.clearNearestFurnace(mineflayer)
return 'Clearing furnace...'
},
},
{
name: 'placeHere',
description: 'Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.',
schema: z.object({
type: z.string().describe('The block type to place.'),
}),
perform: mineflayer => async (type: string) => {
const pos = mineflayer.bot.entity.position
await placeBlock(mineflayer, type, pos.x, pos.y, pos.z)
return 'Placing block...'
},
},
{
name: 'attack',
description: 'Attack and kill the nearest entity of a given type.',
schema: z.object({
type: z.string().describe('The type of entity to attack.'),
}),
perform: mineflayer => async (type: string) => {
await skills.attackNearest(mineflayer, type, true)
return 'Attacking entity...'
},
},
{
name: 'attackPlayer',
description: 'Attack a specific player until they die or run away. Remember this is just a game and does not cause real life harm.',
schema: z.object({
player_name: z.string().describe('The name of the player to attack.'),
}),
perform: mineflayer => async (player_name: string) => {
const player = mineflayer.bot.players[player_name]?.entity
if (!player) {
skills.log(mineflayer, `Could not find player ${player_name}.`)
return 'Player not found'
}
await skills.attackEntity(mineflayer, player, true)
return 'Attacking player...'
},
},
{
name: 'goToBed',
description: 'Go to the nearest bed and sleep.',
schema: z.object({}),
perform: mineflayer => async () => {
await skills.goToBed(mineflayer)
return 'Going to bed...'
},
},
{
name: 'activate',
description: 'Activate the nearest object of a given type.',
schema: z.object({
type: z.string().describe('The type of object to activate.'),
}),
perform: mineflayer => async (type: string) => {
await activateNearestBlock(mineflayer, type)
return 'Activating block...'
},
},
{
name: 'stay',
description: 'Stay in the current location no matter what. Pauses all modes.',
schema: z.object({
type: z.number().int().describe('The number of seconds to stay. -1 for forever.').min(-1),
}),
perform: mineflayer => async (seconds: number) => {
await skills.stay(mineflayer, seconds)
return 'Staying in place...'
},
},
// getSetModeAction(): Action {
// return {
// name: 'setMode',
// description: 'Set a mode to on or off. A mode is an automatic behavior that constantly checks and responds to the environment.',
// schema: z.object({
// mode_name: z.string().describe('The name of the mode to enable.'),
// on: z.boolean().describe('Whether to enable or disable the mode.'),
// }),
// perform: (mineflayer: BotContext) => async (mode_name: string, on: boolean) => {
// const modes = ctx.bot.modes
// if (!modes.exists(mode_name))
// return `Mode ${mode_name} does not exist.${modes.getDocs()}`
// if (modes.isOn(mode_name) === on)
// return `Mode ${mode_name} is already ${on ? 'on' : 'off'}.`
// modes.setOn(mode_name, on)
// return `Mode ${mode_name} is now ${on ? 'on' : 'off'}.`
// },
// }
// },
// getGoalAction(): Action {
// return {
// name: 'goal',
// description: 'Set a goal prompt to endlessly work towards with continuous self-prompting.',
// schema: z.object({
// selfPrompt: z.string().describe('The goal prompt.'),
// }),
// perform: (mineflayer: BotContext) => async (prompt: string) => {
// if (convoManager.inConversation()) {
// ctx.self_prompter.setPrompt(prompt)
// convoManager.scheduleSelfPrompter()
// }
// else {
// ctx.self_prompter.start(prompt)
// }
// return 'Goal set...'
// },
// }
// },
// getEndGoalAction(): Action {
// return {
// name: 'endGoal',
// description: 'Call when you have accomplished your goal. It will stop self-prompting and the current action.',
// schema: z.object({}),
// perform: (mineflayer: BotContext) => async () => {
// ctx.self_prompter.stop()
// convoManager.cancelSelfPrompter()
// return 'Self-prompting stopped.'
// },
// }
// },
// getStartConversationAction(): Action {
// return {
// name: 'startConversation',
// description: 'Start a conversation with a player. Use for bots only.',
// schema: z.object({
// player_name: z.string().describe('The name of the player to send the message to.'),
// message: z.string().describe('The message to send.'),
// }),
// perform: (mineflayer: BotContext) => async (player_name: string, message: string) => {
// if (!convoManager.isOtherAgent(player_name))
// return `${player_name} is not a bot, cannot start conversation.`
// if (convoManager.inConversation() && !convoManager.inConversation(player_name))
// convoManager.forceEndCurrentConversation()
// else if (convoManager.inConversation(player_name))
// ctx.history.add('system', `You are already in conversation with ${player_name}. Don't use this command to talk to them.`)
// convoManager.startConversation(player_name, message)
// },
// }
// },
// getEndConversationAction(): Action {
// return {
// name: 'endConversation',
// description: 'End the conversation with the given player.',
// schema: z.object({
// player_name: z.string().describe('The name of the player to end the conversation with.'),
// }),
// perform: (mineflayer: BotContext) => async (player_name: string) => {
// if (!convoManager.inConversation(player_name))
// return `Not in conversation with ${player_name}.`
// convoManager.endConversation(player_name)
// return `Converstaion with ${player_name} ended.`
// },
// }
// },
]
@@ -0,0 +1,67 @@
import type { ChatHistory } from './types'
import { system, user } from 'neuri/openai'
import { BaseLLMHandler } from '../../libs/llm-agent/handler'
export function generateChatAgentPrompt(): string {
return `You are a Minecraft bot assistant. Your task is to engage in natural conversation with players while helping them achieve their goals.
Guidelines:
1. Be friendly and helpful
2. Keep responses concise but informative
3. Use game-appropriate language
4. Acknowledge player's emotions and intentions
5. Ask for clarification when needed
6. Remember context from previous messages
7. Be proactive in suggesting helpful actions
You can:
- Answer questions about the game
- Help with tasks and crafting
- Give directions and suggestions
- Engage in casual conversation
- Coordinate with other bots
Remember that you're operating in a Minecraft world and should maintain that context in your responses.`
}
export class ChatLLMHandler extends BaseLLMHandler {
public async generateResponse(
message: string,
history: ChatHistory[],
): Promise<string> {
const systemPrompt = generateChatAgentPrompt()
const chatHistory = this.formatChatHistory(history, this.config.maxContextLength ?? 10)
const messages = [
system(systemPrompt),
...chatHistory,
user(message),
]
const result = await this.config.agent.handleStateless(messages, async (context) => {
this.logger.log('Generating response...')
const retryHandler = this.createRetryHandler(
async ctx => (await this.handleCompletion(ctx, 'chat', ctx.messages)).content,
)
return await retryHandler(context)
})
if (!result) {
throw new Error('Failed to generate response')
}
return result
}
private formatChatHistory(
history: ChatHistory[],
maxLength: number,
): Array<{ role: 'user' | 'assistant', content: string }> {
const recentHistory = history.slice(-maxLength)
return recentHistory.map(entry => ({
role: entry.sender === 'bot' ? 'assistant' : 'user',
content: entry.message,
}))
}
}
+168
View File
@@ -0,0 +1,168 @@
import type { ChatAgent } from '../../libs/mineflayer/base-agent'
import type { ChatAgentConfig, ChatContext } from './types'
import { AbstractAgent } from '../../libs/mineflayer/base-agent'
import { generateChatResponse } from './llm'
export class ChatAgentImpl extends AbstractAgent implements ChatAgent {
public readonly type = 'chat' as const
private activeChats: Map<string, ChatContext>
private maxHistoryLength: number
private idleTimeout: number
private llmConfig: ChatAgentConfig['llm']
constructor(config: ChatAgentConfig) {
super(config)
this.activeChats = new Map()
this.maxHistoryLength = config.maxHistoryLength ?? 50
this.idleTimeout = config.idleTimeout ?? 5 * 60 * 1000 // 5 minutes
this.llmConfig = config.llm
}
protected async initializeAgent(): Promise<void> {
this.logger.log('Initializing chat agent')
this.on('message', async ({ sender, message }) => {
await this.handleAgentMessage(sender, message)
})
setInterval(() => {
this.checkIdleChats()
}, 60 * 1000)
}
protected async destroyAgent(): Promise<void> {
this.activeChats.clear()
this.removeAllListeners()
}
public async processMessage(message: string, sender: string): Promise<string> {
if (!this.initialized) {
throw new Error('Chat agent not initialized')
}
this.logger.withFields({ sender, message }).log('Processing message')
try {
// Get or create chat context
const context = this.getOrCreateContext(sender)
// Add message to history
this.addToHistory(context, sender, message)
// Update last activity time
context.lastUpdate = Date.now()
// Generate response using LLM
const response = await this.generateResponse(message, context)
// Add response to history
this.addToHistory(context, this.id, response)
return response
}
catch (error) {
this.logger.withError(error).error('Failed to process message')
throw error
}
}
public startConversation(player: string): void {
if (!this.initialized) {
throw new Error('Chat agent not initialized')
}
this.logger.withField('player', player).log('Starting conversation')
const context = this.getOrCreateContext(player)
context.startTime = Date.now()
context.lastUpdate = Date.now()
}
public endConversation(player: string): void {
if (!this.initialized) {
throw new Error('Chat agent not initialized')
}
this.logger.withField('player', player).log('Ending conversation')
if (this.activeChats.has(player)) {
const context = this.activeChats.get(player)!
// Archive chat history if needed
this.archiveChat(context)
this.activeChats.delete(player)
}
}
private getOrCreateContext(player: string): ChatContext {
let context = this.activeChats.get(player)
if (!context) {
context = {
player,
startTime: Date.now(),
lastUpdate: Date.now(),
history: [],
}
this.activeChats.set(player, context)
}
return context
}
private addToHistory(context: ChatContext, sender: string, message: string): void {
context.history.push({
sender,
message,
timestamp: Date.now(),
})
// Trim history if too long
if (context.history.length > this.maxHistoryLength) {
context.history = context.history.slice(-this.maxHistoryLength)
}
}
private async generateResponse(message: string, context: ChatContext): Promise<string> {
return await generateChatResponse(message, context.history, {
agent: this.llmConfig.agent,
model: this.llmConfig.model,
maxContextLength: this.maxHistoryLength,
})
}
private checkIdleChats(): void {
const now = Date.now()
for (const [player, context] of this.activeChats.entries()) {
if (now - context.lastUpdate > this.idleTimeout) {
this.logger.withField('player', player).log('Ending idle conversation')
this.endConversation(player)
}
}
}
private async archiveChat(context: ChatContext): Promise<void> {
// Archive chat history to persistent storage if needed
this.logger.withFields({
player: context.player,
messageCount: context.history.length,
duration: Date.now() - context.startTime,
}).log('Archiving chat history')
}
private async handleAgentMessage(sender: string, message: string): Promise<void> {
if (sender === 'system') {
if (message.includes('interrupt')) {
// Handle system interrupt
for (const player of this.activeChats.keys()) {
this.endConversation(player)
}
}
}
else {
// Handle messages from other agents
const context = this.activeChats.get(sender)
if (context) {
await this.processMessage(message, sender)
}
}
}
}
+85
View File
@@ -0,0 +1,85 @@
import type { Agent, Neuri } from 'neuri'
import type { ChatHistory } from './types'
import { agent } from 'neuri'
import { system, user } from 'neuri/openai'
import { config as appConfig } from '../../composables/config'
import { toRetriable } from '../../utils/helper'
import { useLogger } from '../../utils/logger'
import { generateChatAgentPrompt } from './adapter'
interface LLMChatConfig {
agent: Neuri
model?: string
retryLimit?: number
delayInterval?: number
maxContextLength?: number
}
export async function createChatNeuriAgent(): Promise<Agent> {
return agent('chat').build()
}
export async function generateChatResponse(
message: string,
history: ChatHistory[],
config: LLMChatConfig,
): Promise<string> {
const systemPrompt = generateChatAgentPrompt()
const chatHistory = formatChatHistory(history, config.maxContextLength ?? 10)
const userPrompt = message
const logger = useLogger()
const messages = [
system(systemPrompt),
...chatHistory,
user(userPrompt),
]
const content = await config.agent.handleStateless(messages, async (c) => {
logger.log('Generating response...')
const handleCompletion = async (c: any): Promise<string> => {
const completion = await c.reroute('chat', c.messages, {
model: config.model ?? appConfig.openai.model,
})
if (!completion || 'error' in completion) {
logger.withFields(c).error('Completion failed')
throw new Error(completion?.error?.message ?? 'Unknown error')
}
const content = await completion.firstContent()
logger.withFields({ usage: completion.usage, content }).log('Response generated')
return content
}
const retriableHandler = toRetriable<any, string>(
config.retryLimit ?? 3,
config.delayInterval ?? 1000,
handleCompletion,
)
return await retriableHandler(c)
})
if (!content) {
throw new Error('Failed to generate response')
}
return content
}
function formatChatHistory(
history: ChatHistory[],
maxLength: number,
): Array<{ role: 'user' | 'assistant', content: string }> {
// Take the most recent messages up to maxLength
const recentHistory = history.slice(-maxLength)
return recentHistory.map(entry => ({
role: entry.sender === 'bot' ? 'assistant' : 'user',
content: entry.message,
}))
}
@@ -0,0 +1,25 @@
import type { Neuri } from 'neuri'
export interface ChatHistory {
sender: string
message: string
timestamp: number
}
export interface ChatContext {
player: string
startTime: number
lastUpdate: number
history: ChatHistory[]
}
export interface ChatAgentConfig {
id: string
type: 'chat'
llm: {
agent: Neuri
model?: string
}
maxHistoryLength?: number
idleTimeout?: number
}
@@ -0,0 +1,107 @@
import type { Message } from 'neuri/openai'
import type { Action } from '../../libs/mineflayer'
import type { AgentConfig, MemoryAgent } from '../../libs/mineflayer/base-agent'
import { Memory } from '../../libs/mineflayer/memory'
import { type Logger, useLogger } from '../../utils/logger'
export class MemoryAgentImpl implements MemoryAgent {
public readonly type = 'memory' as const
public readonly id: string
private memory: Map<string, unknown>
private initialized: boolean
private memoryInstance: Memory
private logger: Logger
constructor(config: AgentConfig) {
this.id = config.id
this.memory = new Map()
this.initialized = false
this.memoryInstance = new Memory()
this.logger = useLogger()
}
async init(): Promise<void> {
if (this.initialized) {
return
}
this.logger.log('Initializing memory agent')
this.initialized = true
}
async destroy(): Promise<void> {
this.memory.clear()
this.initialized = false
}
remember(key: string, value: unknown): void {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
this.logger.withFields({ key, value }).log('Storing memory')
this.memory.set(key, value)
}
recall<T>(key: string): T | undefined {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
const value = this.memory.get(key) as T | undefined
this.logger.withFields({ key, value }).log('Recalling memory')
return value
}
forget(key: string): void {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
this.logger.withFields({ key }).log('Forgetting memory')
this.memory.delete(key)
}
getMemorySnapshot(): Record<string, unknown> {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
return Object.fromEntries(this.memory.entries())
}
addChatMessage(message: Message): void {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
this.memoryInstance.chatHistory.push(message)
this.logger.withFields({ message }).log('Adding chat message to memory')
}
addAction(action: Action): void {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
this.memoryInstance.actions.push(action)
this.logger.withFields({ action }).log('Adding action to memory')
}
getChatHistory(): Message[] {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
return this.memoryInstance.chatHistory
}
getActions(): Action[] {
if (!this.initialized) {
throw new Error('Memory agent not initialized')
}
return this.memoryInstance.actions
}
}
@@ -0,0 +1,146 @@
import type { Agent } from 'neuri'
import type { Action } from '../../libs/mineflayer/action'
import { agent } from 'neuri'
import { system, user } from 'neuri/openai'
import { BaseLLMHandler } from '../../libs/llm-agent/handler'
export async function createPlanningNeuriAgent(): Promise<Agent> {
return agent('planning').build()
}
export interface PlanStep {
description: string
tool: string
params: Record<string, unknown>
}
export class PlanningLLMHandler extends BaseLLMHandler {
public async generatePlan(
goal: string,
availableActions: Action[],
sender: string,
feedback?: string,
): Promise<PlanStep[]> {
const systemPrompt = this.generatePlanningAgentSystemPrompt(availableActions)
const userPrompt = this.generatePlanningAgentUserPrompt(goal, sender, feedback)
const messages = [system(systemPrompt), user(userPrompt)]
const result = await this.config.agent.handleStateless(messages, async (context) => {
this.logger.log('Generating plan...')
const retryHandler = this.createRetryHandler(
async ctx => (await this.handleCompletion(ctx, 'planning', ctx.messages)).content,
)
return await retryHandler(context)
})
if (!result) {
throw new Error('Failed to generate plan')
}
return this.parsePlanContent(result)
}
private parsePlanContent(content: string): PlanStep[] {
// Split content into steps (numbered list)
const steps = content.split(/\d+\./).filter(step => step.trim().length > 0)
return steps.map((step) => {
const lines = step.trim().split('\n')
const description = lines[0].trim()
// Extract tool name and parameters
let tool = ''
const params: Record<string, unknown> = {}
for (const line of lines) {
const trimmed = line.trim()
// Extract tool name
if (trimmed.startsWith('Tool:')) {
tool = trimmed.split(':')[1].trim()
continue
}
// Extract parameters
if (trimmed === 'Params:') {
let i = lines.indexOf(line) + 1
while (i < lines.length) {
const paramLine = lines[i].trim()
if (paramLine === '')
break
const paramMatch = paramLine.match(/(\w+):\s*(.+)/)
if (paramMatch) {
const [, key, value] = paramMatch
// Try to parse numbers and booleans
if (value === 'true')
params[key] = true
else if (value === 'false')
params[key] = false
else if (/^\d+$/.test(value))
params[key] = Number.parseInt(value)
else if (/^\d*\.\d+$/.test(value))
params[key] = Number.parseFloat(value)
else params[key] = value.trim()
}
i++
}
}
}
return {
description,
tool,
params,
}
})
}
private generatePlanningAgentSystemPrompt(availableActions: Action[]): string {
const actionsList = availableActions
.map((action) => {
const params = Object.keys(action.schema.shape)
.map(name => ` - ${name}`)
.join('\n')
return `- ${action.name}: ${action.description}\n Parameters:\n${params}`
})
.join('\n\n')
return `You are a Minecraft bot planner. Break down goals into simple action steps.
Available tools:
${actionsList}
Format each step as:
1. Action description (short, direct command)
2. Tool name
3. Required parameters
Example:
1. Follow player
Tool: followPlayer
Params:
player: luoling8192
follow_dist: 3
Keep steps:
- Short and direct
- Action-focused
- Parameters precise
- Generate all steps at once`
}
private generatePlanningAgentUserPrompt(goal: string, sender: string, feedback?: string): string {
let prompt = `${sender}: ${goal}
Generate minimal steps with exact parameters.
Use the sender's name (${sender}) for player-related parameters.`
if (feedback) {
prompt += `\n\nPrevious attempt failed: ${feedback}`
}
return prompt
}
}
@@ -0,0 +1,650 @@
import type { Neuri } from 'neuri'
import type { Action } from '../../libs/mineflayer/action'
import type { ActionAgent, AgentConfig, MemoryAgent, Plan, PlanningAgent } from '../../libs/mineflayer/base-agent'
import type { PlanStep } from './adapter'
import { AbstractAgent } from '../../libs/mineflayer/base-agent'
import { ActionAgentImpl } from '../action'
import { PlanningLLMHandler } from './adapter'
interface PlanContext {
goal: string
currentStep: number
startTime: number
lastUpdate: number
retryCount: number
isGenerating: boolean
pendingSteps: PlanStep[]
}
export interface PlanningAgentConfig extends AgentConfig {
llm: {
agent: Neuri
model?: string
}
}
export class PlanningAgentImpl extends AbstractAgent implements PlanningAgent {
public readonly type = 'planning' as const
private currentPlan: Plan | null = null
private context: PlanContext | null = null
private actionAgent: ActionAgent | null = null
private memoryAgent: MemoryAgent | null = null
private llmConfig: PlanningAgentConfig['llm']
private llmHandler: PlanningLLMHandler
constructor(config: PlanningAgentConfig) {
super(config)
this.llmConfig = config.llm
this.llmHandler = new PlanningLLMHandler({
agent: this.llmConfig.agent,
model: this.llmConfig.model,
})
}
protected async initializeAgent(): Promise<void> {
this.logger.log('Initializing planning agent')
// Create action agent directly
this.actionAgent = new ActionAgentImpl({
id: 'action',
type: 'action',
})
await this.actionAgent.init()
// Set event listener
this.on('message', async ({ sender, message }) => {
await this.handleAgentMessage(sender, message)
})
this.on('interrupt', () => {
this.handleInterrupt()
})
}
protected async destroyAgent(): Promise<void> {
this.currentPlan = null
this.context = null
this.actionAgent = null
this.memoryAgent = null
this.removeAllListeners()
}
public async createPlan(goal: string): Promise<Plan> {
if (!this.initialized) {
throw new Error('Planning agent not initialized')
}
this.logger.withField('goal', goal).log('Creating plan')
try {
// Check memory for existing plan
const cachedPlan = await this.loadCachedPlan(goal)
if (cachedPlan) {
this.logger.log('Using cached plan')
return cachedPlan
}
// Get available actions from action agent
const availableActions = this.actionAgent?.getAvailableActions() ?? []
// Check if the goal requires actions
const requirements = this.parseGoalRequirements(goal)
const requiresAction = this.doesGoalRequireAction(requirements)
// If no actions needed, return empty plan
if (!requiresAction) {
this.logger.log('Goal does not require actions')
return {
goal,
steps: [],
status: 'completed',
requiresAction: false,
}
}
// Create plan steps based on available actions and goal
const steps = await this.generatePlanSteps(goal, availableActions, 'system')
// Create new plan
const plan: Plan = {
goal,
steps,
status: 'pending',
requiresAction: true,
}
// Cache the plan
await this.cachePlan(plan)
this.currentPlan = plan
this.context = {
goal,
currentStep: 0,
startTime: Date.now(),
lastUpdate: Date.now(),
retryCount: 0,
isGenerating: false,
pendingSteps: [],
}
return plan
}
catch (error) {
this.logger.withError(error).error('Failed to create plan')
throw error
}
}
public async executePlan(plan: Plan): Promise<void> {
if (!this.initialized) {
throw new Error('Planning agent not initialized')
}
if (!plan.requiresAction) {
this.logger.log('Plan does not require actions, skipping execution')
return
}
if (!this.actionAgent) {
throw new Error('Action agent not available')
}
this.logger.withField('plan', plan).log('Executing plan')
try {
plan.status = 'in_progress'
this.currentPlan = plan
// Execute each step
for (const step of plan.steps) {
try {
this.logger.withField('step', step).log('Executing step')
await this.actionAgent.performAction(step)
}
catch (stepError) {
this.logger.withError(stepError).error('Failed to execute step')
// Attempt to adjust plan and retry
if (this.context && this.context.retryCount < 3) {
this.context.retryCount++
// Adjust plan and restart
const adjustedPlan = await this.adjustPlan(
plan,
stepError instanceof Error ? stepError.message : 'Unknown error',
'system',
)
await this.executePlan(adjustedPlan)
return
}
throw stepError
}
}
plan.status = 'completed'
}
catch (error) {
plan.status = 'failed'
throw error
}
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')
}
this.logger.withFields({ plan, feedback }).log('Adjusting plan')
try {
// If there's a current context, use it to adjust the plan
if (this.context) {
const currentStep = this.context.currentStep
const availableActions = this.actionAgent?.getAvailableActions() ?? []
// Generate recovery steps based on feedback
const recoverySteps = this.generateRecoverySteps(feedback)
// Generate new steps from the current point
const newSteps = await this.generatePlanSteps(plan.goal, availableActions, sender, feedback)
// Create adjusted plan
const adjustedPlan: Plan = {
goal: plan.goal,
steps: [
...plan.steps.slice(0, currentStep),
...recoverySteps,
...newSteps,
],
status: 'pending',
requiresAction: true,
}
return adjustedPlan
}
// If no context, create a new plan
return this.createPlan(plan.goal)
}
catch (error) {
this.logger.withError(error).error('Failed to adjust plan')
throw error
}
}
// 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[] = []
if (feedback.includes('not found')) {
steps.push({
description: 'Search in a wider area',
tool: 'searchForBlock',
params: {
blockType: 'oak_log',
range: 64,
},
})
}
if (feedback.includes('inventory full')) {
steps.push({
description: 'Clear inventory space',
tool: 'discard',
params: {
blockType: 'oak_log',
count: 1,
},
})
}
if (feedback.includes('blocked') || feedback.includes('cannot reach')) {
steps.push({
description: 'Move away from obstacles',
tool: 'moveAway',
params: {
range: 64,
},
})
}
if (feedback.includes('too far')) {
steps.push({
description: 'Move closer to target',
tool: 'moveAway',
params: {
range: 64,
},
})
}
if (feedback.includes('need tool')) {
steps.push(
{
description: 'Craft a wooden pickaxe',
tool: 'craftRecipe',
params: {
recipe: 'oak_pickaxe',
},
},
{
description: 'Equip the wooden pickaxe',
tool: 'equip',
params: {
item: 'oak_pickaxe',
},
},
)
}
return steps
}
private async loadCachedPlan(goal: string): Promise<Plan | null> {
if (!this.memoryAgent)
return null
const cachedPlan = this.memoryAgent.recall<Plan>(`plan:${goal}`)
if (cachedPlan && this.isPlanValid(cachedPlan)) {
return cachedPlan
}
return null
}
private async cachePlan(plan: Plan): Promise<void> {
if (!this.memoryAgent)
return
this.memoryAgent.remember(`plan:${plan.goal}`, plan)
}
private isPlanValid(_plan: Plan): boolean {
// Add validation logic here
return true
}
private async handleAgentMessage(sender: string, message: string): Promise<void> {
if (sender === 'system') {
if (message.includes('interrupt')) {
this.handleInterrupt()
}
}
else {
// Process message and potentially adjust plan
this.logger.withFields({ sender, message }).log('Processing agent message')
// If there's a current plan, try to adjust it based on the message
if (this.currentPlan) {
await this.adjustPlan(this.currentPlan, message, sender)
}
}
}
private handleInterrupt(): void {
if (this.currentPlan) {
this.currentPlan.status = 'failed'
this.context = null
}
}
private doesGoalRequireAction(requirements: ReturnType<typeof this.parseGoalRequirements>): boolean {
// Check if any requirement indicates need for action
return requirements.needsItems
|| requirements.needsMovement
|| requirements.needsInteraction
|| requirements.needsCrafting
|| requirements.needsCombat
}
private async generatePlanSteps(
goal: string,
availableActions: Action[],
sender: string,
feedback?: string,
): Promise<PlanStep[]> {
// Generate all steps at once
this.logger.log('Generating plan using LLM')
return await this.llmHandler.generatePlan(goal, availableActions, sender, feedback)
}
private parseGoalRequirements(goal: string): {
needsItems: boolean
items?: string[]
needsMovement: boolean
location?: { x?: number, y?: number, z?: number }
needsInteraction: boolean
target?: string
needsCrafting: boolean
needsCombat: boolean
} {
const requirements = {
needsItems: false,
items: [] as string[],
needsMovement: false,
location: undefined as { x?: number, y?: number, z?: number } | undefined,
needsInteraction: false,
target: undefined as string | undefined,
needsCrafting: false,
needsCombat: false,
}
const goalLower = goal.toLowerCase()
// Extract items from goal
const itemMatches = goalLower.match(/(collect|get|find|craft|make|build|use|equip) (\w+)/g)
if (itemMatches) {
requirements.needsItems = true
requirements.items = itemMatches.map(match => match.split(' ')[1])
}
// Extract location from goal
const locationMatches = goalLower.match(/(go to|move to|at) (\d+)[, ]+(\d+)[, ]+(\d+)/g)
if (locationMatches) {
requirements.needsMovement = true
const [x, y, z] = locationMatches[0].split(/[, ]+/).slice(-3).map(Number)
requirements.location = { x, y, z }
}
// Extract target from goal
const targetMatches = goalLower.match(/(interact with|use|open|activate) (\w+)/g)
if (targetMatches) {
requirements.needsInteraction = true
requirements.target = targetMatches[0].split(' ').pop()
}
// Check for item-related actions
if (goalLower.includes('collect') || goalLower.includes('get') || goalLower.includes('find')) {
requirements.needsItems = true
requirements.needsMovement = true
}
// Check for movement-related actions
if (goalLower.includes('go to') || goalLower.includes('move to') || goalLower.includes('follow')) {
requirements.needsMovement = true
}
// Check for interaction-related actions
if (goalLower.includes('interact') || goalLower.includes('use') || goalLower.includes('open')) {
requirements.needsInteraction = true
}
// Check for crafting-related actions
if (goalLower.includes('craft') || goalLower.includes('make') || goalLower.includes('build')) {
requirements.needsCrafting = true
requirements.needsItems = true
}
// Check for combat-related actions
if (goalLower.includes('attack') || goalLower.includes('fight') || goalLower.includes('kill')) {
requirements.needsCombat = true
requirements.needsMovement = true
}
return requirements
}
}
+33
View File
@@ -0,0 +1,33 @@
import type { MineflayerOptions } from '../libs/mineflayer'
import { Mineflayer } from '../libs/mineflayer'
// Singleton instance of the Mineflayer bot
let botInstance: Mineflayer | null = null
/**
* Initialize a new Mineflayer bot instance.
* Follows singleton pattern to ensure only one bot exists at a time.
*/
export async function initBot(options: MineflayerOptions): Promise<{ bot: Mineflayer }> {
if (botInstance) {
throw new Error('Bot already initialized')
}
botInstance = await Mineflayer.asyncBuild(options)
return { bot: botInstance }
}
/**
* Get the current bot instance.
* Throws if bot is not initialized.
*/
export function useBot(): { bot: Mineflayer } {
if (!botInstance) {
throw new Error('Bot not initialized')
}
return {
bot: botInstance,
}
}
@@ -0,0 +1,87 @@
import type { BotOptions } from 'mineflayer'
import { env } from 'node:process'
import { useLogger } from '../utils/logger'
const logger = useLogger()
// Configuration interfaces
interface OpenAIConfig {
apiKey: string
baseUrl: string
model: string
reasoningModel: string
}
interface AiriConfig {
wsBaseUrl: string
clientName: string
}
interface Config {
openai: OpenAIConfig
bot: BotOptions
airi: AiriConfig
}
// Helper functions for type-safe environment variable parsing
function getEnvVar(key: string, defaultValue: string): string {
return env[key] || defaultValue
}
function getEnvNumber(key: string, defaultValue: number): number {
return Number.parseInt(env[key] || String(defaultValue))
}
// Default configurations
const defaultConfig: Config = {
openai: {
apiKey: '',
baseUrl: '',
model: '',
reasoningModel: '',
},
bot: {
username: 'airi-bot',
host: 'localhost',
port: 25565,
password: '',
version: '1.20',
},
airi: {
wsBaseUrl: 'ws://localhost:6121/ws',
clientName: 'minecraft-bot',
},
}
// Create a singleton config instance
export const config: Config = { ...defaultConfig }
// Initialize environment configuration
export function initEnv(): void {
logger.log('Initializing environment variables')
// Update config with environment variables
config.openai = {
apiKey: getEnvVar('OPENAI_API_KEY', defaultConfig.openai.apiKey),
baseUrl: getEnvVar('OPENAI_API_BASEURL', defaultConfig.openai.baseUrl),
model: getEnvVar('OPENAI_MODEL', defaultConfig.openai.model),
reasoningModel: getEnvVar('OPENAI_REASONING_MODEL', defaultConfig.openai.reasoningModel),
}
config.bot = {
username: getEnvVar('BOT_USERNAME', defaultConfig.bot.username as string),
host: getEnvVar('BOT_HOSTNAME', defaultConfig.bot.host as string),
port: getEnvNumber('BOT_PORT', defaultConfig.bot.port as number),
password: getEnvVar('BOT_PASSWORD', defaultConfig.bot.password as string),
version: getEnvVar('BOT_VERSION', defaultConfig.bot.version as string),
}
config.airi = {
wsBaseUrl: getEnvVar('AIRI_WS_BASEURL', defaultConfig.airi.wsBaseUrl),
clientName: getEnvVar('AIRI_CLIENT_NAME', defaultConfig.airi.clientName),
}
logger.withFields({ config }).log('Environment variables initialized')
}
@@ -0,0 +1,40 @@
import type { Agent, Neuri } from 'neuri'
import type { Mineflayer } from '../libs/mineflayer'
import { neuri } from 'neuri'
import { createActionNeuriAgent } from '../agents/action/adapter'
import { createChatNeuriAgent } from '../agents/chat/llm'
import { createPlanningNeuriAgent } from '../agents/planning/adapter'
import { useLogger } from '../utils/logger'
import { config } from './config'
let neuriAgent: Neuri | undefined
const agents = new Set<Agent | Promise<Agent>>()
export async function createNeuriAgent(mineflayer: Mineflayer): Promise<Neuri> {
useLogger().log('Initializing neuri agent')
let n = neuri()
agents.add(createPlanningNeuriAgent())
agents.add(createActionNeuriAgent(mineflayer))
agents.add(createChatNeuriAgent())
agents.forEach(agent => n = n.agent(agent))
neuriAgent = await n.build({
provider: {
apiKey: config.openai.apiKey,
baseURL: config.openai.baseUrl,
},
})
return neuriAgent
}
export function useNeuriAgent(): Neuri {
if (!neuriAgent) {
throw new Error('Agent not initialized')
}
return neuriAgent
}
@@ -0,0 +1,53 @@
import type { Neuri, NeuriContext } from 'neuri'
import type { Logger } from '../../utils/logger'
import type { MineflayerWithAgents } from './types'
import { system, user } from 'neuri/openai'
import { toRetriable } from '../../utils/helper'
import { handleLLMCompletion } from './completion'
import { generateStatusPrompt } from './prompt'
export async function handleChatMessage(username: string, message: string, bot: MineflayerWithAgents, agent: Neuri, logger: Logger): Promise<void> {
logger.withFields({ username, message }).log('Chat message received')
bot.memory.chatHistory.push(user(`${username}: ${message}`))
logger.log('thinking...')
try {
// Create and execute plan
const plan = await bot.planning.createPlan(message)
logger.withFields({ plan }).log('Plan created')
await bot.planning.executePlan(plan)
logger.log('Plan executed successfully')
// Generate response
// TODO: use chat agent and conversion manager
const statusPrompt = await generateStatusPrompt(bot)
const content = await agent.handleStateless(
[...bot.memory.chatHistory, system(statusPrompt)],
async (c: NeuriContext) => {
logger.log('handling response...')
return toRetriable<NeuriContext, string>(
3,
1000,
ctx => handleLLMCompletion(ctx, bot, logger),
{ onError: err => logger.withError(err).log('error occurred') },
)(c)
},
)
if (content) {
logger.withFields({ content }).log('responded')
bot.bot.chat(content)
}
}
catch (error) {
logger.withError(error).error('Failed to process message')
bot.bot.chat(
`Sorry, I encountered an error: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
)
}
}
@@ -0,0 +1,28 @@
import type { NeuriContext } from 'neuri'
import type { ChatCompletion } from 'neuri/openai'
import type { Logger } from '../../utils/logger'
import type { MineflayerWithAgents } from './types'
import { assistant } from 'neuri/openai'
import { config } from '../../composables/config'
export async function handleLLMCompletion(context: NeuriContext, bot: MineflayerWithAgents, logger: Logger): Promise<string> {
logger.log('rerouting...')
const completion = await context.reroute('action', context.messages, {
model: config.openai.model,
}) as ChatCompletion | { error: { message: string } } & ChatCompletion
if (!completion || 'error' in completion) {
logger.withFields({ completion }).error('Completion')
logger.withFields({ messages: context.messages }).log('messages')
return completion?.error?.message ?? 'Unknown error'
}
const content = await completion.firstContent()
logger.withFields({ usage: completion.usage, content }).log('output')
bot.memory.chatHistory.push(assistant(content))
return content
}
@@ -0,0 +1,70 @@
import type { Neuri } from 'neuri'
import type { Logger } from '../../utils/logger'
import { useLogg } from '@guiiai/logg'
import { asClass, asFunction, createContainer, InjectionMode } from 'awilix'
import { ActionAgentImpl } from '../../agents/action'
import { ChatAgentImpl } from '../../agents/chat'
import { PlanningAgentImpl } from '../../agents/planning'
export interface ContainerServices {
logger: Logger
actionAgent: ActionAgentImpl
planningAgent: PlanningAgentImpl
chatAgent: ChatAgentImpl
neuri: Neuri
}
export function createAgentContainer(options: {
neuri: Neuri
model?: string
}) {
const container = createContainer<ContainerServices>({
injectionMode: InjectionMode.PROXY,
strict: true,
})
// Register services
container.register({
// Create independent logger for each agent
logger: asFunction(() => useLogg('agent').useGlobalConfig()).singleton(),
// Register neuri client
neuri: asFunction(() => options.neuri).singleton(),
// Register agents
actionAgent: asClass(ActionAgentImpl)
.singleton()
.inject(() => ({
id: 'action',
type: 'action' as const,
})),
planningAgent: asClass(PlanningAgentImpl)
.singleton()
.inject(() => ({
id: 'planning',
type: 'planning' as const,
llm: {
agent: options.neuri,
model: options.model,
},
})),
chatAgent: asClass(ChatAgentImpl)
.singleton()
.inject(() => ({
id: 'chat',
type: 'chat' as const,
llm: {
agent: options.neuri,
model: options.model,
},
maxHistoryLength: 50,
idleTimeout: 5 * 60 * 1000, // 5 minutes
})),
})
return container
}
@@ -0,0 +1,46 @@
import type { NeuriContext } from 'neuri'
import type { ChatCompletion, Message } from 'neuri/openai'
import type { LLMConfig, LLMResponse } from './types'
import { config } from '../../composables/config'
import { toRetriable } from '../../utils/helper'
import { type Logger, useLogger } from '../../utils/logger'
export abstract class BaseLLMHandler {
protected logger: Logger
constructor(protected config: LLMConfig) {
this.logger = useLogger()
}
protected async handleCompletion(
context: NeuriContext,
route: string,
messages: Message[],
): Promise<LLMResponse> {
const completion = await context.reroute(route, messages, {
model: this.config.model ?? config.openai.model,
}) as ChatCompletion | ChatCompletion & { error: { message: string } }
if (!completion || 'error' in completion) {
this.logger.withFields(context).error('Completion failed')
throw new Error(completion?.error?.message ?? 'Unknown error')
}
const content = await completion.firstContent()
this.logger.withFields({ usage: completion.usage, content }).log('Generated content')
return {
content,
usage: completion.usage,
}
}
protected createRetryHandler<T>(handler: (context: NeuriContext) => Promise<T>) {
return toRetriable<NeuriContext, T>(
this.config.retryLimit ?? 3,
this.config.delayInterval ?? 1000,
handler,
)
}
}
@@ -0,0 +1,61 @@
import type { MineflayerPlugin } from '../mineflayer'
import type { LLMAgentOptions, MineflayerWithAgents } from './types'
import { system } from 'neuri/openai'
import { config } from '../../composables/config'
import { useLogger } from '../../utils/logger'
import { ChatMessageHandler } from '../mineflayer'
import { handleChatMessage } from './chat'
import { createAgentContainer } from './container'
import { generateActionAgentPrompt } from './prompt'
import { handleVoiceInput } from './voice'
export function LLMAgent(options: LLMAgentOptions): MineflayerPlugin {
return {
async created(bot) {
const logger = useLogger()
// Create container and get required services
const container = createAgentContainer({
neuri: options.agent,
model: config.openai.model,
})
const actionAgent = container.resolve('actionAgent')
const planningAgent = container.resolve('planningAgent')
const chatAgent = container.resolve('chatAgent')
// Initialize agents
await actionAgent.init()
await planningAgent.init()
await chatAgent.init()
// Type conversion
const botWithAgents = bot as unknown as MineflayerWithAgents
botWithAgents.action = actionAgent
botWithAgents.planning = planningAgent
botWithAgents.chat = chatAgent
// Initialize system prompt
bot.memory.chatHistory.push(system(generateActionAgentPrompt(bot)))
// Set message handling
const onChat = new ChatMessageHandler(bot.username).handleChat((username, message) =>
handleChatMessage(username, message, botWithAgents, options.agent, logger))
options.airiClient.onEvent('input:text:voice', event =>
handleVoiceInput(event, botWithAgents, options.agent, logger))
bot.bot.on('chat', onChat)
},
async beforeCleanup(bot) {
const botWithAgents = bot as unknown as MineflayerWithAgents
await botWithAgents.action?.destroy()
await botWithAgents.planning?.destroy()
await botWithAgents.chat?.destroy()
bot.bot.removeAllListeners('chat')
},
}
}
@@ -0,0 +1,50 @@
import type { Mineflayer } from '../mineflayer'
import { listInventory } from '../../skills/actions/inventory'
export async function generateStatusPrompt(mineflayer: Mineflayer): Promise<string> {
// Get inventory items
const inventory = await listInventory(mineflayer)
// Format inventory string
const inventoryStr = inventory.length === 0
? '[Empty]'
: inventory.map(item => `${item.name} x ${item.count}`).join(', ')
// Get currently held item
const itemInHand = inventory.length === 0
? '[Empty]'
: `${inventory[0].name} x ${inventory[0].count}` // TODO: mock
// Build status message
return [
'I will give you the following information:',
mineflayer.status.toOneLiner(),
'',
'Inventory:',
inventoryStr,
'',
'Item in hand:',
itemInHand,
].join('\n')
}
export function generateSystemBasicPrompt(botName: string): string {
// ${ctx.prompt.selfPrompt}
return `You are a playful Minecraft bot named ${botName} that can converse with players, see, move,
mine, build, and interact with the world by using commands.`
}
export function generateActionAgentPrompt(mineflayer: Mineflayer): string {
return `${generateSystemBasicPrompt(mineflayer.username)}
Act human-like as if you were a typical Minecraft player, rather than an AI. Be very brief in
your responses, don't apologize constantly, don't give instructions or make lists unless
asked, and don't refuse requests.
Do not use any emojis. Just call the function given you if needed.
- If I command you 'stop', then call the 'stop' function.
- If I require you to find something, then call the 'nearbyBlocks' function first, then call the 'searchForBlock' function.
`
}
@@ -0,0 +1,28 @@
import type { Client } from '@proj-airi/server-sdk'
import type { Neuri } from 'neuri'
import type { Mineflayer } from '../mineflayer'
import type { ActionAgent, ChatAgent, PlanningAgent } from '../mineflayer/base-agent'
export interface LLMConfig {
agent: Neuri
model?: string
retryLimit?: number
delayInterval?: number
maxContextLength?: number
}
export interface LLMResponse {
content: string
usage?: any
}
export interface MineflayerWithAgents extends Mineflayer {
planning: PlanningAgent
action: ActionAgent
chat: ChatAgent
}
export interface LLMAgentOptions {
agent: Neuri
airiClient: Client
}
@@ -0,0 +1,58 @@
import type { Neuri, NeuriContext } from 'neuri'
import type { Logger } from '../../utils/logger'
import type { MineflayerWithAgents } from './types'
import { system, user } from 'neuri/openai'
import { toRetriable } from '../../utils/helper'
import { handleLLMCompletion } from './completion'
import { generateStatusPrompt } from './prompt'
export async function handleVoiceInput(event: any, bot: MineflayerWithAgents, agent: Neuri, logger: Logger): Promise<void> {
logger
.withFields({
user: event.data.discord?.guildMember,
message: event.data.transcription,
})
.log('Chat message received')
const statusPrompt = await generateStatusPrompt(bot)
bot.memory.chatHistory.push(system(statusPrompt))
bot.memory.chatHistory.push(user(`NekoMeowww: ${event.data.transcription}`))
try {
// Create and execute plan
const plan = await bot.planning.createPlan(event.data.transcription)
logger.withFields({ plan }).log('Plan created')
await bot.planning.executePlan(plan)
logger.log('Plan executed successfully')
// Generate response
const retryHandler = toRetriable<NeuriContext, string>(
3,
1000,
ctx => handleLLMCompletion(ctx, bot, logger),
)
const content = await agent.handleStateless(
[...bot.memory.chatHistory, system(statusPrompt)],
async (c: NeuriContext) => {
logger.log('thinking...')
return retryHandler(c)
},
)
if (content) {
logger.withFields({ content }).log('responded')
bot.bot.chat(content)
}
}
catch (error) {
logger.withError(error).error('Failed to process message')
bot.bot.chat(
`Sorry, I encountered an error: ${
error instanceof Error ? error.message : 'Unknown error'
}`,
)
}
}
@@ -0,0 +1,11 @@
import type { z } from 'zod'
import type { Mineflayer } from './core'
type ActionResult = string | Promise<string>
export interface Action {
readonly name: string
readonly description: string
readonly schema: z.ZodObject<any>
readonly perform: (mineflayer: Mineflayer) => (...args: any[]) => ActionResult
}
@@ -0,0 +1,128 @@
import type { PlanStep } from '../../agents/planning/adapter'
import type { Logger } from '../../utils/logger'
import type { Action } from './action'
import { useLogg } from '@guiiai/logg'
import EventEmitter3 from 'eventemitter3'
export type AgentType = 'action' | 'memory' | 'planning' | 'chat'
export interface AgentConfig {
id: string
type: AgentType
}
export interface BaseAgent {
readonly id: string
readonly type: AgentType
init: () => Promise<void>
destroy: () => Promise<void>
}
export interface ActionAgent extends BaseAgent {
type: 'action'
performAction: (step: PlanStep) => Promise<string>
getAvailableActions: () => Action[]
}
export interface MemoryAgent extends BaseAgent {
type: 'memory'
remember: (key: string, value: unknown) => void
recall: <T>(key: string) => T | undefined
forget: (key: string) => void
getMemorySnapshot: () => Record<string, unknown>
}
export interface Plan {
goal: string
steps: PlanStep[]
status: 'pending' | 'in_progress' | 'completed' | 'failed'
requiresAction: boolean
}
export interface PlanningAgent extends BaseAgent {
type: 'planning'
createPlan: (goal: string) => Promise<Plan>
executePlan: (plan: Plan) => Promise<void>
adjustPlan: (plan: Plan, feedback: string, sender: string) => Promise<Plan>
}
export interface ChatAgent extends BaseAgent {
type: 'chat'
processMessage: (message: string, sender: string) => Promise<string>
startConversation: (player: string) => void
endConversation: (player: string) => void
}
export abstract class AbstractAgent extends EventEmitter3 implements BaseAgent {
public readonly id: string
public readonly type: AgentConfig['type']
public readonly name: string
protected initialized: boolean
protected logger: Logger
// protected actionManager: ReturnType<typeof useActionManager>
// protected conversationStore: ReturnType<typeof useConversationStore>
constructor(config: AgentConfig) {
super()
this.id = config.id // TODO: use uuid, is it needed?
this.type = config.type
this.name = `${this.type}-agent`
this.initialized = false
this.logger = useLogg(this.name).useGlobalConfig()
// Initialize managers
// this.actionManager = useActionManager(this)
// this.conversationStore = useConversationStore({
// agent: this,
// chatBotMessages: true,
// })
}
public async init(): Promise<void> {
if (this.initialized) {
return
}
await this.initializeAgent()
this.initialized = true
}
public async destroy(): Promise<void> {
if (!this.initialized) {
return
}
this.logger.log('Destroying agent')
await this.destroyAgent()
this.initialized = false
}
// Agent interface implementation
// public isIdle(): boolean {
// return !this.actionManager.executing
// }
public handleMessage(sender: string, message: string): void {
this.logger.withFields({ sender, message }).log('Received message')
this.emit('message', { sender, message })
}
public openChat(message: string): void {
this.logger.withField('message', message).log('Opening chat')
this.emit('chat', message)
}
// public clearBotLogs(): void {
// // Implement if needed
// }
public requestInterrupt(): void {
this.emit('interrupt')
}
// Methods to be implemented by specific agents
protected abstract initializeAgent(): Promise<void>
protected abstract destroyAgent(): Promise<void>
}
@@ -0,0 +1,13 @@
export interface CommandContext {
sender: string
isCommand: boolean
command: string
args: string[]
}
export function parseCommand(sender: string, message: string): CommandContext {
const isCommand = message.startsWith('#')
const command = message.split(' ')[0]
const args = message.split(' ').slice(1)
return { sender, isCommand, command, args }
}
@@ -0,0 +1,30 @@
import type { Logg } from '@guiiai/logg'
import type { Handler } from './types'
import { useLogger } from '../../utils/logger'
export class Components {
private components: Map<string, Handler> = new Map()
private logger: Logg
constructor() {
this.logger = useLogger()
}
register(componentName: string, component: Handler) {
this.components.set(componentName, component)
}
get(componentName: string) {
return this.components.get(componentName)
}
list() {
return Array.from(this.components.keys())
}
cleanup() {
this.logger.log('Cleaning up components')
this.components.clear()
}
}
@@ -0,0 +1,346 @@
import type { Logg } from '@guiiai/logg'
import type { Bot, BotOptions } from 'mineflayer'
import type { MineflayerPlugin } from './plugin'
import type { TickEvents, TickEventsHandler } from './ticker'
import type { EventHandlers, EventsHandler } from './types'
import { useLogg } from '@guiiai/logg'
import EventEmitter from 'eventemitter3'
import mineflayer from 'mineflayer'
import { parseCommand } from './command'
import { Components } from './components'
import { Health } from './health'
import { Memory } from './memory'
import { ChatMessageHandler } from './message'
import { Status } from './status'
import { Ticker } from './ticker'
export interface MineflayerOptions {
botConfig: BotOptions
plugins?: Array<MineflayerPlugin>
}
export class Mineflayer extends EventEmitter<EventHandlers> {
public bot: Bot
public username: string
public health: Health = new Health()
public ready: boolean = false
public components: Components = new Components()
public status: Status = new Status()
public memory: Memory = new Memory()
public isCreative: boolean = false
public allowCheats: boolean = false
private options: MineflayerOptions
private logger: Logg
private commands: Map<string, EventsHandler<'command'>> = new Map()
private ticker: Ticker = new Ticker()
constructor(options: MineflayerOptions) {
super()
this.options = options
this.bot = mineflayer.createBot(options.botConfig)
this.username = options.botConfig.username
this.logger = useLogg(`Bot:${this.username}`).useGlobalConfig()
this.on('interrupt', () => {
this.logger.log('Interrupted')
this.bot.chat('Interrupted')
})
}
public static async asyncBuild(options: MineflayerOptions) {
const mineflayer = new Mineflayer(options)
mineflayer.bot.on('messagestr', async (message, _, jsonMsg) => {
// jsonMsg.translate:
// - death.attack.player
// message:
// - <bot username> was slain by <player / entity>
// - <bot username> drowned
if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(mineflayer.username)) {
const deathPos = mineflayer.bot.entity.position
// mineflayer.memory_bank.rememberPlace('last_death_position', deathPos.x, deathPos.y, deathPos.z)
let deathPosStr: string | undefined
if (deathPos) {
deathPosStr = `x: ${deathPos.x.toFixed(2)}, y: ${deathPos.y.toFixed(2)}, z: ${deathPos.x.toFixed(2)}`
}
const dimension = mineflayer.bot.game.dimension
await mineflayer.handleMessage('system', `You died at position ${deathPosStr || 'unknown'} in the ${dimension} dimension with the final message: '${message}'. Your place of death has been saved as 'last_death_position' if you want to return. Previous actions were stopped and you have re-spawned.`)
}
})
mineflayer.bot.once('resourcePack', () => {
mineflayer.bot.acceptResourcePack()
})
mineflayer.bot.on('time', () => {
if (mineflayer.bot.time.timeOfDay === 0)
mineflayer.emit('time:sunrise', { time: mineflayer.bot.time.timeOfDay })
else if (mineflayer.bot.time.timeOfDay === 6000)
mineflayer.emit('time:noon', { time: mineflayer.bot.time.timeOfDay })
else if (mineflayer.bot.time.timeOfDay === 12000)
mineflayer.emit('time:sunset', { time: mineflayer.bot.time.timeOfDay })
else if (mineflayer.bot.time.timeOfDay === 18000)
mineflayer.emit('time:midnight', { time: mineflayer.bot.time.timeOfDay })
})
mineflayer.bot.on('health', () => {
mineflayer.logger.withFields({
health: mineflayer.health.value,
lastDamageTime: mineflayer.health.lastDamageTime,
lastDamageTaken: mineflayer.health.lastDamageTaken,
previousHealth: mineflayer.bot.health,
}).log('Health updated')
if (mineflayer.bot.health < mineflayer.health.value) {
mineflayer.health.lastDamageTime = Date.now()
mineflayer.health.lastDamageTaken = mineflayer.health.value - mineflayer.bot.health
}
mineflayer.health.value = mineflayer.bot.health
})
mineflayer.bot.once('spawn', () => {
mineflayer.ready = true
mineflayer.logger.log('Bot ready')
})
mineflayer.bot.on('death', () => {
mineflayer.logger.error('Bot died')
})
mineflayer.bot.on('kicked', (reason: string) => {
mineflayer.logger.withFields({ reason }).error('Bot was kicked')
})
mineflayer.bot.on('end', (reason) => {
mineflayer.logger.withFields({ reason }).log('Bot ended')
})
mineflayer.bot.on('error', (err: Error) => {
mineflayer.logger.errorWithError('Bot error:', err)
})
mineflayer.bot.on('spawn', () => {
mineflayer.bot.on('chat', mineflayer.handleCommand())
})
mineflayer.bot.on('spawn', async () => {
for (const plugin of options?.plugins || []) {
if (plugin.spawned) {
await plugin.spawned(mineflayer)
}
}
})
for (const plugin of options?.plugins || []) {
if (plugin.created) {
await plugin.created(mineflayer)
}
}
// Load Plugins
for (const plugin of options?.plugins || []) {
if (plugin.loadPlugin) {
mineflayer.bot.loadPlugin(await plugin.loadPlugin(mineflayer, mineflayer.bot, options.botConfig))
}
}
mineflayer.ticker.on('tick', () => {
mineflayer.status.update(mineflayer)
mineflayer.isCreative = mineflayer.bot.game?.gameMode === 'creative'
mineflayer.allowCheats = false
})
return mineflayer
}
public async loadPlugin(plugin: MineflayerPlugin) {
if (plugin.created)
await plugin.created(this)
if (plugin.loadPlugin) {
this.bot.loadPlugin(await plugin.loadPlugin(this, this.bot, this.options.botConfig))
}
if (plugin.spawned)
this.bot.once('spawn', () => plugin.spawned?.(this))
}
public onCommand(commandName: string, cb: EventsHandler<'command'>) {
this.commands.set(commandName, cb)
}
public onTick(event: TickEvents, cb: TickEventsHandler<TickEvents>) {
this.ticker.on(event, cb)
}
public async stop() {
for (const plugin of this.options?.plugins || []) {
if (plugin.beforeCleanup) {
await plugin.beforeCleanup(this)
}
}
this.components.cleanup()
this.bot.removeListener('chat', this.handleCommand())
this.bot.quit()
this.removeAllListeners()
}
private handleCommand() {
return new ChatMessageHandler(this.username).handleChat((sender, message) => {
const { isCommand, command, args } = parseCommand(sender, message)
if (!isCommand)
return
// Remove the # prefix from command
const cleanCommand = command.slice(1)
this.logger.withFields({ sender, command: cleanCommand, args }).log('Command received')
const handler = this.commands.get(cleanCommand)
if (handler) {
handler({ time: this.bot.time.timeOfDay, command: { sender, isCommand, command: cleanCommand, args } })
return
}
// Built-in commands
switch (cleanCommand) {
case 'help': {
const commandList = Array.from(this.commands.keys()).concat(['help'])
this.bot.chat(`Available commands: ${commandList.map(cmd => `#${cmd}`).join(', ')}`)
break
}
default:
this.bot.chat(`Unknown command: ${cleanCommand}`)
}
})
}
private async handleMessage(_source: string, _message: string, _maxResponses: number = Infinity) {
// if (!source || !message) {
// console.warn('Received empty message from', source);
// return false;
// }
// let used_command = false;
// if (maxResponses === null) {
// maxResponses = settings.max_commands === -1 ? Infinity : settings.max_commands;
// }
// if (maxResponses === -1) {
// maxResponses = Infinity;
// }
// const self_prompt = source === 'system' || source === ctx.botName;
// const from_other_bot = convoManager.isOtherAgent(source);
// if (!self_prompt && !from_other_bot) { // from user, check for forced commands
// const user_command_name = containsCommand(message);
// if (user_command_name) {
// if (!commandExists(user_command_name)) {
// this.routeResponse(source, `Command '${user_command_name}' does not exist.`);
// return false;
// }
// this.routeResponse(source, `*${source} used ${user_command_name.substring(1)}*`);
// if (user_command_name === '!newAction') {
// // all user-initiated commands are ignored by the bot except for this one
// // add the preceding message to the history to give context for newAction
// this.history.add(source, message);
// }
// let execute_res = await executeCommand(this, message);
// if (execute_res)
// this.routeResponse(source, execute_res);
// return true;
// }
// }
// if (from_other_bot)
// this.last_sender = source;
// // Now translate the message
// message = await handleEnglishTranslation(message);
// console.log('received message from', source, ':', message);
// const checkInterrupt = () => this.self_prompter.shouldInterrupt(self_prompt) || this.shut_up || convoManager.responseScheduledFor(source);
// let behavior_log = this.bot.modes.flushBehaviorLog();
// if (behavior_log.trim().length > 0) {
// const MAX_LOG = 500;
// if (behavior_log.length > MAX_LOG) {
// behavior_log = '...' + behavior_log.substring(behavior_log.length - MAX_LOG);
// }
// behavior_log = 'Recent behaviors log: \n' + behavior_log.substring(behavior_log.indexOf('\n'));
// await this.history.add('system', behavior_log);
// }
// // Handle other user messages
// await this.history.add(source, message);
// this.history.save();
// if (!self_prompt && this.self_prompter.on) // message is from user during self-prompting
// maxResponses = 1; // force only respond to this message, then let self-prompting take over
// for (let i=0; i<maxResponses; i++) {
// if (checkInterrupt()) break;
// let history = this.history.getHistory();
// let res = await this.prompter.promptConvo(history);
// console.log(`${this.name} full response to ${source}: ""${res}""`);
// if (res.trim().length === 0) {
// console.warn('no response')
// break; // empty response ends loop
// }
// let command_name = containsCommand(res);
// if (command_name) { // contains query or command
// res = truncCommandMessage(res); // everything after the command is ignored
// this.history.add(this.name, res);
// if (!commandExists(command_name)) {
// this.history.add('system', `Command ${command_name} does not exist.`);
// console.warn('Agent hallucinated command:', command_name)
// continue;
// }
// if (checkInterrupt()) break;
// this.self_prompter.handleUserPromptedCmd(self_prompt, isAction(command_name));
// if (settings.verbose_commands) {
// this.routeResponse(source, res);
// }
// else { // only output command name
// let pre_message = res.substring(0, res.indexOf(command_name)).trim();
// let chat_message = `*used ${command_name.substring(1)}*`;
// if (pre_message.length > 0)
// chat_message = `${pre_message} ${chat_message}`;
// this.routeResponse(source, chat_message);
// }
// let execute_res = await executeCommand(this, res);
// console.log('Agent executed:', command_name, 'and got:', execute_res);
// used_command = true;
// if (execute_res)
// this.history.add('system', execute_res);
// else
// break;
// }
// else { // conversation response
// this.history.add(this.name, res);
// this.routeResponse(source, res);
// break;
// }
// this.history.save();
// }
// return used_command;
}
}
@@ -0,0 +1,9 @@
export class Health {
public value: number
public lastDamageTime?: number
public lastDamageTaken?: number
constructor() {
this.value = 20
}
}
@@ -0,0 +1,11 @@
export * from './action'
export * from './command'
export * from './components'
export * from './core'
export * from './health'
export * from './memory'
export * from './message'
export * from './plugin'
export * from './status'
export * from './ticker'
export * from './types'
@@ -0,0 +1,12 @@
import type { Message } from 'neuri/openai'
import type { Action } from './action'
export class Memory {
public chatHistory: Message[]
public actions: Action[]
constructor() {
this.chatHistory = []
this.actions = []
}
}
@@ -0,0 +1,45 @@
import type { Entity } from 'prismarine-entity'
// Represents the context of a chat message in the Minecraft world
interface ChatMessage {
readonly sender: {
username: string
entity: Entity | null
}
readonly content: string
}
// Handles chat message validation and processing
export class ChatMessageHandler {
constructor(private readonly botUsername: string) {}
// Creates a new chat message context with validation
createMessageContext(entity: Entity | null, username: string, content: string): ChatMessage {
return {
sender: {
username,
entity,
},
content,
}
}
// Checks if a message is from the bot itself
isBotMessage(username: string): boolean {
return username === this.botUsername
}
// Checks if a message is a command
isCommand(content: string): boolean {
return content.startsWith('#')
}
// Processes chat messages, filtering out bot's own messages
handleChat(callback: (username: string, message: string) => void): (username: string, message: string) => void {
return (username: string, message: string) => {
if (!this.isBotMessage(username)) {
callback(username, message)
}
}
}
}
@@ -0,0 +1,15 @@
import type { Bot, BotOptions, Plugin } from 'mineflayer'
import type { Mineflayer } from '.'
export interface MineflayerPlugin {
created?: (mineflayer: Mineflayer) => void | Promise<void>
loadPlugin?: (mineflayer: Mineflayer, bot: Bot, options: BotOptions) => Plugin
spawned?: (mineflayer: Mineflayer) => void | Promise<void>
beforeCleanup?: (mineflayer: Mineflayer) => void | Promise<void>
}
export function wrapPlugin(plugin: Plugin): MineflayerPlugin {
return {
loadPlugin: () => (plugin),
}
}
@@ -0,0 +1,46 @@
import type { Mineflayer } from './core'
import type { OneLinerable } from './types'
export class Status implements OneLinerable {
public position: string
public health: string
public weather: string
public timeOfDay: string
constructor() {
this.position = ''
this.health = ''
this.weather = ''
this.timeOfDay = ''
}
public update(mineflayer: Mineflayer) {
if (!mineflayer.ready)
return
Object.assign(this, Status.from(mineflayer))
}
static from(mineflayer: Mineflayer): Status {
if (!mineflayer.ready)
return new Status()
const pos = mineflayer.bot.entity.position
const weather = mineflayer.bot.isRaining ? 'Rain' : mineflayer.bot.thunderState ? 'Thunderstorm' : 'Clear'
const timeOfDay = mineflayer.bot.time.timeOfDay < 6000
? 'Morning'
: mineflayer.bot.time.timeOfDay < 12000 ? 'Afternoon' : 'Night'
const status = new Status()
status.position = `x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)}`
status.health = `${Math.round(mineflayer.bot.health)} / 20`
status.weather = weather
status.timeOfDay = timeOfDay
return status
}
public toOneLiner(): string {
return Object.entries(this).map(([key, value]) => `${key}: ${value}`).join('\n')
}
}
@@ -0,0 +1,57 @@
import EventEmitter from 'eventemitter3'
export interface TickContext {
delta: number
nextTick: () => Promise<void>
}
export interface TickEventHandlers {
tick: (ctx: TickContext) => void
}
export type TickEvents = keyof TickEventHandlers
export type TickEventsHandler<K extends TickEvents> = TickEventHandlers[K]
// This update loop ensures that each update() is called one at a time, even if it takes longer than the interval
export class Ticker extends EventEmitter<TickEventHandlers> {
constructor(options?: { interval?: number }) {
super()
const { interval = 300 } = options ?? { interval: 300 }
let last = Date.now()
setTimeout(async () => {
while (true) {
const start = Date.now()
const nextTickPromise = new Promise<void>((resolve) => {
// Schedule nextTick resolution for after all callbacks complete
setImmediate(resolve)
})
// Run all callbacks without awaiting them
const callbackPromises = this.listeners('tick').map(cb => cb({
delta: start - last,
nextTick: () => nextTickPromise,
}))
// Wait for all callbacks to complete or timeout
await Promise.race([
Promise.all(callbackPromises),
new Promise(resolve =>
setTimeout(resolve, interval),
),
])
const remaining = interval - (Date.now() - start)
if (remaining > 0)
await new Promise(resolve => setTimeout(resolve, remaining))
last = start
}
}, interval)
}
on<K extends TickEvents>(event: K, cb: TickEventsHandler<K>) {
return super.on(event, cb)
}
}
@@ -0,0 +1,23 @@
import type { CommandContext } from './command'
export interface Context {
time: number
command?: CommandContext
}
export interface EventHandlers {
'interrupt': () => void
'command': (ctx: Context) => void | Promise<void>
'time:sunrise': (ctx: Context) => void
'time:noon': (ctx: Context) => void
'time:sunset': (ctx: Context) => void
'time:midnight': (ctx: Context) => void
}
export type Events = keyof EventHandlers
export type EventsHandler<K extends Events> = EventHandlers[K]
export type Handler = (ctx: Context) => void | Promise<void>
export interface OneLinerable {
toOneLiner: () => string
}
+52
View File
@@ -0,0 +1,52 @@
import process, { exit } from 'node:process'
import { Client } from '@proj-airi/server-sdk'
import MineflayerArmorManager from 'mineflayer-armor-manager'
import { loader as MineflayerAutoEat } from 'mineflayer-auto-eat'
import { plugin as MineflayerCollectBlock } from 'mineflayer-collectblock'
import { pathfinder as MineflayerPathfinder } from 'mineflayer-pathfinder'
import { plugin as MineflayerPVP } from 'mineflayer-pvp'
import { plugin as MineflayerTool } from 'mineflayer-tool'
import { initBot } from './composables/bot'
import { config, initEnv } from './composables/config'
import { createNeuriAgent } from './composables/neuri'
import { LLMAgent } from './libs/llm-agent'
import { wrapPlugin } from './libs/mineflayer'
import { initLogger, useLogger } from './utils/logger'
async function main() {
initLogger() // todo: save logs to file
initEnv()
const { bot } = await initBot({
botConfig: config.bot,
plugins: [
wrapPlugin(MineflayerArmorManager),
wrapPlugin(MineflayerAutoEat),
wrapPlugin(MineflayerCollectBlock),
wrapPlugin(MineflayerPathfinder),
wrapPlugin(MineflayerPVP),
wrapPlugin(MineflayerTool),
],
})
// Connect airi server
const airiClient = new Client({
name: config.airi.clientName,
url: config.airi.wsBaseUrl,
})
// Dynamically load LLMAgent after the bot is initialized
const agent = await createNeuriAgent(bot)
await bot.loadPlugin(LLMAgent({ agent, airiClient }))
process.on('SIGINT', () => {
bot.stop()
exit(0)
})
}
main().catch((err: Error) => {
useLogger().errorWithError('Fatal error', err)
exit(1)
})
+23
View File
@@ -0,0 +1,23 @@
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
import { ChatMessageHandler } from '../libs/mineflayer/message'
import { useLogger } from '../utils/logger'
export function Echo(): MineflayerPlugin {
const logger = useLogger()
return {
spawned(mineflayer) {
const onChatHandler = new ChatMessageHandler(mineflayer.username).handleChat((username, message) => {
logger.withFields({ username, message }).log('Chat message received')
mineflayer.bot.chat(message)
})
this.beforeCleanup = () => {
mineflayer.bot.removeListener('chat', onChatHandler)
}
mineflayer.bot.on('chat', onChatHandler)
},
}
}
+67
View File
@@ -0,0 +1,67 @@
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
import pathfinderModel from 'mineflayer-pathfinder'
import { useLogger } from '../utils/logger'
export function FollowCommand(options?: { rangeGoal: number }): MineflayerPlugin {
const logger = useLogger()
const { goals, Movements } = pathfinderModel
return {
created(bot) {
const state = {
following: undefined as string | undefined,
movements: new Movements(bot.bot),
}
function startFollow(username: string): void {
state.following = username
logger.withFields({ username }).log('Starting to follow player')
followPlayer()
}
function stopFollow(): void {
state.following = undefined
logger.log('Stopping follow')
bot.bot.pathfinder.stop()
}
function followPlayer(): void {
if (!state.following)
return
const target = bot.bot.players[state.following]?.entity
if (!target) {
bot.bot.chat('I lost sight of you!')
state.following = undefined
return
}
const { x: playerX, y: playerY, z: playerZ } = target.position
bot.bot.pathfinder.setMovements(state.movements)
bot.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, options?.rangeGoal ?? 1))
}
bot.onCommand('follow', (ctx) => {
const username = ctx.command!.sender
if (!username) {
bot.bot.chat('Please specify a player name!')
return
}
startFollow(username)
})
bot.onCommand('stop', () => {
stopFollow()
})
bot.onTick('tick', () => {
if (state.following)
followPlayer()
})
},
}
}
@@ -0,0 +1,41 @@
import type { Context } from '../libs/mineflayer'
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
import pathfinderModel from 'mineflayer-pathfinder'
import { useLogger } from '../utils/logger'
const { goals, Movements } = pathfinderModel
export function PathFinder(options?: { rangeGoal: number }): MineflayerPlugin {
return {
created(bot) {
const logger = useLogger()
let defaultMove: any
const handleCome = (commandCtx: Context) => {
const username = commandCtx.command!.sender
if (!username) {
bot.bot.chat('Please specify a player name!')
return
}
logger.withFields({ username }).log('Come command received')
const target = bot.bot.players[username]?.entity
if (!target) {
bot.bot.chat('I don\'t see that player!')
return
}
const { x: playerX, y: playerY, z: playerZ } = target.position
bot.bot.pathfinder.setMovements(defaultMove)
bot.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, options?.rangeGoal ?? 1))
}
defaultMove = new Movements(bot.bot)
bot.onCommand('come', handleCome)
},
}
}
+18
View File
@@ -0,0 +1,18 @@
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
import { useLogger } from '../utils/logger'
export function Status(): MineflayerPlugin {
return {
created(bot) {
const logger = useLogger()
logger.log('Loading status component')
bot.onCommand('status', () => {
logger.log('Status command received')
const status = bot.status.toOneLiner()
bot.bot.chat(status)
})
},
}
}
@@ -0,0 +1,176 @@
import type { Block } from 'prismarine-block'
import type { Mineflayer } from '../../libs/mineflayer'
import pathfinder from 'mineflayer-pathfinder'
import { useLogger } from '../../utils/logger'
import { breakBlockAt } from '../blocks'
import { getNearestBlocks } from '../world'
import { ensurePickaxe } from './ensure'
import { pickupNearbyItems } from './world-interactions'
const logger = useLogger()
function isMessagable(err: unknown): err is { message: string } {
return (err instanceof Error || (typeof err === 'object' && !!err && 'message' in err && typeof err.message === 'string'))
}
export async function collectBlock(
mineflayer: Mineflayer,
blockType: string,
num = 1,
range = 16,
): Promise<boolean> {
if (num < 1) {
logger.log(`Invalid number of blocks to collect: ${num}.`)
return false
}
const blockTypes = [blockType]
// Add block variants
if (
[
'coal',
'diamond',
'emerald',
'iron',
'gold',
'lapis_lazuli',
'redstone',
'copper',
].includes(blockType)
) {
blockTypes.push(`${blockType}_ore`, `deepslate_${blockType}_ore`)
}
if (blockType.endsWith('ore')) {
blockTypes.push(`deepslate_${blockType}`)
}
if (blockType === 'dirt') {
blockTypes.push('grass_block')
}
let collected = 0
while (collected < num) {
const blocks = getNearestBlocks(mineflayer, blockTypes, range)
if (blocks.length === 0) {
if (collected === 0)
logger.log(`No ${blockType} nearby to collect.`)
else logger.log(`No more ${blockType} nearby to collect.`)
break
}
const block = blocks[0]
try {
// Equip appropriate tool
if (mineflayer.bot.game.gameMode !== 'creative') {
await mineflayer.bot.tool.equipForBlock(block)
const itemId = mineflayer.bot.heldItem ? mineflayer.bot.heldItem.type : null
if (!block.canHarvest(itemId)) {
logger.log(`Don't have right tools to harvest ${block.name}.`)
if (block.name.includes('ore') || block.name.includes('stone')) {
await ensurePickaxe(mineflayer)
}
throw new Error('Don\'t have right tools to harvest block.')
}
}
// Implement vein mining
const veinBlocks = findVeinBlocks(mineflayer, block, 100, range, 1)
for (const veinBlock of veinBlocks) {
if (collected >= num)
break
// Move to the block using pathfinder
const goal = new pathfinder.goals.GoalGetToBlock(
veinBlock.position.x,
veinBlock.position.y,
veinBlock.position.z,
)
await mineflayer.bot.pathfinder.goto(goal)
// Break the block and collect drops
await mineAndCollect(mineflayer, veinBlock)
collected++
// Check if inventory is full
if (mineflayer.bot.inventory.emptySlotCount() === 0) {
logger.log('Inventory is full, cannot collect more items.')
break
}
}
}
catch (err) {
logger.log(`Failed to collect ${blockType}: ${err}.`)
if (isMessagable(err) && err.message.includes('Digging aborted')) {
break
}
continue
}
}
logger.log(`Collected ${collected} ${blockType}(s).`)
return collected > 0
}
// Helper function to mine a block and collect drops
async function mineAndCollect(mineflayer: Mineflayer, block: Block): Promise<void> {
// Break the block
await breakBlockAt(mineflayer, block.position.x, block.position.y, block.position.z)
// Use your existing function to pick up nearby items
await pickupNearbyItems(mineflayer, 5)
}
// Function to find connected blocks (vein mining)
function findVeinBlocks(
mineflayer: Mineflayer,
startBlock: Block,
maxBlocks = 100,
maxDistance = 16,
floodRadius = 1,
): Block[] {
const veinBlocks: Block[] = []
const visited = new Set<string>()
const queue: Block[] = [startBlock]
while (queue.length > 0 && veinBlocks.length < maxBlocks) {
const block = queue.shift()
if (!block)
continue
const key = block.position.toString()
if (visited.has(key))
continue
visited.add(key)
if (block.name !== startBlock.name)
continue
if (block.position.distanceTo(startBlock.position) > maxDistance)
continue
veinBlocks.push(block)
// Check neighboring blocks within floodRadius
for (let dx = -floodRadius; dx <= floodRadius; dx++) {
for (let dy = -floodRadius; dy <= floodRadius; dy++) {
for (let dz = -floodRadius; dz <= floodRadius; dz++) {
if (dx === 0 && dy === 0 && dz === 0)
continue // Skip the current block
const neighborPos = block.position.offset(dx, dy, dz)
const neighborBlock = mineflayer.bot.blockAt(neighborPos)
if (neighborBlock && !visited.has(neighborPos.toString())) {
queue.push(neighborBlock)
}
}
}
}
}
return veinBlocks
}
@@ -0,0 +1,500 @@
import type { Mineflayer } from '../../libs/mineflayer'
import { useLogger } from '../../utils/logger'
import { getItemId } from '../../utils/mcdata'
import { craftRecipe } from '../crafting'
import { moveAway } from '../movement'
import { collectBlock } from './collect-block'
import { gatherWood } from './gather-wood'
import { getItemCount } from './inventory'
// Constants for crafting and gathering
const PLANKS_PER_LOG = 4
const STICKS_PER_PLANK = 2
const logger = useLogger()
// Helper function to ensure a crafting table
export async function ensureCraftingTable(mineflayer: Mineflayer): Promise<boolean> {
logger.log('Bot: Checking for a crafting table...')
let hasCraftingTable = getItemCount(mineflayer, 'crafting_table') > 0
if (hasCraftingTable) {
logger.log('Bot: Crafting table is available.')
return true
}
while (!hasCraftingTable) {
const planksEnsured = await ensurePlanks(mineflayer, 4)
if (!planksEnsured) {
logger.error('Bot: Failed to ensure planks.')
continue
}
// Craft crafting table
hasCraftingTable = await craftRecipe(mineflayer, 'crafting_table', 1)
if (hasCraftingTable) {
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 hasCraftingTable
}
// Helper function to ensure a specific amount of planks
export async function ensurePlanks(mineflayer: Mineflayer, neededAmount: number): Promise<boolean> {
logger.log('Bot: Checking for planks...')
let planksCount = getItemCount(mineflayer, 'planks')
if (neededAmount < planksCount) {
logger.log('Bot: Have enough planks.')
return true
}
while (neededAmount > planksCount) {
const logsNeeded = Math.ceil((neededAmount - planksCount) / PLANKS_PER_LOG)
// Get all available log types in inventory
const availableLogs = mineflayer.bot.inventory
.items()
.filter(item => item.name.includes('log'))
// If no logs available, gather more wood
if (availableLogs.length === 0) {
await gatherWood(mineflayer, logsNeeded, 80)
logger.error('Bot: Not enough logs for planks.')
continue
}
// Iterate over each log type to craft planks
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}`)
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.`)
}
else {
logger.error(`Bot: Failed to craft ${logType} planks.`)
return false
}
// Check if we have enough planks after crafting
if (planksCount >= neededAmount)
break
}
}
return planksCount >= neededAmount
};
// Helper function to ensure a specific amount of sticks
export async function ensureSticks(mineflayer: Mineflayer, neededAmount: number): Promise<boolean> {
logger.log('Bot: Checking for sticks...')
let sticksCount = getItemCount(mineflayer, 'stick')
if (neededAmount <= sticksCount) {
logger.log('Bot: Have enough sticks.')
return true
}
while (neededAmount >= sticksCount) {
const planksCount = getItemCount(mineflayer, 'planks')
const planksNeeded = Math.max(
Math.ceil((neededAmount - sticksCount) / STICKS_PER_PLANK),
4,
)
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)
sticksCount = getItemCount(mineflayer, 'stick')
mineflayer.bot.chat(`I have made ${Math.abs(neededAmount - sticksCount)} sticks.`)
logger.log(`Bot: Sticks crafted.`)
}
catch (err) {
logger.withError(err).error('Bot: Failed to craft sticks.')
return false
}
}
else {
await ensurePlanks(mineflayer, planksNeeded)
logger.error('Bot: Not enough planks for sticks.')
}
}
return sticksCount >= neededAmount
}
// 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).')
}
}
return chestCount >= quantity
}
// 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).')
}
}
return furnaceCount >= quantity
}
// 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).')
}
}
return torchCount >= quantity
}
// Ensure a campfire
// Todo: rework
export async function ensureCampfire(mineflayer: Mineflayer): Promise<boolean> {
logger.log('Bot: Checking for a campfire...')
const hasCampfire = getItemCount(mineflayer, 'campfire') > 0
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.')
}
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.')
}
return hasCampfire
}
// Helper function to gather cobblestone
export async function ensureCobblestone(mineflayer: Mineflayer, requiredCobblestone: number, maxDistance: number = 4): Promise<boolean> {
let cobblestoneCount = getItemCount(mineflayer, 'cobblestone')
while (cobblestoneCount < requiredCobblestone) {
logger.log('Bot: Gathering more cobblestone...')
const cobblestoneShortage = requiredCobblestone - cobblestoneCount
try {
const success = await collectBlock(
mineflayer,
'stone',
cobblestoneShortage,
maxDistance,
)
if (!success) {
await moveAway(mineflayer, 30)
continue
}
}
catch (err) {
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
}
}
cobblestoneCount = getItemCount(mineflayer, 'cobblestone')
}
logger.log('Bot: Collected enough cobblestone.')
return true
}
export async function ensureCoal(mineflayer: Mineflayer, neededAmount: number, maxDistance: number = 4): Promise<boolean> {
logger.log('Bot: Checking for coal...')
let coalCount = getItemCount(mineflayer, 'coal')
while (coalCount < neededAmount) {
logger.log('Bot: Gathering more coal...')
const coalShortage = neededAmount - coalCount
try {
await collectBlock(mineflayer, 'stone', coalShortage, maxDistance)
}
catch (err) {
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
}
}
coalCount = getItemCount(mineflayer, 'cobblestone')
}
logger.log('Bot: Collected enough cobblestone.')
return true
}
// Define the valid tool types as a union type
type ToolType = 'pickaxe' | 'sword' | 'axe' | 'shovel' | 'hoe'
// Define the valid materials as a union type
type MaterialType = 'diamond' | 'golden' | 'iron' | 'stone' | 'wooden'
// Constants for crafting tools
const TOOLS_MATERIALS: MaterialType[] = [
'diamond',
'golden',
'iron',
'stone',
'wooden',
]
export function materialsForTool(tool: ToolType): number {
switch (tool) {
case 'pickaxe':
case 'axe':
return 3
case 'sword':
case 'hoe':
return 2
case 'shovel':
return 1
default:
return 0
}
}
// Helper function to ensure a specific tool, checking from best materials to wood
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)
// Check if we have enough material for the current tool
if (hasResources) {
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
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}.`)
}
}
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)
}
}
}
return toolCount >= quantity
}
// 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
): Promise<boolean> {
switch (material) {
case 'diamond':
return getItemCount(mineflayer, 'diamond') >= num
case 'golden':
return getItemCount(mineflayer, 'gold_ingot') >= num
case 'iron':
return getItemCount(mineflayer, 'iron_ingot') >= num
case 'stone':
return getItemCount(mineflayer, 'cobblestone') >= num
case 'wooden':
return getItemCount(mineflayer, 'planks') >= num
default:
return false
}
}
// Helper functions for specific tools:
// Ensure a pickaxe
export async function ensurePickaxe(mineflayer: Mineflayer, quantity: number = 1): Promise<boolean> {
return await ensureTool(mineflayer, 'pickaxe', quantity)
};
// Ensure a sword
export async function ensureSword(mineflayer: Mineflayer, quantity: number = 1): Promise<boolean> {
return await ensureTool(mineflayer, 'sword', quantity)
};
// Ensure an axe
export async function ensureAxe(mineflayer: Mineflayer, quantity: number = 1): Promise<boolean> {
return await ensureTool(mineflayer, 'axe', quantity)
};
// Ensure a shovel
export async function ensureShovel(mineflayer: Mineflayer, quantity: number = 1): Promise<boolean> {
return await ensureTool(mineflayer, 'shovel', quantity)
};
export async function ensureHoe(mineflayer: Mineflayer, quantity: number = 1): Promise<boolean> {
return await ensureTool(mineflayer, 'hoe', quantity)
};
@@ -0,0 +1,101 @@
import type { Mineflayer } from '../../libs/mineflayer'
import { sleep } from '../../utils/helper'
import { useLogger } from '../../utils/logger'
import { breakBlockAt } from '../blocks'
import { goToPosition, moveAway } from '../movement'
import { getNearestBlocks } from '../world'
import { pickupNearbyItems } from './world-interactions'
const logger = useLogger()
/**
* Gather wood blocks nearby to collect logs.
*
* @param mineflayer The mineflayer instance.
* @param num The number of wood logs to gather.
* @param maxDistance The maximum distance to search for wood blocks.
* @returns Whether the wood gathering was successful.
*/
export async function gatherWood(
mineflayer: Mineflayer,
num: number,
maxDistance = 64,
): Promise<boolean> {
logger.log(`Gathering wood... I need to collect ${num} logs.`)
mineflayer.bot.chat(`Gathering wood... I need to collect ${num} logs.`)
try {
let logsCount = getLogsCount(mineflayer)
logger.log(`I currently have ${logsCount} logs.`)
while (logsCount < num) {
// Gather 1 extra log to account for any failures
logger.log(`Looking for wood blocks nearby...`, logsCount, num)
const woodBlock = mineflayer.bot.findBlock({
matching: block => block.name.includes('log'),
maxDistance,
})
if (!woodBlock) {
logger.log('No wood blocks found nearby.')
await moveAway(mineflayer, 50)
continue
}
const destinationReached = await goToPosition(
mineflayer,
woodBlock.position.x,
woodBlock.position.y,
woodBlock.position.z,
2,
)
if (!destinationReached) {
logger.log('Unable to reach the wood block.')
continue // Try finding another wood block
}
const aTree = await getNearestBlocks(mineflayer, woodBlock.name, 4, 4)
if (aTree.length === 0) {
logger.log('No wood blocks found nearby.')
await moveAway(mineflayer, 15)
continue
}
try {
for (const aLog of aTree) {
await breakBlockAt(mineflayer, aLog.position.x, aLog.position.y, aLog.position.z)
await sleep(1200) // Simulate gathering delay
}
await pickupNearbyItems(mineflayer)
await sleep(2500)
logsCount = getLogsCount(mineflayer)
logger.log(`Collected logs. Total logs now: ${logsCount}.`)
}
catch (digError) {
console.error('Failed to break the wood block:', digError)
continue // Attempt to find and break another wood block
}
}
logger.log(`Wood gathering complete! Total logs collected: ${logsCount}.`)
return true
}
catch (error) {
console.error('Failed to gather wood:', error)
return false
}
}
/**
* Helper function to count the number of logs in the inventory.
* @returns The total number of logs.
*/
export function getLogsCount(mineflayer: Mineflayer): number {
return mineflayer.bot.inventory
.items()
.filter(item => item.name.includes('log'))
.reduce((acc, item) => acc + item.count, 0)
}
@@ -0,0 +1,303 @@
import type { Item } from 'prismarine-item'
import type { Mineflayer } from '../../libs/mineflayer'
import { useLogger } from '../../utils/logger'
import { goToPlayer, goToPosition } from '../movement'
import { getNearestBlock } from '../world'
const logger = useLogger()
/**
* Equip an item from the bot's inventory.
* @param mineflayer The mineflayer instance.
* @param itemName The name of the item to equip.
* @returns Whether the item was successfully equipped.
*/
export async function equip(mineflayer: Mineflayer, itemName: string): Promise<boolean> {
const item = mineflayer.bot.inventory
.items()
.find(item => item.name.includes(itemName))
if (!item) {
logger.log(`You do not have any ${itemName} to equip.`)
return false
}
let destination: 'hand' | 'head' | 'torso' | 'legs' | 'feet' = 'hand'
if (itemName.includes('leggings'))
destination = 'legs'
else if (itemName.includes('boots'))
destination = 'feet'
else if (itemName.includes('helmet'))
destination = 'head'
else if (itemName.includes('chestplate'))
destination = 'torso'
await mineflayer.bot.equip(item, destination)
return true
}
/**
* Discard an item from the bot's inventory.
* @param mineflayer The mineflayer instance.
* @param itemName The name of the item to discard.
* @param num The number of items to discard. Default is -1 for all.
* @returns Whether the item was successfully discarded.
*/
export async function discard(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
let discarded = 0
while (true) {
const item = mineflayer.bot.inventory
.items()
.find(item => item.name.includes(itemName))
if (!item) {
break
}
const toDiscard
= num === -1 ? item.count : Math.min(num - discarded, item.count)
await mineflayer.bot.toss(item.type, null, toDiscard)
discarded += toDiscard
if (num !== -1 && discarded >= num) {
break
}
}
if (discarded === 0) {
logger.log(`You do not have any ${itemName} to discard.`)
return false
}
logger.log(`Successfully discarded ${discarded} ${itemName}.`)
return true
}
export async function putInChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
const chest = getNearestBlock(mineflayer, 'chest', 32)
if (!chest) {
logger.log(`Could not find a chest nearby.`)
return false
}
const item = mineflayer.bot.inventory
.items()
.find(item => item.name.includes(itemName))
if (!item) {
logger.log(`You do not have any ${itemName} to put in the chest.`)
return false
}
const toPut = num === -1 ? item.count : Math.min(num, item.count)
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z)
const chestContainer = await mineflayer.bot.openContainer(chest)
await chestContainer.deposit(item.type, null, toPut)
await chestContainer.close()
logger.log(`Successfully put ${toPut} ${itemName} in the chest.`)
return true
}
export async function takeFromChest(
mineflayer: Mineflayer,
itemName: string,
num = -1,
): Promise<boolean> {
const chest = getNearestBlock(mineflayer, 'chest', 32)
if (!chest) {
logger.log(`Could not find a chest nearby.`)
return false
}
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z)
const chestContainer = await mineflayer.bot.openContainer(chest)
const item = chestContainer
.containerItems()
.find(item => item.name.includes(itemName))
if (!item) {
logger.log(`Could not find any ${itemName} in the chest.`)
await chestContainer.close()
return false
}
const toTake = num === -1 ? item.count : Math.min(num, item.count)
await chestContainer.withdraw(item.type, null, toTake)
await chestContainer.close()
logger.log(`Successfully took ${toTake} ${itemName} from the chest.`)
return true
}
/**
* View the contents of a chest near the bot.
* @param mineflayer The mineflayer instance.
* @returns Whether the chest was successfully viewed.
*/
export async function viewChest(mineflayer: Mineflayer): Promise<boolean> {
const chest = getNearestBlock(mineflayer, 'chest', 32)
if (!chest) {
logger.log(`Could not find a chest nearby.`)
return false
}
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z)
const chestContainer = await mineflayer.bot.openContainer(chest)
const items = chestContainer.containerItems()
if (items.length === 0) {
logger.log(`The chest is empty.`)
}
else {
logger.log(`The chest contains:`)
for (const item of items) {
logger.log(`${item.count} ${item.name}`)
}
}
await chestContainer.close()
return true
}
/**
* Ask to bot to eat a food item from its inventory.
* @param mineflayer The mineflayer instance.
* @param foodName The name of the food item to eat.
* @returns Whether the food was successfully eaten.
*/
export async function eat(mineflayer: Mineflayer, foodName = ''): Promise<boolean> {
let item: Item | undefined
let name: string
if (foodName) {
item = mineflayer.bot.inventory.items().find(item => item.name.includes(foodName))
name = foodName
}
else {
// @ts-expect-error -- ?
item = mineflayer.bot.inventory.items().find(item => item.foodPoints > 0)
name = 'food'
}
if (!item) {
logger.log(`You do not have any ${name} to eat.`)
return false
}
await mineflayer.bot.equip(item, 'hand')
await mineflayer.bot.consume()
logger.log(`Successfully ate ${item.name}.`)
return true
}
/**
* Give an item to a player.
* @param mineflayer The mineflayer instance.
* @param itemType The name of the item to give.
* @param username The username of the player to give the item to.
* @param num The number of items to give.
* @returns Whether the item was successfully given.
*/
export async function giveToPlayer(
mineflayer: Mineflayer,
itemType: string,
username: string,
num = 1,
): Promise<boolean> {
const player = mineflayer.bot.players[username]?.entity
if (!player) {
logger.log(`Could not find a player with username: ${username}.`)
return false
}
await goToPlayer(mineflayer, username)
await mineflayer.bot.lookAt(player.position)
await discard(mineflayer, itemType, num)
return true
}
/**
* List the items in the bot's inventory.
* @param mineflayer The mineflayer instance.
* @returns An array of items in the bot's inventory.
*/
export async function listInventory(mineflayer: Mineflayer): Promise<{ name: string, count: number }[]> {
const items = await mineflayer.bot.inventory.items()
// sayItems(mineflayer, items)
return items.map(item => ({
name: item.name,
count: item.count,
}))
}
export async function checkForItem(mineflayer: Mineflayer, itemName: string): Promise<void> {
const items = await mineflayer.bot.inventory.items()
const searchableItems = items.filter(item => item.name.includes(itemName))
sayItems(mineflayer, searchableItems)
}
export async function sayItems(mineflayer: Mineflayer, items: Array<Item> | null = null) {
if (!items) {
items = mineflayer.bot.inventory.items()
if (mineflayer.bot.registry.isNewerOrEqualTo('1.9') && mineflayer.bot.inventory.slots[45])
items.push(mineflayer.bot.inventory.slots[45])
}
const output = items.map(item => `${item.name} x ${item.count}`).join(', ')
if (output) {
mineflayer.bot.chat(`My inventory contains: ${output}`)
}
else {
mineflayer.bot.chat('My inventory is empty.')
}
}
/**
* Find the number of free slots in the bot's inventory.
* @param mineflayer The mineflayer instance.
* @returns The number of free slots in the bot's inventory.
*/
export function checkFreeSpace(mineflayer: Mineflayer): number {
const totalSlots = mineflayer.bot.inventory.slots.length
const usedSlots = mineflayer.bot.inventory.items().length
const freeSlots = totalSlots - usedSlots
logger.log(`You have ${freeSlots} free slots in your inventory.`)
return freeSlots
}
/**
* Transfer all items from the bot's inventory to a chest.
* @param mineflayer The mineflayer instance.
* @returns Whether the items were successfully transferred.
*/
export async function transferAllToChest(mineflayer: Mineflayer): Promise<boolean> {
const chest = getNearestBlock(mineflayer, 'chest', 32)
if (!chest) {
logger.log(`Could not find a chest nearby.`)
return false
}
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z)
const chestContainer = await mineflayer.bot.openContainer(chest)
for (const item of mineflayer.bot.inventory.items()) {
await chestContainer.deposit(item.type, null, item.count)
logger.log(`Put ${item.count} ${item.name} in the chest.`)
}
await chestContainer.close()
return true
}
/**
* Utility function to get item count in inventory
* @param mineflayer The mineflayer instance.
* @param itemName - The name of the item to count.
* @returns number of items in inventory
*/
export function getItemCount(mineflayer: Mineflayer, itemName: string): number {
return mineflayer.bot.inventory
.items()
.filter(item => item.name.includes(itemName))
.reduce((acc, item) => acc + item.count, 0)
}
/**
* Organize the bot's inventory.
* @param mineflayer The mineflayer instance.
* @returns Whether the inventory was successfully organized.
*/
export async function organizeInventory(mineflayer: Mineflayer): Promise<void> {
const items = mineflayer.bot.inventory.items()
if (items.length === 0) {
logger.log(`Inventory is empty, nothing to organize.`)
return
}
for (const item of items) {
await mineflayer.bot.moveSlotItem(
item.slot,
mineflayer.bot.inventory.findInventoryItem(item.type, null, false)?.slot ?? item.slot,
)
}
logger.log(`Inventory has been organized.`)
}
@@ -0,0 +1,374 @@
import type { Bot } from 'mineflayer'
import type { Block } from 'prismarine-block'
import type { Mineflayer } from '../../libs/mineflayer'
import pathfinder from 'mineflayer-pathfinder'
import { Vec3 } from 'vec3'
import { sleep } from '../../utils/helper'
import { useLogger } from '../../utils/logger'
import { getNearestBlock, makeItem } from '../../utils/mcdata'
import { goToPosition } from '../movement'
const logger = useLogger()
export async function placeBlock(
mineflayer: Mineflayer,
blockType: string,
x: number,
y: number,
z: number,
placeOn: string = 'bottom',
): Promise<boolean> {
// if (!gameData.getBlockId(blockType)) {
// logger.log(`Invalid block type: ${blockType}.`);
// return false;
// }
const targetDest = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z))
let block = mineflayer.bot.inventory
.items()
.find(item => item.name.includes(blockType))
if (!block && mineflayer.bot.game.gameMode === 'creative') {
// TODO: Rework
await mineflayer.bot.creative.setInventorySlot(36, makeItem(blockType, 1)) // 36 is first hotbar slot
block = mineflayer.bot.inventory.items().find(item => item.name.includes(blockType))
}
if (!block) {
logger.log(`Don't have any ${blockType} to place.`)
return false
}
const targetBlock = mineflayer.bot.blockAt(targetDest)
if (!targetBlock) {
logger.log(`No block found at ${targetDest}.`)
return false
}
if (targetBlock.name === blockType) {
logger.log(`${blockType} already at ${targetBlock.position}.`)
return false
}
const emptyBlocks = [
'air',
'water',
'lava',
'grass',
'tall_grass',
'snow',
'dead_bush',
'fern',
]
if (!emptyBlocks.includes(targetBlock.name)) {
logger.log(
`${targetBlock.name} is in the way at ${targetBlock.position}.`,
)
const removed = await breakBlockAt(mineflayer, x, y, z)
if (!removed) {
logger.log(
`Cannot place ${blockType} at ${targetBlock.position}: block in the way.`,
)
return false
}
await new Promise(resolve => setTimeout(resolve, 200)) // Wait for block to break
}
// Determine the build-off block and face vector
const dirMap: { [key: string]: Vec3 } = {
top: new Vec3(0, 1, 0),
bottom: new Vec3(0, -1, 0),
north: new Vec3(0, 0, -1),
south: new Vec3(0, 0, 1),
east: new Vec3(1, 0, 0),
west: new Vec3(-1, 0, 0),
}
const dirs: Vec3[] = []
if (placeOn === 'side') {
dirs.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west)
}
else if (dirMap[placeOn]) {
dirs.push(dirMap[placeOn])
}
else {
dirs.push(dirMap.bottom)
logger.log(`Unknown placeOn value "${placeOn}". Defaulting to bottom.`)
}
// Add remaining directions
dirs.push(...Object.values(dirMap).filter(d => !dirs.includes(d)))
let buildOffBlock: Block | null = null
let faceVec: Vec3 | null = null
for (const d of dirs) {
const adjacentBlock = mineflayer.bot.blockAt(targetDest.plus(d))
if (adjacentBlock && !emptyBlocks.includes(adjacentBlock.name)) {
buildOffBlock = adjacentBlock
faceVec = d.scaled(-1) // Invert direction
break
}
}
if (!buildOffBlock || !faceVec) {
logger.log(
`Cannot place ${blockType} at ${targetBlock.position}: nothing to place on.`,
)
return false
}
// Move away if too close
const pos = mineflayer.bot.entity.position
const posAbove = pos.offset(0, 1, 0)
const dontMoveFor = [
'torch',
'redstone_torch',
'redstone',
'lever',
'button',
'rail',
'detector_rail',
'powered_rail',
'activator_rail',
'tripwire_hook',
'tripwire',
'water_bucket',
]
if (
!dontMoveFor.includes(blockType)
&& (pos.distanceTo(targetBlock.position) < 1
|| posAbove.distanceTo(targetBlock.position) < 1)
) {
const goal = new pathfinder.goals.GoalInvert(
new pathfinder.goals.GoalNear(
targetBlock.position.x,
targetBlock.position.y,
targetBlock.position.z,
2,
),
)
// bot.pathfinder.setMovements(new pf.Movements(bot));
await mineflayer.bot.pathfinder.goto(goal)
}
// Move closer if too far
if (mineflayer.bot.entity.position.distanceTo(targetBlock.position) > 4.5) {
await goToPosition(
mineflayer,
targetBlock.position.x,
targetBlock.position.y,
targetBlock.position.z,
4,
)
}
await mineflayer.bot.equip(block, 'hand')
await mineflayer.bot.lookAt(buildOffBlock.position)
await sleep(500)
try {
await mineflayer.bot.placeBlock(buildOffBlock, faceVec)
logger.log(`Placed ${blockType} at ${targetDest}.`)
await new Promise(resolve => setTimeout(resolve, 200))
return true
}
catch (err) {
if (err instanceof Error) {
logger.log(
`Failed to place ${blockType} at ${targetDest}: ${err.message}`,
)
}
else {
logger.log(
`Failed to place ${blockType} at ${targetDest}: ${String(err)}`,
)
}
return false
}
}
export async function breakBlockAt(
mineflayer: Mineflayer,
x: number,
y: number,
z: number,
): Promise<boolean> {
if (x == null || y == null || z == null) {
throw new Error('Invalid position to break block at.')
}
const blockPos = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z))
const block = mineflayer.bot.blockAt(blockPos)
if (!block) {
logger.log(`No block found at position ${blockPos}.`)
return false
}
if (block.name !== 'air' && block.name !== 'water' && block.name !== 'lava') {
if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) {
await goToPosition(mineflayer, x, y, z)
}
if (mineflayer.bot.game.gameMode !== 'creative') {
await mineflayer.bot.tool.equipForBlock(block)
const itemId = mineflayer.bot.heldItem ? mineflayer.bot.heldItem.type : null
if (!block.canHarvest(itemId)) {
logger.log(`Don't have right tools to break ${block.name}.`)
return false
}
}
if (!mineflayer.bot.canDigBlock(block)) {
logger.log(`Cannot break ${block.name} at ${blockPos}.`)
return false
}
await mineflayer.bot.lookAt(block.position, true) // Ensure the bot has finished turning
await sleep(500)
try {
await mineflayer.bot.dig(block, true)
logger.log(
`Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(
1,
)}, z:${z.toFixed(1)}.`,
)
return true
}
catch (err) {
console.error(`Failed to dig the block: ${err}`)
return false
}
}
else {
logger.log(
`Skipping block at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(
1,
)} because it is ${block.name}.`,
)
return false
}
}
export async function activateNearestBlock(mineflayer: Mineflayer, type: string) {
/**
* Activate the nearest block of the given type.
* @param {string} type, the type of block to activate.
* @returns {Promise<boolean>} true if the block was activated, false otherwise.
* @example
* await skills.activateNearestBlock( "lever");
*
*/
const block = getNearestBlock(mineflayer.bot, type, 16)
if (!block) {
logger.log(`Could not find any ${type} to activate.`)
return false
}
if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) {
const pos = block.position
// bot.pathfinder.setMovements(new pf.Movements(bot));
await mineflayer.bot.pathfinder.goto(new pathfinder.goals.GoalNear(pos.x, pos.y, pos.z, 4))
}
await mineflayer.bot.activateBlock(block)
logger.log(
`Activated ${type} at x:${block.position.x.toFixed(
1,
)}, y:${block.position.y.toFixed(1)}, z:${block.position.z.toFixed(1)}.`,
)
return true
}
export async function tillAndSow(
mineflayer: Mineflayer,
x: number,
y: number,
z: number,
seedType: string | null = null,
): Promise<boolean> {
x = Math.round(x)
y = Math.round(y)
z = Math.round(z)
const blockPos = new Vec3(x, y, z)
const block = mineflayer.bot.blockAt(blockPos)
if (!block) {
logger.log(`No block found at ${blockPos}.`)
return false
}
if (
block.name !== 'grass_block'
&& block.name !== 'dirt'
&& block.name !== 'farmland'
) {
logger.log(`Cannot till ${block.name}, must be grass_block or dirt.`)
return false
}
const above = mineflayer.bot.blockAt(blockPos.offset(0, 1, 0))
if (above && above.name !== 'air') {
logger.log(`Cannot till, there is ${above.name} above the block.`)
return false
}
// Move closer if too far
if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) {
await goToPosition(mineflayer, x, y, z, 4)
}
if (block.name !== 'farmland') {
const hoe = mineflayer.bot.inventory.items().find(item => item.name.includes('hoe'))
if (!hoe) {
logger.log(`Cannot till, no hoes.`)
return false
}
await mineflayer.bot.equip(hoe, 'hand')
await mineflayer.bot.activateBlock(block)
logger.log(
`Tilled block x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`,
)
}
if (seedType) {
if (seedType.endsWith('seed') && !seedType.endsWith('seeds'))
seedType += 's' // Fixes common mistake
const seeds = mineflayer.bot.inventory
.items()
.find(item => item.name.includes(seedType || 'seed'))
if (!seeds) {
logger.log(`No ${seedType} to plant.`)
return false
}
await mineflayer.bot.equip(seeds, 'hand')
await mineflayer.bot.placeBlock(block, new Vec3(0, -1, 0))
logger.log(
`Planted ${seedType} at x:${x.toFixed(1)}, y:${y.toFixed(
1,
)}, z:${z.toFixed(1)}.`,
)
}
return true
}
export async function pickupNearbyItems(
mineflayer: Mineflayer,
distance = 8,
): Promise<boolean> {
const getNearestItem = (bot: Bot) =>
bot.nearestEntity(
entity =>
entity.name === 'item'
&& entity.onGround
&& bot.entity.position.distanceTo(entity.position) < distance,
)
let nearestItem = getNearestItem(mineflayer.bot)
let pickedUp = 0
while (nearestItem) {
// bot.pathfinder.setMovements(new pf.Movements(bot));
await mineflayer.bot.pathfinder.goto(
new pathfinder.goals.GoalFollow(nearestItem, 0.8),
() => {},
)
await sleep(500)
const prev = nearestItem
nearestItem = getNearestItem(mineflayer.bot)
if (prev === nearestItem) {
break
}
pickedUp++
}
logger.log(`Picked up ${pickedUp} items.`)
return true
}
+27
View File
@@ -0,0 +1,27 @@
import type { Mineflayer } from '../libs/mineflayer'
import { useLogger } from '../utils/logger'
const logger = useLogger()
/**
* Log a message to the context's output buffer
*/
export function log(mineflayer: Mineflayer, message: string): void {
logger.log(message)
mineflayer.bot.chat(message)
}
/**
* Position in the world
*/
export interface Position {
x: number
y: number
z: number
}
/**
* Block face direction
*/
export type BlockFace = 'top' | 'bottom' | 'north' | 'south' | 'east' | 'west' | 'side'
+675
View File
@@ -0,0 +1,675 @@
import type { SafeBlock } from 'mineflayer-pathfinder'
import type { Mineflayer } from '../libs/mineflayer'
import type { BlockFace } from './base'
import pathfinderModel from 'mineflayer-pathfinder'
import { Vec3 } from 'vec3'
import { getBlockId, makeItem } from '../utils/mcdata'
import { log } from './base'
import { goToPosition } from './movement'
import { getNearestBlock, getNearestBlocks, getPosition, shouldPlaceTorch } from './world'
const { goals, Movements } = pathfinderModel
/**
* Place a torch if needed
*/
async function autoLight(mineflayer: Mineflayer): Promise<boolean> {
if (shouldPlaceTorch(mineflayer)) {
try {
const pos = getPosition(mineflayer)
return await placeBlock(mineflayer, 'torch', pos.x, pos.y, pos.z, 'bottom', true)
}
catch {
return false
}
}
return false
}
/**
* Break a block at the specified position
*/
export async function breakBlockAt(
mineflayer: Mineflayer,
x: number,
y: number,
z: number,
): Promise<boolean> {
validatePosition(x, y, z)
const block = mineflayer.bot.blockAt(new Vec3(x, y, z))
if (isUnbreakableBlock(block))
return false
if (mineflayer.allowCheats) {
return breakWithCheats(mineflayer, x, y, z)
}
await moveIntoRange(mineflayer, block)
if (mineflayer.isCreative) {
return breakInCreative(mineflayer, block, x, y, z)
}
return breakInSurvival(mineflayer, block, x, y, z)
}
function validatePosition(x: number, y: number, z: number) {
if (x == null || y == null || z == null) {
throw new Error('Invalid position to break block at.')
}
}
function isUnbreakableBlock(block: any): boolean {
return block.name === 'air' || block.name === 'water' || block.name === 'lava'
}
async function breakWithCheats(mineflayer: Mineflayer, x: number, y: number, z: number): Promise<boolean> {
mineflayer.bot.chat(`/setblock ${Math.floor(x)} ${Math.floor(y)} ${Math.floor(z)} air`)
log(mineflayer, `Used /setblock to break block at ${x}, ${y}, ${z}.`)
return true
}
async function moveIntoRange(mineflayer: Mineflayer, block: any) {
if (mineflayer.bot.entity.position.distanceTo(block.position) > 4.5) {
const pos = block.position
const movements = new Movements(mineflayer.bot)
movements.allowParkour = false
movements.allowSprinting = false
mineflayer.bot.pathfinder.setMovements(movements)
await mineflayer.bot.pathfinder.goto(new goals.GoalNear(pos.x, pos.y, pos.z, 4))
}
}
async function breakInCreative(mineflayer: Mineflayer, block: any, x: number, y: number, z: number): Promise<boolean> {
await mineflayer.bot.dig(block, true)
log(mineflayer, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`)
return true
}
async function breakInSurvival(mineflayer: Mineflayer, block: any, x: number, y: number, z: number): Promise<boolean> {
await mineflayer.bot.tool.equipForBlock(block)
const itemId = mineflayer.bot.heldItem?.type
if (!block.canHarvest(itemId)) {
log(mineflayer, `Don't have right tools to break ${block.name}.`)
return false
}
await mineflayer.bot.dig(block, true)
log(mineflayer, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`)
return true
}
/**
* Place a block at the specified position
*/
export async function placeBlock(
mineflayer: Mineflayer,
blockType: string,
x: number,
y: number,
z: number,
placeOn: BlockFace = 'bottom',
dontCheat = false,
): Promise<boolean> {
if (!getBlockId(blockType)) {
log(mineflayer, `Invalid block type: ${blockType}.`)
return false
}
const targetDest = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z))
if (mineflayer.allowCheats && !dontCheat) {
return placeWithCheats(mineflayer, blockType, targetDest, placeOn)
}
return placeWithoutCheats(mineflayer, blockType, targetDest, placeOn)
}
function getBlockState(blockType: string, placeOn: BlockFace): string {
const face = getInvertedFace(placeOn as 'north' | 'south' | 'east' | 'west')
let blockState = blockType
if (blockType.includes('torch') && placeOn !== 'bottom') {
blockState = handleTorchState(blockType, placeOn, face)
}
if (blockType.includes('button') || blockType === 'lever') {
blockState = handleButtonLeverState(blockState, placeOn, face)
}
if (needsFacingState(blockType)) {
blockState += `[facing=${face}]`
}
return blockState
}
function getInvertedFace(placeOn: BlockFace): string {
const faceMap: Record<string, string> = {
north: 'south',
south: 'north',
east: 'west',
west: 'east',
}
return faceMap[placeOn] || placeOn
}
function handleTorchState(blockType: string, placeOn: BlockFace, face: string): string {
let state = blockType.replace('torch', 'wall_torch')
if (placeOn !== 'side' && placeOn !== 'top') {
state += `[facing=${face}]`
}
return state
}
function handleButtonLeverState(blockState: string, placeOn: BlockFace, face: string): string {
if (placeOn === 'top') {
return `${blockState}[face=ceiling]`
}
if (placeOn === 'bottom') {
return `${blockState}[face=floor]`
}
return `${blockState}[facing=${face}]`
}
function needsFacingState(blockType: string): boolean {
return blockType === 'ladder'
|| blockType === 'repeater'
|| blockType === 'comparator'
|| blockType.includes('stairs')
}
async function placeWithCheats(
mineflayer: Mineflayer,
blockType: string,
targetDest: Vec3,
placeOn: BlockFace,
): Promise<boolean> {
const blockState = getBlockState(blockType, placeOn)
mineflayer.bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z} ${blockState}`)
if (blockType.includes('door')) {
mineflayer.bot.chat(`/setblock ${targetDest.x} ${targetDest.y + 1} ${targetDest.z} ${blockState}[half=upper]`)
}
if (blockType.includes('bed')) {
mineflayer.bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z - 1} ${blockState}[part=head]`)
}
log(mineflayer, `Used /setblock to place ${blockType} at ${targetDest}.`)
return true
}
async function placeWithoutCheats(
mineflayer: Mineflayer,
blockType: string,
targetDest: Vec3,
placeOn: BlockFace,
): Promise<boolean> {
const itemName = blockType === 'redstone_wire' ? 'redstone' : blockType
let block = mineflayer.bot.inventory.items().find(item => item.name === itemName)
if (!block && mineflayer.isCreative) {
await mineflayer.bot.creative.setInventorySlot(36, makeItem(itemName, 1))
block = mineflayer.bot.inventory.items().find(item => item.name === itemName)
}
if (!block) {
log(mineflayer, `Don't have any ${blockType} to place.`)
return false
}
const targetBlock = mineflayer.bot.blockAt(targetDest)
if (targetBlock?.name === blockType) {
log(mineflayer, `${blockType} already at ${targetBlock.position}.`)
return false
}
const emptyBlocks = ['air', 'water', 'lava', 'grass', 'short_grass', 'tall_grass', 'snow', 'dead_bush', 'fern']
if (!emptyBlocks.includes(targetBlock?.name ?? '')) {
if (!await clearBlockSpace(mineflayer, targetBlock, blockType)) {
return false
}
}
const { buildOffBlock, faceVec } = findPlacementSpot(mineflayer, targetDest, placeOn, emptyBlocks)
if (!buildOffBlock) {
log(mineflayer, `Cannot place ${blockType} at ${targetBlock?.position}: nothing to place on.`)
return false
}
if (!faceVec) {
log(mineflayer, `Cannot place ${blockType} at ${targetBlock?.position}: no valid face to place on.`)
return false
}
await moveIntoPosition(mineflayer, blockType, targetBlock)
return await tryPlaceBlock(mineflayer, block, buildOffBlock, faceVec, blockType, targetDest)
}
async function clearBlockSpace(
mineflayer: Mineflayer,
targetBlock: any,
blockType: string,
): Promise<boolean> {
const removed = await breakBlockAt(mineflayer, targetBlock.position.x, targetBlock.position.y, targetBlock.position.z,
)
if (!removed) {
log(mineflayer, `Cannot place ${blockType} at ${targetBlock.position}: block in the way.`)
return false
}
await new Promise(resolve => setTimeout(resolve, 200))
return true
}
function findPlacementSpot(mineflayer: Mineflayer, targetDest: Vec3, placeOn: BlockFace, emptyBlocks: string[]) {
const dirMap = {
top: new Vec3(0, 1, 0),
bottom: new Vec3(0, -1, 0),
north: new Vec3(0, 0, -1),
south: new Vec3(0, 0, 1),
east: new Vec3(1, 0, 0),
west: new Vec3(-1, 0, 0),
}
const dirs = getPlacementDirections(placeOn, dirMap)
for (const d of dirs) {
const block = mineflayer.bot.blockAt(targetDest.plus(d))
if (!emptyBlocks.includes(block?.name ?? '')) {
return {
buildOffBlock: block,
faceVec: new Vec3(-d.x, -d.y, -d.z),
}
}
}
return { buildOffBlock: null, faceVec: null }
}
function getPlacementDirections(placeOn: BlockFace, dirMap: Record<string, Vec3>): Vec3[] {
const directions: Vec3[] = []
if (placeOn === 'side') {
directions.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west)
}
else if (dirMap[placeOn]) {
directions.push(dirMap[placeOn])
}
else {
directions.push(dirMap.bottom)
}
directions.push(...Object.values(dirMap).filter(d => !directions.includes(d)))
return directions
}
async function moveIntoPosition(mineflayer: Mineflayer, blockType: string, targetBlock: any) {
const dontMoveFor = [
'torch',
'redstone_torch',
'redstone_wire',
'lever',
'button',
'rail',
'detector_rail',
'powered_rail',
'activator_rail',
'tripwire_hook',
'tripwire',
'water_bucket',
]
const pos = mineflayer.bot.entity.position
const posAbove = pos.plus(new Vec3(0, 1, 0))
if (!dontMoveFor.includes(blockType)
&& (pos.distanceTo(targetBlock.position) < 1
|| posAbove.distanceTo(targetBlock.position) < 1)) {
await moveAwayFromBlock(mineflayer, targetBlock)
}
if (mineflayer.bot.entity.position.distanceTo(targetBlock.position) > 4.5) {
await moveToBlock(mineflayer, targetBlock)
}
}
async function moveAwayFromBlock(mineflayer: Mineflayer, targetBlock: any) {
const goal = new goals.GoalNear(
targetBlock.position.x,
targetBlock.position.y,
targetBlock.position.z,
2,
)
const invertedGoal = new goals.GoalInvert(goal)
mineflayer.bot.pathfinder.setMovements(new Movements(mineflayer.bot))
await mineflayer.bot.pathfinder.goto(invertedGoal)
}
async function moveToBlock(mineflayer: Mineflayer, targetBlock: any) {
const pos = targetBlock.position
const movements = new Movements(mineflayer.bot)
mineflayer.bot.pathfinder.setMovements(movements)
await mineflayer.bot.pathfinder.goto(
new goals.GoalNear(pos.x, pos.y, pos.z, 4),
)
}
async function tryPlaceBlock(
mineflayer: Mineflayer,
block: any,
buildOffBlock: any,
faceVec: Vec3,
blockType: string,
targetDest: Vec3,
): Promise<boolean> {
await mineflayer.bot.equip(block, 'hand')
await mineflayer.bot.lookAt(buildOffBlock.position)
try {
await mineflayer.bot.placeBlock(buildOffBlock, faceVec)
log(mineflayer, `Placed ${blockType} at ${targetDest}.`)
await new Promise(resolve => setTimeout(resolve, 200))
return true
}
catch {
log(mineflayer, `Failed to place ${blockType} at ${targetDest}.`)
return false
}
}
/**
* Use a door at the specified position
*/
export async function useDoor(mineflayer: Mineflayer, doorPos: Vec3 | null = null): Promise<boolean> {
doorPos = doorPos || await findNearestDoor(mineflayer.bot)
if (!doorPos) {
log(mineflayer, 'Could not find a door to use.')
return false
}
await goToPosition(mineflayer, doorPos.x, doorPos.y, doorPos.z, 1)
while (mineflayer.bot.pathfinder.isMoving()) {
await new Promise(resolve => setTimeout(resolve, 100))
}
return await operateDoor(mineflayer, doorPos)
}
async function findNearestDoor(bot: any): Promise<Vec3 | null> {
const doorTypes = [
'oak_door',
'spruce_door',
'birch_door',
'jungle_door',
'acacia_door',
'dark_oak_door',
'mangrove_door',
'cherry_door',
'bamboo_door',
'crimson_door',
'warped_door',
]
for (const doorType of doorTypes) {
const block = getNearestBlock(bot, doorType, 16)
if (block) {
return block.position
}
}
return null
}
async function operateDoor(mineflayer: Mineflayer, doorPos: Vec3): Promise<boolean> {
const doorBlock = mineflayer.bot.blockAt(doorPos)
await mineflayer.bot.lookAt(doorPos)
if (!doorBlock) {
log(mineflayer, `Cannot find door at ${doorPos}.`)
return false
}
if (!doorBlock.getProperties().open) {
await mineflayer.bot.activateBlock(doorBlock)
}
mineflayer.bot.setControlState('forward', true)
await new Promise(resolve => setTimeout(resolve, 600))
mineflayer.bot.setControlState('forward', false)
await mineflayer.bot.activateBlock(doorBlock)
mineflayer.bot.setControlState('forward', true)
await new Promise(resolve => setTimeout(resolve, 600))
mineflayer.bot.setControlState('forward', false)
await mineflayer.bot.activateBlock(doorBlock)
log(mineflayer, `Used door at ${doorPos}.`)
return true
}
export async function tillAndSow(
mineflayer: Mineflayer,
x: number,
y: number,
z: number,
seedType: string | null = null,
): Promise<boolean> {
const pos = { x: Math.round(x), y: Math.round(y), z: Math.round(z) }
const block = mineflayer.bot.blockAt(new Vec3(pos.x, pos.y, pos.z))
if (!block) {
log(mineflayer, `Cannot till, no block at ${pos}.`)
return false
}
if (!canTillBlock(block)) {
log(mineflayer, `Cannot till ${block.name}, must be grass_block or dirt.`)
return false
}
const above = mineflayer.bot.blockAt(new Vec3(pos.x, pos.y + 1, pos.z))
if (!above) {
log(mineflayer, `Cannot till, no block above the block.`)
return false
}
if (!isBlockClear(above)) {
log(mineflayer, `Cannot till, there is ${above.name} above the block.`)
return false
}
await moveIntoRange(mineflayer, block)
if (!await tillBlock(mineflayer, block, pos)) {
return false
}
if (seedType) {
return await sowSeeds(mineflayer, block, seedType, pos)
}
return true
}
function canTillBlock(block: any): boolean {
return block.name === 'grass_block' || block.name === 'dirt' || block.name === 'farmland'
}
function isBlockClear(block: any): boolean {
return block.name === 'air'
}
async function tillBlock(mineflayer: Mineflayer, block: any, pos: any): Promise<boolean> {
if (block.name === 'farmland') {
return true
}
const hoe = mineflayer.bot.inventory.items().find(item => item.name.includes('hoe'))
if (!hoe) {
log(mineflayer, 'Cannot till, no hoes.')
return false
}
await mineflayer.bot.equip(hoe, 'hand')
await mineflayer.bot.activateBlock(block)
log(mineflayer, `Tilled block x:${pos.x.toFixed(1)}, y:${pos.y.toFixed(1)}, z:${pos.z.toFixed(1)}.`)
return true
}
async function sowSeeds(mineflayer: Mineflayer, block: any, seedType: string, pos: any): Promise<boolean> {
seedType = fixSeedName(seedType)
const seeds = mineflayer.bot.inventory.items().find(item => item.name === seedType)
if (!seeds) {
log(mineflayer, `No ${seedType} to plant.`)
return false
}
await mineflayer.bot.equip(seeds, 'hand')
await mineflayer.bot.placeBlock(block, new Vec3(0, -1, 0))
log(mineflayer, `Planted ${seedType} at x:${pos.x.toFixed(1)}, y:${pos.y.toFixed(1)}, z:${pos.z.toFixed(1)}.`)
return true
}
function fixSeedName(seedType: string): string {
if (seedType.endsWith('seed') && !seedType.endsWith('seeds')) {
return `${seedType}s` // Fix common mistake
}
return seedType
}
export async function activateNearestBlock(mineflayer: Mineflayer, type: string): Promise<boolean> {
const block = getNearestBlock(mineflayer, type, 16)
if (!block) {
log(mineflayer, `Could not find any ${type} to activate.`)
return false
}
await moveIntoRange(mineflayer, block)
await mineflayer.bot.activateBlock(block)
log(mineflayer, `Activated ${type} at x:${block.position.x.toFixed(1)}, y:${block.position.y.toFixed(1)}, z:${block.position.z.toFixed(1)}.`)
return true
}
export async function collectBlock(
mineflayer: Mineflayer,
blockType: string,
num: number = 1,
exclude: Vec3[] | null = null,
): Promise<boolean> {
if (num < 1) {
log(mineflayer, `Invalid number of blocks to collect: ${num}.`)
return false
}
const blocktypes = getBlockTypes(blockType)
let collected = 0
mineflayer.once('interrupt', () => {
collected = -1
})
for (let i = 0; i < num && collected >= 0; i++) {
const blocks = getValidBlocks(mineflayer, blocktypes, exclude)
if (blocks.length === 0) {
logNoBlocksMessage(mineflayer, blockType, collected)
break
}
const block = blocks[0]
if (!await canHarvestBlock(mineflayer, block, blockType)) {
return false
}
if (!await tryCollectBlock(mineflayer, block, blockType)) {
break
}
collected++
}
if (collected < 0) {
log(mineflayer, 'Collection interrupted.')
return false
}
log(mineflayer, `Collected ${collected} ${blockType}.`)
return collected > 0
}
function getBlockTypes(blockType: string): string[] {
const blocktypes: string[] = [blockType]
const ores = ['coal', 'diamond', 'emerald', 'iron', 'gold', 'lapis_lazuli', 'redstone']
if (ores.includes(blockType)) {
blocktypes.push(`${blockType}_ore`)
}
if (blockType.endsWith('ore')) {
blocktypes.push(`deepslate_${blockType}`)
}
if (blockType === 'dirt') {
blocktypes.push('grass_block')
}
return blocktypes
}
function getValidBlocks(mineflayer: Mineflayer, blocktypes: string[], exclude: Vec3[] | null): any[] {
let blocks = getNearestBlocks(mineflayer, blocktypes, 64)
if (exclude) {
blocks = blocks.filter(
block => !exclude.some(pos =>
pos.x === block.position.x
&& pos.y === block.position.y
&& pos.z === block.position.z,
),
)
}
const movements = new Movements(mineflayer.bot)
movements.dontMineUnderFallingBlock = false
return blocks.filter(block => movements.safeToBreak(block as SafeBlock))
}
function logNoBlocksMessage(mineflayer: Mineflayer, blockType: string, collected: number): void {
log(mineflayer, collected === 0
? `No ${blockType} nearby to collect.`
: `No more ${blockType} nearby to collect.`)
}
async function canHarvestBlock(mineflayer: Mineflayer, block: any, blockType: string): Promise<boolean> {
await mineflayer.bot.tool.equipForBlock(block)
const itemId = mineflayer.bot.heldItem ? mineflayer.bot.heldItem.type : null
if (!block.canHarvest(itemId)) {
log(mineflayer, `Don't have right tools to harvest ${blockType}.`)
return false
}
return true
}
async function tryCollectBlock(mineflayer: Mineflayer, block: any, blockType: string): Promise<boolean> {
try {
await mineflayer.bot.collectBlock.collect(block)
await autoLight(mineflayer)
return true
}
catch (err) {
if (err instanceof Error && err.name === 'NoChests') {
log(mineflayer, `Failed to collect ${blockType}: Inventory full, no place to deposit.`)
return false
}
log(mineflayer, `Failed to collect ${blockType}: ${err}.`)
return true
}
}
+135
View File
@@ -0,0 +1,135 @@
import type { Entity } from 'prismarine-entity'
import type { Item } from 'prismarine-item'
import type { Mineflayer } from '../libs/mineflayer'
import pathfinderModel from 'mineflayer-pathfinder'
import { sleep } from '../utils/helper'
import { isHostile } from '../utils/mcdata'
import { log } from './base'
import { getNearbyEntities, getNearestEntityWhere } from './world'
const { goals } = pathfinderModel
interface WeaponItem extends Item {
attackDamage: number
}
async function equipHighestAttack(mineflayer: Mineflayer): Promise<void> {
const weapons = mineflayer.bot.inventory.items().filter(item =>
item.name.includes('sword')
|| (item.name.includes('axe') && !item.name.includes('pickaxe')),
) as WeaponItem[]
if (weapons.length === 0) {
const tools = mineflayer.bot.inventory.items().filter(item =>
item.name.includes('pickaxe')
|| item.name.includes('shovel'),
) as WeaponItem[]
if (tools.length === 0)
return
tools.sort((a, b) => b.attackDamage - a.attackDamage)
const tool = tools[0]
if (tool)
await mineflayer.bot.equip(tool, 'hand')
return
}
weapons.sort((a, b) => b.attackDamage - a.attackDamage)
const weapon = weapons[0]
if (weapon)
await mineflayer.bot.equip(weapon, 'hand')
}
export async function attackNearest(
mineflayer: Mineflayer,
mobType: string,
kill = true,
): Promise<boolean> {
const mob = getNearbyEntities(mineflayer, 24).find(entity => entity.name === mobType)
if (mob) {
return await attackEntity(mineflayer, mob, kill)
}
log(mineflayer, `Could not find any ${mobType} to attack.`)
return false
}
export async function attackEntity(
mineflayer: Mineflayer,
entity: Entity,
kill = true,
): Promise<boolean> {
const pos = entity.position
await equipHighestAttack(mineflayer)
if (!kill) {
if (mineflayer.bot.entity.position.distanceTo(pos) > 5) {
const goal = new goals.GoalNear(pos.x, pos.y, pos.z, 4)
await mineflayer.bot.pathfinder.goto(goal)
}
await mineflayer.bot.attack(entity)
return true
}
mineflayer.once('interrupt', () => {
mineflayer.bot.pvp.stop()
})
mineflayer.bot.pvp.attack(entity)
while (getNearbyEntities(mineflayer, 24).includes(entity)) {
await new Promise(resolve => setTimeout(resolve, 1000))
}
log(mineflayer, `Successfully killed ${entity.name}.`)
return true
}
export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise<boolean> {
let attacked = false
let enemy = getNearestEntityWhere(mineflayer, entity => isHostile(entity), range)
while (enemy) {
await equipHighestAttack(mineflayer)
if (mineflayer.bot.entity.position.distanceTo(enemy.position) >= 4
&& enemy.name !== 'creeper' && enemy.name !== 'phantom') {
try {
const goal = new goals.GoalFollow(enemy, 3.5)
await mineflayer.bot.pathfinder.goto(goal)
}
catch { /* might error if entity dies, ignore */ }
}
if (mineflayer.bot.entity.position.distanceTo(enemy.position) <= 2) {
try {
const followGoal = new goals.GoalFollow(enemy, 2)
const invertedGoal = new goals.GoalInvert(followGoal)
await mineflayer.bot.pathfinder.goto(invertedGoal)
}
catch { /* might error if entity dies, ignore */ }
}
mineflayer.bot.pvp.attack(enemy)
attacked = true
await sleep(500)
enemy = getNearestEntityWhere(mineflayer, entity => isHostile(entity), range)
mineflayer.once('interrupt', () => {
mineflayer.bot.pvp.stop()
return false
})
}
mineflayer.bot.pvp.stop()
if (attacked) {
log(mineflayer, 'Successfully defended self.')
}
else {
log(mineflayer, 'No enemies nearby to defend self from.')
}
return attacked
}
+343
View File
@@ -0,0 +1,343 @@
import type { Block } from 'prismarine-block'
import type { Item } from 'prismarine-item'
import type { Recipe } from 'prismarine-recipe'
import type { Mineflayer } from '../libs/mineflayer'
import { useLogger } from '../utils/logger'
import { getItemId, getItemName } from '../utils/mcdata'
import { ensureCraftingTable } from './actions/ensure'
import { collectBlock, placeBlock } from './blocks'
import { goToNearestBlock, goToPosition, moveAway } from './movement'
import { getInventoryCounts, getNearestBlock, getNearestFreeSpace } from './world'
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,
num = 1,
): Promise<boolean> {
let itemName = incomingItemName.replace(' ', '_').toLowerCase()
if (itemName.endsWith('plank'))
itemName += 's' // Correct common mistakes
const itemId = getItemId(itemName)
if (itemId === null) {
logger.log(`Invalid item name: ${itemName}`)
return false
}
// Helper function to attempt crafting
async function attemptCraft(
recipes: Recipe[] | null,
craftingTable: Block | null = null,
): Promise<boolean> {
if (recipes && recipes.length > 0) {
const recipe = recipes[0]
try {
await mineflayer.bot.craft(recipe, num, craftingTable ?? undefined)
logger.log(
`Successfully crafted ${num} ${itemName}${
craftingTable ? ' using crafting table' : ''
}.`,
)
return true
}
catch (err) {
logger.log(`Failed to craft ${itemName}: ${(err as Error).message}`)
return false
}
}
return false
}
// Helper function to move to a crafting table and attempt crafting with retry logic
async function moveToAndCraft(craftingTable: Block): Promise<boolean> {
logger.log(`Crafting table found, moving to it.`)
const maxRetries = 2
let attempts = 0
let success = false
while (attempts < maxRetries && !success) {
try {
await goToPosition(
mineflayer,
craftingTable.position.x,
craftingTable.position.y,
craftingTable.position.z,
1,
)
const recipes = mineflayer.bot.recipesFor(itemId, null, 1, craftingTable)
success = await attemptCraft(recipes, craftingTable)
}
catch (err) {
logger.log(
`Attempt ${attempts + 1} to move to crafting table failed: ${
(err as Error).message
}`,
)
}
attempts++
}
return success
}
// Helper function to find and use or place a crafting table
async function findAndUseCraftingTable(
craftingTableRange: number,
): Promise<boolean> {
let craftingTable = getNearestBlock(mineflayer, 'crafting_table', craftingTableRange)
if (craftingTable) {
return await moveToAndCraft(craftingTable)
}
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
}
const pos = getNearestFreeSpace(mineflayer, 1, 10)
if (pos) {
moveAway(mineflayer, 4)
logger.log(
`Placing crafting table at position (${pos.x}, ${pos.y}, ${pos.z}).`,
)
await placeBlock(mineflayer, 'crafting_table', pos.x, pos.y, pos.z)
craftingTable = getNearestBlock(mineflayer, 'crafting_table', craftingTableRange)
if (craftingTable) {
return await moveToAndCraft(craftingTable)
}
}
else {
logger.log('No suitable position found to place the crafting table.')
moveAway(mineflayer, 5)
return false
}
return false
}
// 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
}
// Step 2: Find and use a crafting table
logger.log(`Step 2: Find and use a crafting table`)
const craftingTableRange = 32
if (await findAndUseCraftingTable(craftingTableRange)) {
return true
}
return false
}
export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = 1): Promise<boolean> {
const foods = [
'beef',
'chicken',
'cod',
'mutton',
'porkchop',
'rabbit',
'salmon',
'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.
let placedFurnace = false
let furnaceBlock = getNearestBlock(mineflayer, 'furnace', 32)
if (!furnaceBlock) {
// Try to place furnace
const hasFurnace = getInventoryCounts(mineflayer).furnace > 0
if (hasFurnace) {
const pos = getNearestFreeSpace(mineflayer, 1, 32)
if (pos) {
await placeBlock(mineflayer, 'furnace', pos.x, pos.y, pos.z)
}
else {
logger.log('No suitable position found to place the furnace.')
return false
}
furnaceBlock = getNearestBlock(mineflayer, 'furnace', 32)
placedFurnace = true
}
}
if (!furnaceBlock) {
logger.log(`There is no furnace nearby and I have no furnace.`)
return false
}
if (mineflayer.bot.entity.position.distanceTo(furnaceBlock.position) > 4) {
await goToNearestBlock(mineflayer, 'furnace', 4, 32)
}
await mineflayer.bot.lookAt(furnaceBlock.position)
logger.log('smelting...')
const furnace = await mineflayer.bot.openFurnace(furnaceBlock)
// Check if the furnace is already smelting something
const inputItem = furnace.inputItem()
if (
inputItem
&& 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
}
// 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
}
// Fuel the furnace
if (!furnace.fuelItem()) {
const fuel = mineflayer.bot.inventory
.items()
.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
}
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
}
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))
while (total < num) {
await new Promise(resolve => setTimeout(resolve, 10000))
logger.log('checking...')
let collected = false
if (furnace.outputItem()) {
smeltedItem = await furnace.takeOutput()
if (smeltedItem) {
total += smeltedItem.count
collected = true
}
}
if (!collected && !collectedLast) {
break // if nothing was collected this time or last time
}
collectedLast = collected
}
await mineflayer.bot.closeWindow(furnace)
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
}
logger.log(
`Successfully smelted ${itemName}, got ${total} ${getItemName(
smeltedItem?.type || 0,
)}.`,
)
return true
}
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
}
logger.log('clearing furnace...')
const furnace = await mineflayer.bot.openFurnace(furnaceBlock)
logger.log('opened furnace...')
// Take the items out of the furnace
let smeltedItem: Item | null = null
let inputItem: Item | null = null
let fuelItem: Item | null = null
if (furnace.outputItem())
smeltedItem = await furnace.takeOutput()
if (furnace.inputItem())
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
}
+6
View File
@@ -0,0 +1,6 @@
export * from './base'
export * from './blocks'
export * from './combat'
export * from './crafting'
export * from './inventory'
export * from './movement'
+222
View File
@@ -0,0 +1,222 @@
import type { Mineflayer } from '../libs/mineflayer'
import { log } from './base'
import { goToPlayer, goToPosition } from './movement'
import { getNearestBlock } from './world'
export async function equip(mineflayer: Mineflayer, itemName: string): Promise<boolean> {
const item = mineflayer.bot.inventory.slots.find(slot => slot && slot.name === itemName)
if (!item) {
log(mineflayer, `You do not have any ${itemName} to equip.`)
return false
}
if (itemName.includes('leggings')) {
await mineflayer.bot.equip(item, 'legs')
}
else if (itemName.includes('boots')) {
await mineflayer.bot.equip(item, 'feet')
}
else if (itemName.includes('helmet')) {
await mineflayer.bot.equip(item, 'head')
}
else if (itemName.includes('chestplate') || itemName.includes('elytra')) {
await mineflayer.bot.equip(item, 'torso')
}
else if (itemName.includes('shield')) {
await mineflayer.bot.equip(item, 'off-hand')
}
else {
await mineflayer.bot.equip(item, 'hand')
}
log(mineflayer, `Equipped ${itemName}.`)
return true
}
export async function discard(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
let discarded = 0
while (true) {
const item = mineflayer.bot.inventory.items().find(item => item.name === itemName)
if (!item) {
break
}
const toDiscard = num === -1 ? item.count : Math.min(num - discarded, item.count)
await mineflayer.bot.toss(item.type, null, toDiscard)
discarded += toDiscard
if (num !== -1 && discarded >= num) {
break
}
}
if (discarded === 0) {
log(mineflayer, `You do not have any ${itemName} to discard.`)
return false
}
log(mineflayer, `Discarded ${discarded} ${itemName}.`)
return true
}
export async function putInChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
const chest = getNearestBlock(mineflayer, 'chest', 32)
if (!chest) {
log(mineflayer, 'Could not find a chest nearby.')
return false
}
const item = mineflayer.bot.inventory.items().find(item => item.name === itemName)
if (!item) {
log(mineflayer, `You do not have any ${itemName} to put in the chest.`)
return false
}
const toPut = num === -1 ? item.count : Math.min(num, item.count)
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z, 2)
const chestContainer = await mineflayer.bot.openContainer(chest)
await chestContainer.deposit(item.type, null, toPut)
await chestContainer.close()
log(mineflayer, `Successfully put ${toPut} ${itemName} in the chest.`)
return true
}
export async function takeFromChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
const chest = getNearestBlock(mineflayer, 'chest', 32)
if (!chest) {
log(mineflayer, 'Could not find a chest nearby.')
return false
}
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z, 2)
const chestContainer = await mineflayer.bot.openContainer(chest)
const item = chestContainer.containerItems().find(item => item.name === itemName)
if (!item) {
log(mineflayer, `Could not find any ${itemName} in the chest.`)
await chestContainer.close()
return false
}
const toTake = num === -1 ? item.count : Math.min(num, item.count)
await chestContainer.withdraw(item.type, null, toTake)
await chestContainer.close()
log(mineflayer, `Successfully took ${toTake} ${itemName} from the chest.`)
return true
}
export async function viewChest(mineflayer: Mineflayer): Promise<boolean> {
const chest = getNearestBlock(mineflayer, 'chest', 32)
if (!chest) {
log(mineflayer, 'Could not find a chest nearby.')
return false
}
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z, 2)
const chestContainer = await mineflayer.bot.openContainer(chest)
const items = chestContainer.containerItems()
if (items.length === 0) {
log(mineflayer, 'The chest is empty.')
}
else {
log(mineflayer, 'The chest contains:')
for (const item of items) {
log(mineflayer, `${item.count} ${item.name}`)
}
}
await chestContainer.close()
return true
}
export async function consume(mineflayer: Mineflayer, itemName = ''): Promise<boolean> {
let item
let name
if (itemName) {
item = mineflayer.bot.inventory.items().find(item => item.name === itemName)
name = itemName
}
if (!item) {
log(mineflayer, `You do not have any ${name} to eat.`)
return false
}
await mineflayer.bot.equip(item, 'hand')
await mineflayer.bot.consume()
log(mineflayer, `Consumed ${item.name}.`)
return true
}
export async function giveToPlayer(
mineflayer: Mineflayer,
itemType: string,
username: string,
num = 1,
): Promise<boolean> {
const player = mineflayer.bot.players[username]?.entity
if (!player) {
log(mineflayer, `Could not find ${username}.`)
return false
}
// Move to player position
await goToPlayer(mineflayer, username, 3)
// Look at player before dropping items
await mineflayer.bot.lookAt(player.position)
// Drop items and wait for collection
const success = await dropItemsAndWaitForCollection(mineflayer, itemType, username, num)
if (!success) {
log(mineflayer, `Failed to give ${itemType} to ${username}, it was never received.`)
return false
}
return true
}
async function dropItemsAndWaitForCollection(
mineflayer: Mineflayer,
itemType: string,
username: string,
num: number,
): Promise<boolean> {
if (!await discard(mineflayer, itemType, num)) {
return false
}
return new Promise((resolve) => {
const timeout = setTimeout(() => {
// Clean up playerCollect listener when timeout occurs
// eslint-disable-next-line ts/no-use-before-define
mineflayer.bot.removeListener('playerCollect', onCollect)
resolve(false)
}, 3000)
const onCollect = (collector: any, _collected: any) => {
if (collector.username === username) {
log(mineflayer, `${username} received ${itemType}.`)
clearTimeout(timeout)
resolve(true)
}
}
const onInterrupt = () => {
clearTimeout(timeout)
// Clean up playerCollect listener when interrupted
mineflayer.bot.removeListener('playerCollect', onCollect)
resolve(false)
}
mineflayer.bot.once('playerCollect', onCollect)
mineflayer.once('interrupt', onInterrupt)
})
}
+230
View File
@@ -0,0 +1,230 @@
import type { Entity } from 'prismarine-entity'
import type { Mineflayer } from '../libs/mineflayer'
import { randomInt } from 'es-toolkit'
import pathfinder from 'mineflayer-pathfinder'
import { Vec3 } from 'vec3'
import { sleep } from '../utils/helper'
import { useLogger } from '../utils/logger'
import { log } from './base'
import { getNearestBlock, getNearestEntityWhere } from './world'
const logger = useLogger()
const { goals, Movements } = pathfinder
export async function goToPosition(
mineflayer: Mineflayer,
x: number,
y: number,
z: number,
minDistance = 2,
): Promise<boolean> {
if (x == null || y == null || z == null) {
log(mineflayer, `Missing coordinates, given x:${x} y:${y} z:${z}`)
return false
}
if (mineflayer.allowCheats) {
mineflayer.bot.chat(`/tp @s ${x} ${y} ${z}`)
log(mineflayer, `Teleported to ${x}, ${y}, ${z}.`)
return true
}
await mineflayer.bot.pathfinder.goto(new goals.GoalNear(x, y, z, minDistance))
log(mineflayer, `You have reached ${x}, ${y}, ${z}.`)
return true
}
export async function goToNearestBlock(
mineflayer: Mineflayer,
blockType: string,
minDistance = 2,
range = 64,
): Promise<boolean> {
const MAX_RANGE = 512
if (range > MAX_RANGE) {
log(mineflayer, `Maximum search range capped at ${MAX_RANGE}.`)
range = MAX_RANGE
}
const block = getNearestBlock(mineflayer, blockType, range)
if (!block) {
log(mineflayer, `Could not find any ${blockType} in ${range} blocks.`)
return false
}
log(mineflayer, `Found ${blockType} at ${block.position}.`)
await goToPosition(mineflayer, block.position.x, block.position.y, block.position.z, minDistance)
return true
}
export async function goToNearestEntity(
mineflayer: Mineflayer,
entityType: string,
minDistance = 2,
range = 64,
): Promise<boolean> {
const entity = getNearestEntityWhere(
mineflayer,
entity => entity.name === entityType,
range,
)
if (!entity) {
log(mineflayer, `Could not find any ${entityType} in ${range} blocks.`)
return false
}
const distance = mineflayer.bot.entity.position.distanceTo(entity.position)
log(mineflayer, `Found ${entityType} ${distance} blocks away.`)
await goToPosition(
mineflayer,
entity.position.x,
entity.position.y,
entity.position.z,
minDistance,
)
return true
}
export async function goToPlayer(
mineflayer: Mineflayer,
username: string,
distance = 3,
): Promise<boolean> {
if (mineflayer.allowCheats) {
mineflayer.bot.chat(`/tp @s ${username}`)
log(mineflayer, `Teleported to ${username}.`)
return true
}
const player = mineflayer.bot.players[username]?.entity
if (!player) {
log(mineflayer, `Could not find ${username}.`)
return false
}
await mineflayer.bot.pathfinder.goto(new goals.GoalFollow(player, distance))
log(mineflayer, `You have reached ${username}.`)
return true
}
export async function followPlayer(
mineflayer: Mineflayer,
username: string,
distance = 4,
): Promise<boolean> {
const player = mineflayer.bot.players[username]?.entity
if (!player) {
return false
}
log(mineflayer, `I am now actively following player ${username}.`)
const movements = new Movements(mineflayer.bot)
mineflayer.bot.pathfinder.setMovements(movements)
mineflayer.bot.pathfinder.setGoal(new goals.GoalFollow(player, distance), true)
mineflayer.once('interrupt', () => {
mineflayer.bot.pathfinder.stop()
})
return true
}
export async function moveAway(mineflayer: Mineflayer, distance: number): Promise<boolean> {
try {
const pos = mineflayer.bot.entity.position
let newX: number = 0
let newZ: number = 0
let suitableGoal = false
while (!suitableGoal) {
const rand1 = randomInt(0, 2)
const rand2 = randomInt(0, 2)
const bigRand1 = randomInt(0, 101)
const bigRand2 = randomInt(0, 101)
newX = Math.floor(
pos.x + ((distance * bigRand1) / 100) * (rand1 ? 1 : -1),
)
newZ = Math.floor(
pos.z + ((distance * bigRand2) / 100) * (rand2 ? 1 : -1),
)
const block = mineflayer.bot.blockAt(new Vec3(newX, pos.y - 1, newZ))
if (block?.name !== 'water' && block?.name !== 'lava') {
suitableGoal = true
}
}
const farGoal = new pathfinder.goals.GoalXZ(newX, newZ)
await mineflayer.bot.pathfinder.goto(farGoal)
const newPos = mineflayer.bot.entity.position
logger.log(`I moved away from nearest entity to ${newPos}.`)
await sleep(500)
return true
}
catch (err) {
logger.log(`I failed to move away: ${(err as Error).message}`)
return false
}
}
export async function moveAwayFromEntity(
mineflayer: Mineflayer,
entity: Entity,
distance = 16,
): Promise<boolean> {
const goal = new goals.GoalFollow(entity, distance)
const invertedGoal = new goals.GoalInvert(goal)
await mineflayer.bot.pathfinder.goto(invertedGoal)
return true
}
export async function stay(mineflayer: Mineflayer, seconds = 30): Promise<boolean> {
const start = Date.now()
const targetTime = seconds === -1 ? Infinity : start + seconds * 1000
while (Date.now() < targetTime) {
await sleep(500)
}
log(mineflayer, `I stayed for ${(Date.now() - start) / 1000} seconds.`)
return true
}
export async function goToBed(mineflayer: Mineflayer): Promise<boolean> {
const beds = mineflayer.bot.findBlocks({
matching: block => block.name.includes('bed'),
maxDistance: 32,
count: 1,
})
if (beds.length === 0) {
log(mineflayer, 'I could not find a bed to sleep in.')
return false
}
const loc = beds[0]
await goToPosition(mineflayer, loc.x, loc.y, loc.z)
const bed = mineflayer.bot.blockAt(loc)
if (!bed) {
log(mineflayer, 'I could not find a bed to sleep in.')
return false
}
await mineflayer.bot.sleep(bed)
log(mineflayer, 'I am in bed.')
while (mineflayer.bot.isSleeping) {
await sleep(500)
}
log(mineflayer, 'I have woken up.')
return true
}
+196
View File
@@ -0,0 +1,196 @@
import type { Block } from 'prismarine-block'
import type { Entity } from 'prismarine-entity'
import type { Item } from 'prismarine-item'
import type { Vec3 } from 'vec3'
import type { Mineflayer } from '../libs/mineflayer'
import pf from 'mineflayer-pathfinder'
import * as mc from '../utils/mcdata'
export function getNearestFreeSpace(
mineflayer: Mineflayer,
size: number = 1,
distance: number = 8,
): Vec3 | undefined {
/**
* Get the nearest empty space with solid blocks beneath it of the given size.
* @param {number} size - The (size x size) of the space to find, default 1.
* @param {number} distance - The maximum distance to search, default 8.
* @returns {Vec3} - The south west corner position of the nearest free space.
* @example
* let position = world.getNearestFreeSpace( 1, 8);
*/
const empty_pos = mineflayer.bot.findBlocks({
matching: (block: Block | null) => {
return block !== null && block.name === 'air'
},
maxDistance: distance,
count: 1000,
})
for (let i = 0; i < empty_pos.length; i++) {
let empty = true
for (let x = 0; x < size; x++) {
for (let z = 0; z < size; z++) {
const top = mineflayer.bot.blockAt(empty_pos[i].offset(x, 0, z))
const bottom = mineflayer.bot.blockAt(empty_pos[i].offset(x, -1, z))
if (
!top
|| top.name !== 'air'
|| !bottom
|| (bottom.drops?.length ?? 0) === 0
|| !bottom.diggable
) {
empty = false
break
}
}
if (!empty)
break
}
if (empty) {
return empty_pos[i]
}
}
return undefined
}
export function getNearestBlocks(mineflayer: Mineflayer, blockTypes: string[] | string | null = null, distance: number = 16, count: number = 10000): Block[] {
const blockIds = blockTypes === null
? mc.getAllBlockIds(['air'])
: (Array.isArray(blockTypes) ? blockTypes : [blockTypes]).map(mc.getBlockId).filter((id): id is number => id !== null)
const positions = mineflayer.bot.findBlocks({ matching: blockIds, maxDistance: distance, count })
return positions
.map((pos) => {
const block = mineflayer.bot.blockAt(pos)
const dist = pos.distanceTo(mineflayer.bot.entity.position)
return block ? { block, distance: dist } : null
})
.filter((item): item is { block: Block, distance: number } => item !== null)
.sort((a, b) => a.distance - b.distance)
.map(item => item.block)
}
export function getNearestBlock(mineflayer: Mineflayer, blockType: string, distance: number = 16): Block | null {
const blocks = getNearestBlocks(mineflayer, blockType, distance, 1)
return blocks[0] || null
}
export function getNearbyEntities(mineflayer: Mineflayer, maxDistance: number = 16): Entity[] {
return Object.values(mineflayer.bot.entities)
.filter((entity): entity is Entity =>
entity !== null
&& entity.position.distanceTo(mineflayer.bot.entity.position) <= maxDistance,
)
.sort((a, b) =>
a.position.distanceTo(mineflayer.bot.entity.position)
- b.position.distanceTo(mineflayer.bot.entity.position),
)
}
export function getNearestEntityWhere(mineflayer: Mineflayer, predicate: (entity: Entity) => boolean, maxDistance: number = 16): Entity | null {
return mineflayer.bot.nearestEntity(entity =>
predicate(entity)
&& mineflayer.bot.entity.position.distanceTo(entity.position) < maxDistance,
)
}
export function getNearbyPlayers(mineflayer: Mineflayer, maxDistance: number = 16): Entity[] {
return getNearbyEntities(mineflayer, maxDistance)
.filter(entity =>
entity.type === 'player'
&& entity.username !== mineflayer.bot.username,
)
}
export function getInventoryStacks(mineflayer: Mineflayer): Item[] {
return mineflayer.bot.inventory.items().filter((item): item is Item => item !== null)
}
export function getInventoryCounts(mineflayer: Mineflayer): Record<string, number> {
return getInventoryStacks(mineflayer).reduce((counts, item) => {
counts[item.name] = (counts[item.name] || 0) + item.count
return counts
}, {} as Record<string, number>)
}
export function getCraftableItems(mineflayer: Mineflayer): string[] {
const table = getNearestBlock(mineflayer, 'crafting_table')
|| getInventoryStacks(mineflayer).find(item => item.name === 'crafting_table')
return mc.getAllItems()
.filter(item => mineflayer.bot.recipesFor(item.id, null, 1, table as Block | null).length > 0)
.map(item => item.name)
}
export function getPosition(mineflayer: Mineflayer): Vec3 {
return mineflayer.bot.entity.position
}
export function getNearbyEntityTypes(mineflayer: Mineflayer): string[] {
return [...new Set(
getNearbyEntities(mineflayer, 16)
.map(mob => mob.name)
.filter((name): name is string => name !== undefined),
)]
}
export function getNearbyPlayerNames(mineflayer: Mineflayer): string[] {
return [...new Set(
getNearbyPlayers(mineflayer, 64)
.map(player => player.username)
.filter((name): name is string =>
name !== undefined
&& name !== mineflayer.bot.username,
),
)]
}
export function getNearbyBlockTypes(mineflayer: Mineflayer, distance: number = 16): string[] {
return [...new Set(
getNearestBlocks(mineflayer, null, distance)
.map(block => block.name),
)]
}
export async function isClearPath(mineflayer: Mineflayer, target: Entity): Promise<boolean> {
const movements = new pf.Movements(mineflayer.bot)
movements.canDig = false
// movements.canPlaceOn = false // TODO: fix this
const goal = new pf.goals.GoalNear(
target.position.x,
target.position.y,
target.position.z,
1,
)
const path = await mineflayer.bot.pathfinder.getPathTo(movements, goal, 100)
return path.status === 'success'
}
export function shouldPlaceTorch(mineflayer: Mineflayer): boolean {
// if (!mineflayer.bot.modes.isOn('torch_placing') || mineflayer.bot.interrupt_code) {
// return false
// }
const pos = getPosition(mineflayer)
const nearestTorch = getNearestBlock(mineflayer, 'torch', 6)
|| getNearestBlock(mineflayer, 'wall_torch', 6)
if (nearestTorch) {
return false
}
const block = mineflayer.bot.blockAt(pos)
const hasTorch = mineflayer.bot.inventory.items().some(item => item?.name === 'torch')
return Boolean(hasTorch && block?.name === 'air')
}
export function getBiomeName(mineflayer: Mineflayer): string {
const biomeId = mineflayer.bot.world.getBiome(mineflayer.bot.entity.position)
return mc.getAllBiomes()[biomeId].name
}
+39
View File
@@ -0,0 +1,39 @@
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
/**
* Returns a retirable anonymous function with configured retryLimit and delayInterval
*
* @param retryLimit Number of retry attempts
* @param delayInterval Delay between retries in milliseconds
* @param func Function to be called
* @returns A wrapped function with the same signature as func
*/
export function toRetriable<A, R>(
retryLimit: number,
delayInterval: number,
func: (...args: A[]) => Promise<R>,
hooks?: {
onError?: (err: unknown) => void
},
): (...args: A[]) => Promise<R> {
let retryCount = 0
return async function (args: A): Promise<R> {
try {
return await func(args)
}
catch (err) {
if (hooks?.onError) {
hooks.onError(err)
}
if (retryCount < retryLimit) {
retryCount++
await sleep(delayInterval)
return await toRetriable(retryLimit - retryCount, delayInterval, func)(args)
}
else {
throw err
}
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import { Format, LogLevel, setGlobalFormat, setGlobalLogLevel, useLogg } from '@guiiai/logg'
export type Logger = ReturnType<typeof useLogg>
export function initLogger() {
setGlobalLogLevel(LogLevel.Debug)
setGlobalFormat(Format.Pretty)
const logger = useLogg('logger').useGlobalConfig()
logger.log('Logger initialized')
}
/**
* Get logger instance with directory name and filename
* @returns logger instance configured with "directoryName/filename"
*/
export function useLogger() {
const stack = new Error('logger').stack
const caller = stack?.split('\n')[2]
// Match the parent directory and filename without extension
const match = caller?.match(/\/([^/]+)\/([^/]+?)\.[jt]s/)
const dirName = match?.[1] || 'unknown'
const fileName = match?.[2] || 'unknown'
return useLogg(`${dirName}/${fileName}`).useGlobalConfig()
}
+274
View File
@@ -0,0 +1,274 @@
import type { Biome, ShapedRecipe, ShapelessRecipe } from 'minecraft-data'
import type { Bot } from 'mineflayer'
import type { Entity } from 'prismarine-entity'
import minecraftData from 'minecraft-data'
import prismarineItem from 'prismarine-item'
const GAME_VERSION = '1.20'
export const gameData = minecraftData(GAME_VERSION)
export const Item = prismarineItem(GAME_VERSION)
export const WOOD_TYPES: string[] = [
'oak',
'spruce',
'birch',
'jungle',
'acacia',
'dark_oak',
]
export const MATCHING_WOOD_BLOCKS: string[] = [
'log',
'planks',
'sign',
'boat',
'fence_gate',
'door',
'fence',
'slab',
'stairs',
'button',
'pressure_plate',
'trapdoor',
]
export const WOOL_COLORS: string[] = [
'white',
'orange',
'magenta',
'light_blue',
'yellow',
'lime',
'pink',
'gray',
'light_gray',
'cyan',
'purple',
'blue',
'brown',
'green',
'red',
'black',
]
export function isHuntable(mob: Entity): boolean {
if (!mob || !mob.name)
return false
const animals: string[] = [
'chicken',
'cow',
'llama',
'mooshroom',
'pig',
'rabbit',
'sheep',
]
return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16] // metadata[16] indicates baby status
}
export function isHostile(mob: Entity): boolean {
if (!mob || !mob.name)
return false
return (
(mob.type === 'mob' || mob.type === 'hostile')
&& mob.name !== 'iron_golem'
&& mob.name !== 'snow_golem'
)
}
export function getItemId(itemName: string): number {
const item = gameData.itemsByName[itemName]
return item?.id || 0
}
export function getItemName(itemId: number): string {
const item = gameData.items[itemId]
return item.name || ''
}
export function getBlockId(blockName: string): number {
const block = gameData.blocksByName?.[blockName]
return block?.id || 0
}
export function getBlockName(blockId: number): string {
const block = gameData.blocks[blockId]
return block.name || ''
}
export function getAllItems(ignore: string[] = []): any[] {
const items: any[] = []
for (const itemId in gameData.items) {
const item = gameData.items[itemId]
if (!ignore.includes(item.name)) {
items.push(item)
}
}
return items
}
export function getAllItemIds(ignore: string[] = []): number[] {
const items = getAllItems(ignore)
const itemIds: number[] = []
for (const item of items) {
itemIds.push(item.id)
}
return itemIds
}
export function getAllBlocks(ignore: string[] = []): any[] {
const blocks: any[] = []
for (const blockId in gameData.blocks) {
const block = gameData.blocks[blockId]
if (!ignore.includes(block.name)) {
blocks.push(block)
}
}
return blocks
}
export function getAllBlockIds(ignore: string[] = []): number[] {
const blocks = getAllBlocks(ignore)
const blockIds: number[] = []
for (const block of blocks) {
blockIds.push(block.id)
}
return blockIds
}
export function getAllBiomes(): Record<number, Biome> {
return gameData.biomes
}
export function getItemCraftingRecipes(itemName: string): any[] | null {
const itemId = getItemId(itemName)
if (!itemId || !gameData.recipes[itemId]) {
return null
}
const recipes: Record<string, number>[] = []
for (const r of gameData.recipes[itemId]) {
const recipe: Record<string, number> = {}
let ingredients: number[] = []
if (isShapelessRecipe(r)) {
// Handle shapeless recipe
ingredients = r.ingredients.map((ing: any) => ing.id)
}
else if (isShapedRecipe(r)) {
// Handle shaped recipe
ingredients = r.inShape
.flat()
.map((ing: any) => ing?.id)
.filter(Boolean)
}
for (const ingredientId of ingredients) {
const ingredientName = getItemName(ingredientId)
if (ingredientName === null)
continue
if (!recipe[ingredientName])
recipe[ingredientName] = 0
recipe[ingredientName]++
}
recipes.push(recipe)
}
return recipes
}
// Type guards
function isShapelessRecipe(recipe: any): recipe is ShapelessRecipe {
return 'ingredients' in recipe
}
function isShapedRecipe(recipe: any): recipe is ShapedRecipe {
return 'inShape' in recipe
}
export function getItemSmeltingIngredient(
itemName: string,
): string | undefined {
return {
baked_potato: 'potato',
steak: 'raw_beef',
cooked_chicken: 'raw_chicken',
cooked_cod: 'raw_cod',
cooked_mutton: 'raw_mutton',
cooked_porkchop: 'raw_porkchop',
cooked_rabbit: 'raw_rabbit',
cooked_salmon: 'raw_salmon',
dried_kelp: 'kelp',
iron_ingot: 'raw_iron',
gold_ingot: 'raw_gold',
copper_ingot: 'raw_copper',
glass: 'sand',
}[itemName]
}
export function getItemBlockSources(itemName: string): string[] {
const itemId = getItemId(itemName)
const sources: string[] = []
if (!itemId)
return sources
for (const block of getAllBlocks()) {
if (block.drops && block.drops.includes(itemId)) {
sources.push(block.name)
}
}
return sources
}
export function getItemAnimalSource(itemName: string): string | undefined {
return {
raw_beef: 'cow',
raw_chicken: 'chicken',
raw_cod: 'cod',
raw_mutton: 'sheep',
raw_porkchop: 'pig',
raw_rabbit: 'rabbit',
raw_salmon: 'salmon',
leather: 'cow',
wool: 'sheep',
}[itemName]
}
export function getBlockTool(blockName: string): string | null {
const block = gameData.blocksByName[blockName]
if (!block || !block.harvestTools) {
return null
}
const toolIds = Object.keys(block.harvestTools).map(id => Number.parseInt(id))
const toolName = getItemName(toolIds[0])
return toolName || null // Assuming the first tool is the simplest
}
export function makeItem(name: string, amount = 1): InstanceType<typeof Item> {
const itemId = getItemId(name)
if (itemId === null)
throw new Error(`Item ${name} not found.`)
return new Item(itemId, amount)
}
// Function to get the nearest block of a specific type using Mineflayer
export function getNearestBlock(
bot: Bot,
blockType: string,
maxDistance: number,
) {
const blocks = bot.findBlocks({
matching: block => block.name === blockType,
maxDistance,
count: 1,
})
if (blocks.length === 0)
return null
const nearestBlockPosition = blocks[0]
return bot.blockAt(nearestBlockPosition)
}