Merge branch 'main' of https://github.com/moeru-ai/airi-minecraft into main
Co-authored-by: RainbowBird <44741987+luoling8192@users.noreply.github.com>
This commit is contained in:
@@ -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,
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './base'
|
||||
export * from './blocks'
|
||||
export * from './combat'
|
||||
export * from './crafting'
|
||||
export * from './inventory'
|
||||
export * from './movement'
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user