refactor: better structure v1
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { messages, system, user } from 'neuri/openai'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createBot, useBot } from '../composables/bot'
|
||||
import { initBot, useBot } from '../composables/bot'
|
||||
import { botConfig, initEnv } from '../composables/config'
|
||||
import { genActionAgentPrompt, genQueryAgentPrompt } from '../prompts/agent'
|
||||
import { sleep } from '../utils/helper'
|
||||
@@ -11,17 +11,17 @@ describe('actions agent', { timeout: 0 }, () => {
|
||||
beforeAll(() => {
|
||||
initLogger()
|
||||
initEnv()
|
||||
createBot(botConfig)
|
||||
initBot({ botConfig })
|
||||
})
|
||||
|
||||
it('should choose right query command', async () => {
|
||||
const { ctx } = useBot()
|
||||
const agent = await initAgent(ctx)
|
||||
const { bot } = useBot()
|
||||
const agent = await initAgent(bot)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
ctx.bot.once('spawn', async () => {
|
||||
bot.bot.once('spawn', async () => {
|
||||
const text = await agent.handle(messages(
|
||||
system(genQueryAgentPrompt(ctx)),
|
||||
system(genQueryAgentPrompt(bot)),
|
||||
user('What are you status?'),
|
||||
), async (c) => {
|
||||
const completion = await c.reroute('query', c.messages, { model: 'openai/gpt-4o-mini' })
|
||||
@@ -37,15 +37,15 @@ describe('actions agent', { timeout: 0 }, () => {
|
||||
})
|
||||
|
||||
it('should choose right action command', async () => {
|
||||
const { ctx } = useBot()
|
||||
const agent = await initAgent(ctx)
|
||||
const { bot } = useBot()
|
||||
const agent = await initAgent(bot)
|
||||
|
||||
// console.log(JSON.stringify(agent, null, 2))
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
ctx.bot.on('spawn', async () => {
|
||||
bot.bot.on('spawn', async () => {
|
||||
const text = await agent.handle(messages(
|
||||
system(genActionAgentPrompt(ctx)),
|
||||
system(genActionAgentPrompt(bot)),
|
||||
user('goToPlayer: luoling8192'),
|
||||
), async (c) => {
|
||||
console.log(JSON.stringify(c, null, 2))
|
||||
|
||||
@@ -1,18 +1,8 @@
|
||||
import type { SkillContext } from '../skills'
|
||||
import type { Action } from '../libs/mineflayer'
|
||||
import { z } from 'zod'
|
||||
import { getStatusToString } from '../components/status'
|
||||
import * as world from '../composables/world'
|
||||
import * as skills from '../skills'
|
||||
|
||||
type ActionResult = string | Promise<string>
|
||||
|
||||
export interface Action {
|
||||
readonly name: string
|
||||
readonly description: string
|
||||
readonly schema: z.ZodObject<any>
|
||||
readonly perform: (ctx: SkillContext) => (...args: any[]) => ActionResult
|
||||
}
|
||||
|
||||
// Utils
|
||||
const pad = (str: string): string => `\n${str}\n`
|
||||
|
||||
@@ -29,28 +19,27 @@ export const actionsList: Action[] = [
|
||||
name: 'stats',
|
||||
description: 'Get your bot\'s location, health, hunger, and time of day.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => (): string => getStatusToString(ctx.botCtx),
|
||||
perform: mineflayer => (): string => mineflayer.status.toOneLiner(),
|
||||
},
|
||||
{
|
||||
name: 'inventory',
|
||||
description: 'Get your bot\'s inventory.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => (): string => {
|
||||
const { bot } = ctx
|
||||
const inventory = world.getInventoryCounts(world.createWorldContext(ctx.botCtx))
|
||||
perform: mineflayer => (): string => {
|
||||
const inventory = world.getInventoryCounts(mineflayer)
|
||||
const items = Object.entries(inventory)
|
||||
.map(([item, count]) => formatInventoryItem(item, count))
|
||||
.join('')
|
||||
|
||||
const wearing = [
|
||||
formatWearingItem('Head', bot.inventory.slots[5]?.name),
|
||||
formatWearingItem('Torso', bot.inventory.slots[6]?.name),
|
||||
formatWearingItem('Legs', bot.inventory.slots[7]?.name),
|
||||
formatWearingItem('Feet', bot.inventory.slots[8]?.name),
|
||||
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'}
|
||||
${bot.game.gameMode === 'creative' ? '\n(You have infinite items in creative mode. You do not need to gather resources!!)' : ''}
|
||||
${mineflayer.bot.game.gameMode === 'creative' ? '\n(You have infinite items in creative mode. You do not need to gather resources!!)' : ''}
|
||||
WEARING: ${wearing || 'Nothing'}`)
|
||||
},
|
||||
},
|
||||
@@ -58,8 +47,8 @@ export const actionsList: Action[] = [
|
||||
name: 'nearbyBlocks',
|
||||
description: 'Get the blocks near the bot.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => (): string => {
|
||||
const blocks = world.getNearbyBlockTypes(world.createWorldContext(ctx.botCtx))
|
||||
perform: mineflayer => (): string => {
|
||||
const blocks = world.getNearbyBlockTypes(mineflayer)
|
||||
return pad(`NEARBY_BLOCKS${blocks.map((b: string) => `\n- ${b}`).join('') || ': none'}`)
|
||||
},
|
||||
},
|
||||
@@ -67,8 +56,8 @@ export const actionsList: Action[] = [
|
||||
name: 'craftable',
|
||||
description: 'Get the craftable items with the bot\'s inventory.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => (): string => {
|
||||
const craftable = world.getCraftableItems(world.createWorldContext(ctx.botCtx))
|
||||
perform: mineflayer => (): string => {
|
||||
const craftable = world.getCraftableItems(mineflayer)
|
||||
return pad(`CRAFTABLE_ITEMS${craftable.map((i: string) => `\n- ${i}`).join('') || ': none'}`)
|
||||
},
|
||||
},
|
||||
@@ -76,10 +65,9 @@ export const actionsList: Action[] = [
|
||||
name: 'entities',
|
||||
description: 'Get the nearby players and entities.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => (): string => {
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
const players = world.getNearbyPlayerNames(worldCtx)
|
||||
const entities = world.getNearbyEntityTypes(worldCtx)
|
||||
perform: mineflayer => (): string => {
|
||||
const players = world.getNearbyPlayerNames(mineflayer)
|
||||
const entities = world.getNearbyEntityTypes(mineflayer)
|
||||
.filter((e: string) => e !== 'player' && e !== 'item')
|
||||
|
||||
const result = [
|
||||
@@ -97,10 +85,10 @@ export const actionsList: Action[] = [
|
||||
// schema: z.object({
|
||||
// prompt: z.string().describe('A natural language prompt to guide code generation. Make a detailed step-by-step plan.'),
|
||||
// }),
|
||||
// perform: (ctx: BotContext) => async (prompt: string) => {
|
||||
// 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(ctx.history)
|
||||
// return await ctx.coder.generateCode(mineflayer.history)
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
@@ -110,14 +98,14 @@ export const actionsList: Action[] = [
|
||||
name: 'stop',
|
||||
description: 'Force stop all actions and commands that are currently executing.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => async () => {
|
||||
perform: mineflayer => async () => {
|
||||
// await ctx.actions.stop()
|
||||
// ctx.clearBotLogs()
|
||||
// ctx.actions.cancelResume()
|
||||
// ctx.bot.emit('idle')
|
||||
ctx.shouldInterrupt = true
|
||||
mineflayer.shouldInterrupt = true
|
||||
const msg = 'Agent stopped.'
|
||||
// if (ctx.self_prompter.on)
|
||||
// if (mineflayer.self_prompter.on)
|
||||
// msg += ' Self-prompting still active.'
|
||||
return msg
|
||||
},
|
||||
@@ -128,7 +116,7 @@ export const actionsList: Action[] = [
|
||||
// name: 'stfu',
|
||||
// description: 'Stop all chatting and self prompting, but continue current action.',
|
||||
// schema: z.object({}),
|
||||
// perform: (ctx: BotContext) => async () => {
|
||||
// perform: (mineflayer: BotContext) => async () => {
|
||||
// ctx.openChat('Shutting up.')
|
||||
// ctx.shutUp()
|
||||
// return 'Shutting up.'
|
||||
@@ -141,7 +129,7 @@ export const actionsList: Action[] = [
|
||||
// name: 'restart',
|
||||
// description: 'Restart the agent process.',
|
||||
// schema: z.object({}),
|
||||
// perform: (ctx: BotContext) => async () => {
|
||||
// perform: (mineflayer: BotContext) => async () => {
|
||||
// ctx.cleanKill()
|
||||
// return 'Restarting agent...'
|
||||
// },
|
||||
@@ -153,7 +141,7 @@ export const actionsList: Action[] = [
|
||||
// name: 'clearChat',
|
||||
// description: 'Clear the chat history.',
|
||||
// schema: z.object({}),
|
||||
// perform: (ctx: BotContext) => async () => {
|
||||
// perform: (mineflayer: BotContext) => async () => {
|
||||
// ctx.history.clear()
|
||||
// return `${ctx.name}'s chat history was cleared, starting new conversation from scratch.`
|
||||
// },
|
||||
@@ -166,8 +154,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (player_name: string, closeness: number) => {
|
||||
await skills.goToPlayer(ctx, player_name, closeness)
|
||||
perform: mineflayer => async (player_name: string, closeness: number) => {
|
||||
await skills.goToPlayer(mineflayer, player_name, closeness)
|
||||
return 'Moving to player...'
|
||||
},
|
||||
},
|
||||
@@ -179,8 +167,8 @@ export const actionsList: Action[] = [
|
||||
player_name: z.string().describe('name of the player to follow.'),
|
||||
follow_dist: z.number().describe('The distance to follow from.').min(0),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (player_name: string, follow_dist: number) => {
|
||||
await skills.followPlayer(ctx, player_name, follow_dist)
|
||||
perform: mineflayer => async (player_name: string, follow_dist: number) => {
|
||||
await skills.followPlayer(mineflayer, player_name, follow_dist)
|
||||
return 'Following player...'
|
||||
},
|
||||
},
|
||||
@@ -194,8 +182,8 @@ export const actionsList: Action[] = [
|
||||
z: z.number().describe('The z coordinate.'),
|
||||
closeness: z.number().describe('How close to get to the location.').min(0),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (x: number, y: number, z: number, closeness: number) => {
|
||||
await skills.goToPosition(ctx, x, y, z, closeness)
|
||||
perform: mineflayer => async (x: number, y: number, z: number, closeness: number) => {
|
||||
await skills.goToPosition(mineflayer, x, y, z, closeness)
|
||||
return 'Moving to coordinates...'
|
||||
},
|
||||
},
|
||||
@@ -207,8 +195,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (block_type: string, range: number) => {
|
||||
await skills.goToNearestBlock(ctx, block_type, 4, range)
|
||||
perform: mineflayer => async (block_type: string, range: number) => {
|
||||
await skills.goToNearestBlock(mineflayer, block_type, 4, range)
|
||||
return 'Searching for block...'
|
||||
},
|
||||
},
|
||||
@@ -220,8 +208,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (entity_type: string, range: number) => {
|
||||
await skills.goToNearestEntity(ctx, entity_type, 4, range)
|
||||
perform: mineflayer => async (entity_type: string, range: number) => {
|
||||
await skills.goToNearestEntity(mineflayer, entity_type, 4, range)
|
||||
return 'Searching for entity...'
|
||||
},
|
||||
},
|
||||
@@ -232,8 +220,8 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
distance: z.number().describe('The distance to move away.').min(0),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (distance: number) => {
|
||||
await skills.moveAway(ctx, distance)
|
||||
perform: mineflayer => async (distance: number) => {
|
||||
await skills.moveAway(mineflayer, distance)
|
||||
return 'Moving away...'
|
||||
},
|
||||
},
|
||||
@@ -246,8 +234,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (player_name: string, item_name: string, num: number) => {
|
||||
await skills.giveToPlayer(ctx, item_name, player_name, num)
|
||||
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...'
|
||||
},
|
||||
},
|
||||
@@ -258,8 +246,8 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the item to consume.'),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (item_name: string) => {
|
||||
await skills.consume(ctx, item_name)
|
||||
perform: mineflayer => async (item_name: string) => {
|
||||
await skills.consume(mineflayer, item_name)
|
||||
return 'Consuming item...'
|
||||
},
|
||||
},
|
||||
@@ -270,8 +258,8 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
item_name: z.string().describe('The name of the item to equip.'),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (item_name: string) => {
|
||||
await skills.equip(ctx, item_name)
|
||||
perform: mineflayer => async (item_name: string) => {
|
||||
await skills.equip(mineflayer, item_name)
|
||||
return 'Equipping item...'
|
||||
},
|
||||
},
|
||||
@@ -283,8 +271,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (item_name: string, num: number) => {
|
||||
await skills.putInChest(ctx, item_name, num)
|
||||
perform: mineflayer => async (item_name: string, num: number) => {
|
||||
await skills.putInChest(mineflayer, item_name, num)
|
||||
return 'Putting items in chest...'
|
||||
},
|
||||
},
|
||||
@@ -296,8 +284,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (item_name: string, num: number) => {
|
||||
await skills.takeFromChest(ctx, item_name, num)
|
||||
perform: mineflayer => async (item_name: string, num: number) => {
|
||||
await skills.takeFromChest(mineflayer, item_name, num)
|
||||
return 'Taking items from chest...'
|
||||
},
|
||||
},
|
||||
@@ -306,8 +294,8 @@ export const actionsList: Action[] = [
|
||||
name: 'viewChest',
|
||||
description: 'View the items/counts of the nearest chest.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => async () => {
|
||||
await skills.viewChest(ctx)
|
||||
perform: mineflayer => async () => {
|
||||
await skills.viewChest(mineflayer)
|
||||
return 'Viewing chest contents...'
|
||||
},
|
||||
},
|
||||
@@ -319,11 +307,11 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (item_name: string, num: number) => {
|
||||
const start_loc = ctx.bot.entity.position
|
||||
await skills.moveAway(ctx, 5)
|
||||
await skills.discard(ctx, item_name, num)
|
||||
await skills.goToPosition(ctx, start_loc.x, start_loc.y, start_loc.z, 0)
|
||||
perform: mineflayer => async (item_name: string, num: number) => {
|
||||
const start_loc = mineflayer.bot.entity.position
|
||||
await skills.moveAway(mineflayer, 5)
|
||||
await skills.discard(mineflayer, item_name, num)
|
||||
await skills.goToPosition(mineflayer, start_loc.x, start_loc.y, start_loc.z, 0)
|
||||
return 'Discarding items...'
|
||||
},
|
||||
},
|
||||
@@ -335,8 +323,8 @@ export const actionsList: Action[] = [
|
||||
type: z.string().describe('The block type to collect.'),
|
||||
num: z.number().int().describe('The number of blocks to collect.').min(1),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (type: string, num: number) => {
|
||||
await skills.collectBlock(ctx, type, num)
|
||||
perform: mineflayer => async (type: string, num: number) => {
|
||||
await skills.collectBlock(mineflayer, type, num)
|
||||
return 'Collecting blocks...'
|
||||
},
|
||||
},
|
||||
@@ -348,8 +336,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (recipe_name: string, num: number) => {
|
||||
await skills.craftRecipe(ctx, recipe_name, num)
|
||||
perform: mineflayer => async (recipe_name: string, num: number) => {
|
||||
await skills.craftRecipe(mineflayer, recipe_name, num)
|
||||
return 'Crafting items...'
|
||||
},
|
||||
},
|
||||
@@ -361,8 +349,8 @@ export const actionsList: Action[] = [
|
||||
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: (ctx: SkillContext) => async (item_name: string, num: number) => {
|
||||
await skills.smeltItem(ctx, item_name, num)
|
||||
perform: mineflayer => async (item_name: string, num: number) => {
|
||||
await skills.smeltItem(mineflayer, item_name, num)
|
||||
return 'Smelting items...'
|
||||
},
|
||||
},
|
||||
@@ -371,8 +359,8 @@ export const actionsList: Action[] = [
|
||||
name: 'clearFurnace',
|
||||
description: 'Take all items out of the nearest furnace.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => async () => {
|
||||
await skills.clearNearestFurnace(ctx)
|
||||
perform: mineflayer => async () => {
|
||||
await skills.clearNearestFurnace(mineflayer)
|
||||
return 'Clearing furnace...'
|
||||
},
|
||||
},
|
||||
@@ -383,9 +371,9 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
type: z.string().describe('The block type to place.'),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (type: string) => {
|
||||
const pos = ctx.bot.entity.position
|
||||
await skills.placeBlock(ctx, type, pos.x, pos.y, pos.z)
|
||||
perform: mineflayer => async (type: string) => {
|
||||
const pos = mineflayer.bot.entity.position
|
||||
await skills.placeBlock(mineflayer, type, pos.x, pos.y, pos.z)
|
||||
return 'Placing block...'
|
||||
},
|
||||
},
|
||||
@@ -396,8 +384,8 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
type: z.string().describe('The type of entity to attack.'),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (type: string) => {
|
||||
await skills.attackNearest(ctx, type, true)
|
||||
perform: mineflayer => async (type: string) => {
|
||||
await skills.attackNearest(mineflayer, type, true)
|
||||
return 'Attacking entity...'
|
||||
},
|
||||
},
|
||||
@@ -408,13 +396,13 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
player_name: z.string().describe('The name of the player to attack.'),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (player_name: string) => {
|
||||
const player = ctx.bot.players[player_name]?.entity
|
||||
perform: mineflayer => async (player_name: string) => {
|
||||
const player = mineflayer.bot.players[player_name]?.entity
|
||||
if (!player) {
|
||||
skills.log(ctx, `Could not find player ${player_name}.`)
|
||||
skills.log(mineflayer, `Could not find player ${player_name}.`)
|
||||
return 'Player not found'
|
||||
}
|
||||
await skills.attackEntity(ctx, player, true)
|
||||
await skills.attackEntity(mineflayer, player, true)
|
||||
return 'Attacking player...'
|
||||
},
|
||||
},
|
||||
@@ -423,8 +411,8 @@ export const actionsList: Action[] = [
|
||||
name: 'goToBed',
|
||||
description: 'Go to the nearest bed and sleep.',
|
||||
schema: z.object({}),
|
||||
perform: (ctx: SkillContext) => async () => {
|
||||
await skills.goToBed(ctx)
|
||||
perform: mineflayer => async () => {
|
||||
await skills.goToBed(mineflayer)
|
||||
return 'Going to bed...'
|
||||
},
|
||||
},
|
||||
@@ -435,8 +423,8 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
type: z.string().describe('The type of object to activate.'),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (type: string) => {
|
||||
await skills.activateNearestBlock(ctx, type)
|
||||
perform: mineflayer => async (type: string) => {
|
||||
await skills.activateNearestBlock(mineflayer, type)
|
||||
return 'Activating block...'
|
||||
},
|
||||
},
|
||||
@@ -447,8 +435,8 @@ export const actionsList: Action[] = [
|
||||
schema: z.object({
|
||||
type: z.number().int().describe('The number of seconds to stay. -1 for forever.').min(-1),
|
||||
}),
|
||||
perform: (ctx: SkillContext) => async (seconds: number) => {
|
||||
await skills.stay(ctx, seconds)
|
||||
perform: mineflayer => async (seconds: number) => {
|
||||
await skills.stay(mineflayer, seconds)
|
||||
return 'Staying in place...'
|
||||
},
|
||||
},
|
||||
@@ -460,7 +448,7 @@ export const actionsList: Action[] = [
|
||||
// mode_name: z.string().describe('The name of the mode to enable.'),
|
||||
// on: z.boolean().describe('Whether to enable or disable the mode.'),
|
||||
// }),
|
||||
// perform: (ctx: BotContext) => async (mode_name: string, on: boolean) => {
|
||||
// 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()}`
|
||||
@@ -479,7 +467,7 @@ export const actionsList: Action[] = [
|
||||
// schema: z.object({
|
||||
// selfPrompt: z.string().describe('The goal prompt.'),
|
||||
// }),
|
||||
// perform: (ctx: BotContext) => async (prompt: string) => {
|
||||
// perform: (mineflayer: BotContext) => async (prompt: string) => {
|
||||
// if (convoManager.inConversation()) {
|
||||
// ctx.self_prompter.setPrompt(prompt)
|
||||
// convoManager.scheduleSelfPrompter()
|
||||
@@ -497,7 +485,7 @@ export const actionsList: Action[] = [
|
||||
// name: 'endGoal',
|
||||
// description: 'Call when you have accomplished your goal. It will stop self-prompting and the current action.',
|
||||
// schema: z.object({}),
|
||||
// perform: (ctx: BotContext) => async () => {
|
||||
// perform: (mineflayer: BotContext) => async () => {
|
||||
// ctx.self_prompter.stop()
|
||||
// convoManager.cancelSelfPrompter()
|
||||
// return 'Self-prompting stopped.'
|
||||
@@ -513,7 +501,7 @@ export const actionsList: Action[] = [
|
||||
// player_name: z.string().describe('The name of the player to send the message to.'),
|
||||
// message: z.string().describe('The message to send.'),
|
||||
// }),
|
||||
// perform: (ctx: BotContext) => async (player_name: string, message: string) => {
|
||||
// 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))
|
||||
@@ -532,7 +520,7 @@ export const actionsList: Action[] = [
|
||||
// schema: z.object({
|
||||
// player_name: z.string().describe('The name of the player to end the conversation with.'),
|
||||
// }),
|
||||
// perform: (ctx: BotContext) => async (player_name: string) => {
|
||||
// perform: (mineflayer: BotContext) => async (player_name: string) => {
|
||||
// if (!convoManager.inConversation(player_name))
|
||||
// return `Not in conversation with ${player_name}.`
|
||||
// convoManager.endConversation(player_name)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { messages, system, user } from 'neuri/openai'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { createBot, useBot } from '../composables/bot'
|
||||
import { initBot, useBot } from '../composables/bot'
|
||||
import { botConfig, initEnv } from '../composables/config'
|
||||
import { genSystemBasicPrompt } from '../prompts/agent'
|
||||
import { initLogger } from '../utils/logger'
|
||||
@@ -10,15 +10,15 @@ describe('openAI agent', { timeout: 0 }, () => {
|
||||
beforeAll(() => {
|
||||
initLogger()
|
||||
initEnv()
|
||||
createBot(botConfig)
|
||||
initBot({ botConfig })
|
||||
})
|
||||
|
||||
it('should initialize the agent', async () => {
|
||||
const { ctx } = useBot()
|
||||
const agent = await initAgent(ctx)
|
||||
const { bot } = useBot()
|
||||
const agent = await initAgent(bot)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
ctx.bot.once('spawn', async () => {
|
||||
bot.bot.once('spawn', async () => {
|
||||
const text = await agent.handle(
|
||||
messages(
|
||||
system(genSystemBasicPrompt('airi')),
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Agent, Neuri } from 'neuri'
|
||||
import type { BotContext } from '../composables/bot'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { agent, neuri } from 'neuri'
|
||||
import { openaiConfig } from '../composables/config'
|
||||
import { useSkillContext } from '../skills'
|
||||
import { actionsList } from './actions'
|
||||
|
||||
let neuriAgent: Neuri | undefined
|
||||
@@ -11,11 +10,11 @@ const agents = new Set<Agent | Promise<Agent>>()
|
||||
|
||||
const logger = useLogg('openai').useGlobalConfig()
|
||||
|
||||
export async function initAgent(ctx: BotContext): Promise<Neuri> {
|
||||
export async function initAgent(mineflayer: Mineflayer): Promise<Neuri> {
|
||||
logger.log('Initializing agent')
|
||||
let n = neuri()
|
||||
|
||||
agents.add(initActionAgent(ctx))
|
||||
agents.add(initActionAgent(mineflayer))
|
||||
|
||||
agents.forEach(agent => n = n.agent(agent))
|
||||
|
||||
@@ -36,7 +35,7 @@ export function getAgent(): Neuri {
|
||||
return neuriAgent
|
||||
}
|
||||
|
||||
export async function initActionAgent(ctx: BotContext): Promise<Agent> {
|
||||
export async function initActionAgent(mineflayer: Mineflayer): Promise<Agent> {
|
||||
logger.log('Initializing action agent')
|
||||
let actionAgent = agent('action')
|
||||
|
||||
@@ -46,8 +45,8 @@ export async function initActionAgent(ctx: BotContext): Promise<Agent> {
|
||||
action.schema,
|
||||
async ({ parameters }) => {
|
||||
logger.withFields({ name: action.name, parameters }).log('Calling action')
|
||||
ctx.memory.actions.push(action)
|
||||
return action.perform(useSkillContext(ctx))(...Object.values(parameters))
|
||||
mineflayer.memory.actions.push(action)
|
||||
return action.perform(mineflayer)(...Object.values(parameters))
|
||||
},
|
||||
{ description: action.description },
|
||||
)
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import type { BotContext, ComponentLifecycle } from '../composables/bot'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { assistant, type Message, system, user } from 'neuri/openai'
|
||||
import { getAgent } from '../agents/openai'
|
||||
import { formBotChat } from '../middlewares/chat'
|
||||
import { genActionAgentPrompt } from '../prompts/agent'
|
||||
|
||||
export function createAiChatComponent(ctx: BotContext): ComponentLifecycle {
|
||||
const logger = useLogg('aichat').useGlobalConfig()
|
||||
logger.log('Loading aichat plugin')
|
||||
|
||||
ctx.memory.chatHistory.push(system(genActionAgentPrompt(ctx)))
|
||||
|
||||
// todo: get system message
|
||||
const onChat = formBotChat(ctx, async (username, message) => {
|
||||
logger.withFields({ username, message }).log('Chat message received')
|
||||
|
||||
ctx.memory.chatHistory.push(user(`${username}: ${message}`))
|
||||
|
||||
const agent = getAgent()
|
||||
const content = await agent.handleStateless([...ctx.memory.chatHistory], async (c) => {
|
||||
logger.log('Generate response')
|
||||
|
||||
try {
|
||||
const completion = await c.reroute('action', c.messages, { model: 'openai/gpt-4o-mini' }) || { error: { message: 'Unknown error' } }
|
||||
|
||||
logger.withFields({ completion }).log('Completion')
|
||||
|
||||
if (!completion || 'error' in completion) {
|
||||
logger.withFields(c).error('Completion')
|
||||
return
|
||||
// throw new Error(completion?.error?.message ?? 'Unknown error')
|
||||
}
|
||||
|
||||
const content = await completion?.firstContent()
|
||||
ctx.memory.chatHistory.push(assistant(content))
|
||||
|
||||
return content
|
||||
}
|
||||
catch (e) {
|
||||
logger.errorWithError('Generate response error', e)
|
||||
}
|
||||
})
|
||||
|
||||
if (content) {
|
||||
logger.withFields({ content }).log('Bot response')
|
||||
ctx.bot.chat(content)
|
||||
}
|
||||
})
|
||||
|
||||
ctx.bot.on('chat', onChat)
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
ctx.bot.removeListener('chat', onChat)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { BotContext, ComponentLifecycle } from '../composables/bot'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { commands } from '../composables/command'
|
||||
import { formBotChat } from '../middlewares/chat'
|
||||
import { parseCommand } from '../middlewares/command'
|
||||
|
||||
const logger = useLogg('command').useGlobalConfig()
|
||||
|
||||
export function createCommandComponent(ctx: BotContext): ComponentLifecycle {
|
||||
const onChat = formBotChat(ctx, (sender, message) => {
|
||||
const { isCommand, command, args } = parseCommand(sender, message)
|
||||
|
||||
if (!isCommand)
|
||||
return
|
||||
|
||||
// Remove the # prefix from command
|
||||
const cleanCommand = command.slice(1)
|
||||
|
||||
logger.withFields({ sender, command: cleanCommand, args }).log('Command received')
|
||||
|
||||
const handler = commands.get(cleanCommand)
|
||||
if (handler) {
|
||||
handler({ sender, isCommand, command: cleanCommand, args })
|
||||
return
|
||||
}
|
||||
|
||||
// Built-in commands
|
||||
switch (cleanCommand) {
|
||||
case 'help': {
|
||||
const commandList = Array.from(commands.keys()).concat(['help'])
|
||||
ctx.bot.chat(`Available commands: ${commandList.map(cmd => `#${cmd}`).join(', ')}`)
|
||||
break
|
||||
}
|
||||
default:
|
||||
ctx.bot.chat(`Unknown command: ${cleanCommand}`)
|
||||
}
|
||||
})
|
||||
|
||||
ctx.bot.on('chat', onChat)
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
ctx.bot.removeListener('chat', onChat)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import type { BotContext, ComponentLifecycle } from '../composables/bot'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { formBotChat } from '../middlewares/chat'
|
||||
|
||||
const logger = useLogg('echo').useGlobalConfig()
|
||||
|
||||
export function createEchoComponent(ctx: BotContext): ComponentLifecycle {
|
||||
const onChat = formBotChat(ctx, (username, message) => {
|
||||
logger.withFields({ username, message }).log('Chat message received')
|
||||
ctx.bot.chat(message)
|
||||
})
|
||||
|
||||
ctx.bot.on('chat', onChat)
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
ctx.bot.removeListener('chat', onChat)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
import type { BotContext, ComponentLifecycle } from '../composables/bot'
|
||||
import type { CommandContext } from '../middlewares/command'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import pathfinderModel from 'mineflayer-pathfinder'
|
||||
import { registerCommand } from '../composables/command'
|
||||
|
||||
const { goals, Movements } = pathfinderModel
|
||||
|
||||
export function createFollowComponent(ctx: BotContext, config?: {
|
||||
rangeGoal: number
|
||||
}): ComponentLifecycle {
|
||||
const logger = useLogg('follow').useGlobalConfig()
|
||||
|
||||
const state = {
|
||||
following: undefined as string | undefined,
|
||||
movements: new Movements(ctx.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')
|
||||
ctx.bot.pathfinder.stop()
|
||||
}
|
||||
|
||||
function followPlayer(): void {
|
||||
if (!state.following)
|
||||
return
|
||||
|
||||
const target = ctx.bot.players[state.following]?.entity
|
||||
if (!target) {
|
||||
ctx.bot.chat('I lost sight of you!')
|
||||
state.following = undefined
|
||||
return
|
||||
}
|
||||
|
||||
const { x: playerX, y: playerY, z: playerZ } = target.position
|
||||
|
||||
ctx.bot.pathfinder.setMovements(state.movements)
|
||||
ctx.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, config?.rangeGoal ?? 1))
|
||||
}
|
||||
|
||||
registerCommand('follow', (commandCtx: CommandContext) => {
|
||||
const username = commandCtx.sender
|
||||
if (!username) {
|
||||
ctx.bot.chat('Please specify a player name!')
|
||||
return
|
||||
}
|
||||
startFollow(username)
|
||||
})
|
||||
|
||||
registerCommand('stop', () => {
|
||||
stopFollow()
|
||||
})
|
||||
|
||||
// Continuously update path to follow player
|
||||
const followInterval = setInterval(() => {
|
||||
if (state.following)
|
||||
followPlayer()
|
||||
}, 1000)
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
clearInterval(followInterval)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import type { BotContext, ComponentLifecycle } from '../composables/bot'
|
||||
import type { CommandContext } from '../middlewares/command'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import pathfinderModel from 'mineflayer-pathfinder'
|
||||
import { registerCommand } from '../composables/command'
|
||||
|
||||
const { goals, Movements } = pathfinderModel
|
||||
|
||||
export function createPathFinderComponent(ctx: BotContext, config?: {
|
||||
rangeGoal: number
|
||||
}): ComponentLifecycle {
|
||||
const logger = useLogg('pathfinder').useGlobalConfig()
|
||||
|
||||
let defaultMove: any
|
||||
|
||||
const handleCome = (commandCtx: CommandContext) => {
|
||||
const username = commandCtx.sender
|
||||
if (!username) {
|
||||
ctx.bot.chat('Please specify a player name!')
|
||||
return
|
||||
}
|
||||
|
||||
logger.withFields({ username }).log('Come command received')
|
||||
const target = ctx.bot.players[username]?.entity
|
||||
if (!target) {
|
||||
ctx.bot.chat('I don\'t see that player!')
|
||||
return
|
||||
}
|
||||
|
||||
const { x: playerX, y: playerY, z: playerZ } = target.position
|
||||
|
||||
ctx.bot.pathfinder.setMovements(defaultMove)
|
||||
ctx.bot.pathfinder.setGoal(new goals.GoalNear(playerX, playerY, playerZ, config?.rangeGoal ?? 1))
|
||||
}
|
||||
|
||||
defaultMove = new Movements(ctx.bot)
|
||||
registerCommand('come', handleCome)
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
// Commands are cleaned up automatically
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { BotContext, ComponentLifecycle } from '../composables/bot'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { registerCommand } from '../composables/command'
|
||||
|
||||
const status = new Map<string, string>()
|
||||
|
||||
export function getStatusToString(ctx: BotContext): string {
|
||||
return Array.from(getStatus(ctx).entries()).map(([key, value]) => `${key}: ${value}`).join('\n')
|
||||
}
|
||||
|
||||
export function getStatus(ctx: BotContext): Map<string, string> {
|
||||
const pos = ctx.bot.entity.position
|
||||
const weather = ctx.bot.isRaining ? 'Rain' : ctx.bot.thunderState ? 'Thunderstorm' : 'Clear'
|
||||
const timeOfDay = ctx.bot.time.timeOfDay < 6000
|
||||
? 'Morning'
|
||||
: ctx.bot.time.timeOfDay < 12000 ? 'Afternoon' : 'Night'
|
||||
|
||||
status.set('position', `x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)}`)
|
||||
status.set('health', `${Math.round(ctx.bot.health)} / 20`)
|
||||
status.set('weather', weather)
|
||||
status.set('timeOfDay', timeOfDay)
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
export function createStatusComponent(ctx: BotContext): ComponentLifecycle {
|
||||
const logger = useLogg('status').useGlobalConfig()
|
||||
logger.log('Loading status component')
|
||||
|
||||
registerCommand('status', () => {
|
||||
logger.log('Status command received')
|
||||
const status = getStatusToString(ctx)
|
||||
ctx.bot.chat(status)
|
||||
})
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
// Commands are cleaned up automatically
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,354 +1,13 @@
|
||||
import type { Message } from 'neuri/openai'
|
||||
import type { Action } from '../agents/actions'
|
||||
import type { BotInternalEventHandlers, BotInternalEvents } from './events'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import mineflayer, { type Bot, type BotOptions } from 'mineflayer'
|
||||
import armorManager from 'mineflayer-armor-manager'
|
||||
import { loader as autoEat } from 'mineflayer-auto-eat'
|
||||
import { plugin as collectblock } from 'mineflayer-collectblock'
|
||||
import { pathfinder } from 'mineflayer-pathfinder'
|
||||
import { plugin as pvp } from 'mineflayer-pvp'
|
||||
import { plugin as tool } from 'mineflayer-tool'
|
||||
import { Mineflayer, type MineflayerOptions } from '../libs/mineflayer'
|
||||
|
||||
const logger = useLogg('bot').useGlobalConfig()
|
||||
let mineflayer: Mineflayer
|
||||
|
||||
let ctx: BotContext | undefined
|
||||
|
||||
export interface BotContext {
|
||||
bot: Bot
|
||||
botName: string
|
||||
ready: boolean
|
||||
|
||||
components: Map<string, ComponentLifecycle>
|
||||
|
||||
prompt: {
|
||||
selfPrompt: string
|
||||
}
|
||||
|
||||
memory: {
|
||||
chatHistory: Message[]
|
||||
actions: Action[]
|
||||
// getSummary: () => string
|
||||
}
|
||||
|
||||
status: Map<string, string>
|
||||
// status: {
|
||||
// position: Position
|
||||
// health: number
|
||||
// weather: string
|
||||
// timeOfDay: string
|
||||
// }
|
||||
|
||||
health: {
|
||||
value: number
|
||||
lastDamageTime: number
|
||||
lastDamageTaken: number
|
||||
}
|
||||
|
||||
emit: (event: BotInternalEvents) => any
|
||||
eventListeners: Record<BotInternalEvents, Array<BotInternalEventHandlers[BotInternalEvents]>>
|
||||
}
|
||||
|
||||
export interface Component {
|
||||
(ctx: BotContext): ComponentLifecycle
|
||||
}
|
||||
|
||||
export interface ComponentLifecycle {
|
||||
cleanup: () => void
|
||||
}
|
||||
|
||||
// todo: reconnect
|
||||
export function createBot(options: BotOptions): Bot {
|
||||
logger.withFields({ options }).log('Creating bot')
|
||||
ctx = {
|
||||
ready: false,
|
||||
bot: mineflayer.createBot({
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
username: options.username,
|
||||
password: options.password,
|
||||
}),
|
||||
components: new Map(),
|
||||
botName: options.username,
|
||||
prompt: {
|
||||
selfPrompt: '',
|
||||
},
|
||||
memory: {
|
||||
chatHistory: [],
|
||||
actions: [],
|
||||
},
|
||||
status: new Map(),
|
||||
health: {
|
||||
value: 20,
|
||||
lastDamageTime: 0,
|
||||
lastDamageTaken: 0,
|
||||
},
|
||||
emit: (event: BotInternalEvents) => {
|
||||
if (!ctx)
|
||||
return
|
||||
const listeners = ctx.eventListeners[event]
|
||||
if (listeners) {
|
||||
listeners.forEach(listener => listener())
|
||||
}
|
||||
},
|
||||
eventListeners: {
|
||||
'time:sunrise': [],
|
||||
'time:noon': [],
|
||||
'time:sunset': [],
|
||||
'time:midnight': [],
|
||||
},
|
||||
}
|
||||
|
||||
logger.log('Loading plugins')
|
||||
ctx.bot.loadPlugin(pathfinder)
|
||||
ctx.bot.loadPlugin(pvp)
|
||||
ctx.bot.loadPlugin(collectblock)
|
||||
ctx.bot.loadPlugin(autoEat)
|
||||
ctx.bot.loadPlugin(armorManager) // auto equip armor
|
||||
ctx.bot.loadPlugin(tool)
|
||||
ctx.bot.once('resourcePack', () => {
|
||||
ctx?.bot.acceptResourcePack()
|
||||
})
|
||||
logger.log('Plugins loaded')
|
||||
|
||||
ctx.bot.on('time', () => {
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
if (ctx.bot.time.timeOfDay === 0)
|
||||
ctx.emit('time:sunrise')
|
||||
else if (ctx.bot.time.timeOfDay === 6000)
|
||||
ctx.emit('time:noon')
|
||||
else if (ctx.bot.time.timeOfDay === 12000)
|
||||
ctx.emit('time:sunset')
|
||||
else if (ctx.bot.time.timeOfDay === 18000)
|
||||
ctx.emit('time:midnight')
|
||||
})
|
||||
|
||||
ctx.bot.on('health', () => {
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
logger.withFields({
|
||||
health: ctx.health.value,
|
||||
lastDamageTime: ctx.health.lastDamageTime,
|
||||
lastDamageTaken: ctx.health.lastDamageTaken,
|
||||
previousHealth: ctx.bot.health,
|
||||
}).log('Health updated')
|
||||
|
||||
if (ctx.bot.health < ctx.health.value) {
|
||||
ctx.health.lastDamageTime = Date.now()
|
||||
ctx.health.lastDamageTaken = ctx.health.value - ctx.bot.health
|
||||
}
|
||||
|
||||
ctx.health.value = ctx.bot.health
|
||||
})
|
||||
|
||||
ctx.bot.once('spawn', () => {
|
||||
ctx!.ready = true
|
||||
logger.log('Bot ready')
|
||||
})
|
||||
|
||||
ctx.bot.on('death', () => {
|
||||
logger.error('Bot died')
|
||||
})
|
||||
|
||||
ctx.bot.on('messagestr', async (message, _, jsonMsg) => {
|
||||
if (!ctx)
|
||||
return
|
||||
|
||||
// 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(ctx.botName)) {
|
||||
const deathPos = ctx.bot.entity.position
|
||||
|
||||
// this.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 = ctx.bot.game.dimension
|
||||
await handleMessage(ctx, '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.`)
|
||||
}
|
||||
})
|
||||
|
||||
ctx.bot.on('end', (reason) => {
|
||||
logger.withFields({ reason }).log('Bot ended')
|
||||
})
|
||||
|
||||
ctx.bot.on('kicked', (reason: string) => {
|
||||
logger.withFields({ reason }).error('Bot was kicked')
|
||||
})
|
||||
|
||||
ctx.bot.on('error', (err: Error) => {
|
||||
logger.errorWithError('Bot error:', err)
|
||||
})
|
||||
|
||||
logger.log('Bot created')
|
||||
return ctx.bot
|
||||
}
|
||||
|
||||
async function handleMessage(ctx: BotContext, 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;
|
||||
export function initBot(options: MineflayerOptions) {
|
||||
mineflayer = new Mineflayer(options)
|
||||
}
|
||||
|
||||
export function useBot() {
|
||||
if (ctx == null || ctx.bot == null) {
|
||||
throw new Error('Bot instance not found')
|
||||
}
|
||||
|
||||
const cleanup = () => {
|
||||
logger.log('Cleaning up bot and components')
|
||||
ctx!.components.forEach((BotContext: ComponentLifecycle) => BotContext.cleanup?.())
|
||||
ctx!.components.clear()
|
||||
ctx!.bot.end()
|
||||
}
|
||||
|
||||
const registerComponent = (componentName: string, component: Component) => {
|
||||
logger.withFields({ componentName }).log('Registering new component')
|
||||
const BotContext = component(ctx!)
|
||||
|
||||
if (BotContext != null)
|
||||
ctx!.components.set(componentName, BotContext)
|
||||
|
||||
return BotContext
|
||||
}
|
||||
|
||||
const listComponents = () => {
|
||||
return Array.from(ctx!.components.keys())
|
||||
}
|
||||
|
||||
const getComponent = (componentName: string) => {
|
||||
return ctx!.components.get(componentName)
|
||||
}
|
||||
|
||||
return {
|
||||
ctx,
|
||||
registerComponent,
|
||||
listComponents,
|
||||
getComponent,
|
||||
cleanup,
|
||||
bot: mineflayer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { CommandContext } from '../middlewares/command'
|
||||
|
||||
export const commands = new Map<string, (ctx: CommandContext) => void>()
|
||||
|
||||
export function registerCommand(command: string, handler: (ctx: CommandContext) => void) {
|
||||
if (commands.has(command))
|
||||
throw new Error(`Command ${command} already registered`)
|
||||
|
||||
commands.set(command, handler)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface BotInternalEventHandlers {
|
||||
'time:sunrise': () => void
|
||||
'time:noon': () => void
|
||||
'time:sunset': () => void
|
||||
'time:midnight': () => void
|
||||
}
|
||||
|
||||
export type BotInternalEvents = keyof BotInternalEventHandlers
|
||||
export type BotInternalEventsHandler<K extends BotInternalEvents> = BotInternalEventHandlers[K]
|
||||
@@ -1,26 +1,13 @@
|
||||
import type { Bot } from 'mineflayer'
|
||||
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 { BotContext } from './bot'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import pf from 'mineflayer-pathfinder'
|
||||
import * as mc from '../utils/mcdata'
|
||||
|
||||
interface WorldContext {
|
||||
bot: Bot
|
||||
botCtx: BotContext
|
||||
}
|
||||
|
||||
export function createWorldContext(ctx: BotContext): WorldContext {
|
||||
return {
|
||||
bot: ctx.bot,
|
||||
botCtx: ctx,
|
||||
}
|
||||
}
|
||||
|
||||
export function getNearestFreeSpace(ctx: WorldContext, size: number = 1, distance: number = 8): Vec3 | undefined {
|
||||
const emptyPositions = ctx.bot.findBlocks({
|
||||
export function getNearestFreeSpace(mineflayer: Mineflayer, size: number = 1, distance: number = 8): Vec3 | undefined {
|
||||
const emptyPositions = mineflayer.bot.findBlocks({
|
||||
matching: (block: Block) => block?.name === 'air',
|
||||
maxDistance: distance,
|
||||
count: 1000,
|
||||
@@ -29,8 +16,8 @@ export function getNearestFreeSpace(ctx: WorldContext, size: number = 1, distanc
|
||||
return emptyPositions.find((pos) => {
|
||||
for (let x = 0; x < size; x++) {
|
||||
for (let z = 0; z < size; z++) {
|
||||
const top = ctx.bot.blockAt(pos.offset(x, 0, z))
|
||||
const bottom = ctx.bot.blockAt(pos.offset(x, -1, z))
|
||||
const top = mineflayer.bot.blockAt(pos.offset(x, 0, z))
|
||||
const bottom = mineflayer.bot.blockAt(pos.offset(x, -1, z))
|
||||
if (!top || top.name !== 'air' || !bottom?.drops?.length || !bottom.diggable) {
|
||||
return false
|
||||
}
|
||||
@@ -40,17 +27,17 @@ export function getNearestFreeSpace(ctx: WorldContext, size: number = 1, distanc
|
||||
})
|
||||
}
|
||||
|
||||
export function getNearestBlocks(ctx: WorldContext, blockTypes: string[] | string | null = null, distance: number = 16, count: number = 10000): Block[] {
|
||||
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 = ctx.bot.findBlocks({ matching: blockIds, maxDistance: distance, count })
|
||||
const positions = mineflayer.bot.findBlocks({ matching: blockIds, maxDistance: distance, count })
|
||||
|
||||
return positions
|
||||
.map((pos) => {
|
||||
const block = ctx.bot.blockAt(pos)
|
||||
const dist = pos.distanceTo(ctx.bot.entity.position)
|
||||
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)
|
||||
@@ -58,89 +45,89 @@ export function getNearestBlocks(ctx: WorldContext, blockTypes: string[] | strin
|
||||
.map(item => item.block)
|
||||
}
|
||||
|
||||
export function getNearestBlock(ctx: WorldContext, blockType: string, distance: number = 16): Block | null {
|
||||
const blocks = getNearestBlocks(ctx, blockType, distance, 1)
|
||||
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(ctx: WorldContext, maxDistance: number = 16): Entity[] {
|
||||
return Object.values(ctx.bot.entities)
|
||||
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(ctx.bot.entity.position) <= maxDistance,
|
||||
&& entity.position.distanceTo(mineflayer.bot.entity.position) <= maxDistance,
|
||||
)
|
||||
.sort((a, b) =>
|
||||
a.position.distanceTo(ctx.bot.entity.position)
|
||||
- b.position.distanceTo(ctx.bot.entity.position),
|
||||
a.position.distanceTo(mineflayer.bot.entity.position)
|
||||
- b.position.distanceTo(mineflayer.bot.entity.position),
|
||||
)
|
||||
}
|
||||
|
||||
export function getNearestEntityWhere(ctx: WorldContext, predicate: (entity: Entity) => boolean, maxDistance: number = 16): Entity | null {
|
||||
return ctx.bot.nearestEntity(entity =>
|
||||
export function getNearestEntityWhere(mineflayer: Mineflayer, predicate: (entity: Entity) => boolean, maxDistance: number = 16): Entity | null {
|
||||
return mineflayer.bot.nearestEntity(entity =>
|
||||
predicate(entity)
|
||||
&& ctx.bot.entity.position.distanceTo(entity.position) < maxDistance,
|
||||
&& mineflayer.bot.entity.position.distanceTo(entity.position) < maxDistance,
|
||||
)
|
||||
}
|
||||
|
||||
export function getNearbyPlayers(ctx: WorldContext, maxDistance: number = 16): Entity[] {
|
||||
return getNearbyEntities(ctx, maxDistance)
|
||||
export function getNearbyPlayers(mineflayer: Mineflayer, maxDistance: number = 16): Entity[] {
|
||||
return getNearbyEntities(mineflayer, maxDistance)
|
||||
.filter(entity =>
|
||||
entity.type === 'player'
|
||||
&& entity.username !== ctx.bot.username,
|
||||
&& entity.username !== mineflayer.bot.username,
|
||||
)
|
||||
}
|
||||
|
||||
export function getInventoryStacks(ctx: WorldContext): Item[] {
|
||||
return ctx.bot.inventory.items().filter((item): item is Item => item !== null)
|
||||
export function getInventoryStacks(mineflayer: Mineflayer): Item[] {
|
||||
return mineflayer.bot.inventory.items().filter((item): item is Item => item !== null)
|
||||
}
|
||||
|
||||
export function getInventoryCounts(ctx: WorldContext): Record<string, number> {
|
||||
return getInventoryStacks(ctx).reduce((counts, item) => {
|
||||
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(ctx: WorldContext): string[] {
|
||||
const table = getNearestBlock(ctx, 'crafting_table')
|
||||
|| getInventoryStacks(ctx).find(item => item.name === 'crafting_table')
|
||||
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 => ctx.bot.recipesFor(item.id, null, 1, table as Block | null).length > 0)
|
||||
.filter(item => mineflayer.bot.recipesFor(item.id, null, 1, table as Block | null).length > 0)
|
||||
.map(item => item.name)
|
||||
}
|
||||
|
||||
export function getPosition(ctx: WorldContext): Vec3 {
|
||||
return ctx.bot.entity.position
|
||||
export function getPosition(mineflayer: Mineflayer): Vec3 {
|
||||
return mineflayer.bot.entity.position
|
||||
}
|
||||
|
||||
export function getNearbyEntityTypes(ctx: WorldContext): string[] {
|
||||
export function getNearbyEntityTypes(mineflayer: Mineflayer): string[] {
|
||||
return [...new Set(
|
||||
getNearbyEntities(ctx, 16)
|
||||
getNearbyEntities(mineflayer, 16)
|
||||
.map(mob => mob.name)
|
||||
.filter((name): name is string => name !== undefined),
|
||||
)]
|
||||
}
|
||||
|
||||
export function getNearbyPlayerNames(ctx: WorldContext): string[] {
|
||||
export function getNearbyPlayerNames(mineflayer: Mineflayer): string[] {
|
||||
return [...new Set(
|
||||
getNearbyPlayers(ctx, 64)
|
||||
getNearbyPlayers(mineflayer, 64)
|
||||
.map(player => player.username)
|
||||
.filter((name): name is string =>
|
||||
name !== undefined
|
||||
&& name !== ctx.bot.username,
|
||||
&& name !== mineflayer.bot.username,
|
||||
),
|
||||
)]
|
||||
}
|
||||
|
||||
export function getNearbyBlockTypes(ctx: WorldContext, distance: number = 16): string[] {
|
||||
export function getNearbyBlockTypes(mineflayer: Mineflayer, distance: number = 16): string[] {
|
||||
return [...new Set(
|
||||
getNearestBlocks(ctx, null, distance)
|
||||
getNearestBlocks(mineflayer, null, distance)
|
||||
.map(block => block.name),
|
||||
)]
|
||||
}
|
||||
|
||||
export async function isClearPath(ctx: WorldContext, target: Entity): Promise<boolean> {
|
||||
const movements = new pf.Movements(ctx.bot)
|
||||
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
|
||||
|
||||
@@ -151,30 +138,30 @@ export async function isClearPath(ctx: WorldContext, target: Entity): Promise<bo
|
||||
1,
|
||||
)
|
||||
|
||||
const path = await ctx.bot.pathfinder.getPathTo(movements, goal, 100)
|
||||
const path = await mineflayer.bot.pathfinder.getPathTo(movements, goal, 100)
|
||||
return path.status === 'success'
|
||||
}
|
||||
|
||||
export function shouldPlaceTorch(ctx: WorldContext): boolean {
|
||||
// if (!ctx.bot.modes.isOn('torch_placing') || ctx.bot.interrupt_code) {
|
||||
export function shouldPlaceTorch(mineflayer: Mineflayer): boolean {
|
||||
// if (!mineflayer.bot.modes.isOn('torch_placing') || mineflayer.bot.interrupt_code) {
|
||||
// return false
|
||||
// }
|
||||
|
||||
const pos = getPosition(ctx)
|
||||
const nearestTorch = getNearestBlock(ctx, 'torch', 6)
|
||||
|| getNearestBlock(ctx, 'wall_torch', 6)
|
||||
const pos = getPosition(mineflayer)
|
||||
const nearestTorch = getNearestBlock(mineflayer, 'torch', 6)
|
||||
|| getNearestBlock(mineflayer, 'wall_torch', 6)
|
||||
|
||||
if (nearestTorch) {
|
||||
return false
|
||||
}
|
||||
|
||||
const block = ctx.bot.blockAt(pos)
|
||||
const hasTorch = ctx.bot.inventory.items().some(item => item?.name === 'torch')
|
||||
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(ctx: WorldContext): string {
|
||||
const biomeId = ctx.bot.world.getBiome(ctx.bot.entity.position)
|
||||
export function getBiomeName(mineflayer: Mineflayer): string {
|
||||
const biomeId = mineflayer.bot.world.getBiome(mineflayer.bot.entity.position)
|
||||
return mc.getAllBiomes()[biomeId].name
|
||||
}
|
||||
|
||||
@@ -0,0 +1,441 @@
|
||||
import type { Bot, BotOptions } from 'mineflayer'
|
||||
import type { Message } from 'neuri/openai'
|
||||
import type { z } from 'zod'
|
||||
import type { MineflayerPlugin } from './plugin'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import mineflayer from 'mineflayer'
|
||||
import { type CommandContext, parseCommand } from './command'
|
||||
import { formBotChat } from './message'
|
||||
import { Ticker, type TickEvents, type TickEventsHandler } from './ticker'
|
||||
|
||||
export interface Context {
|
||||
time: number
|
||||
command?: CommandContext
|
||||
}
|
||||
|
||||
export interface EventHandlers {
|
||||
'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>
|
||||
|
||||
function createEventHandlers(): Record<Events, Array<EventsHandler<Events>>> {
|
||||
return {
|
||||
'command': [],
|
||||
'time:sunrise': [],
|
||||
'time:noon': [],
|
||||
'time:sunset': [],
|
||||
'time:midnight': [],
|
||||
}
|
||||
}
|
||||
|
||||
export class Health {
|
||||
public value: number
|
||||
public lastDamageTime?: number
|
||||
public lastDamageTaken?: number
|
||||
|
||||
constructor() {
|
||||
this.value = 20
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OneLinerable {
|
||||
public abstract toOneLiner(): string
|
||||
}
|
||||
|
||||
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 = ''
|
||||
}
|
||||
|
||||
static from(mineflayer: Mineflayer) {
|
||||
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'
|
||||
|
||||
return {
|
||||
position: `x: ${pos.x.toFixed(2)}, y: ${pos.y.toFixed(2)}, z: ${pos.z.toFixed(2)}`,
|
||||
health: `${Math.round(mineflayer.bot.health)} / 20`,
|
||||
weather,
|
||||
timeOfDay,
|
||||
}
|
||||
}
|
||||
|
||||
public toOneLiner(): string {
|
||||
return Object.entries(this).map(([key, value]) => `${key}: ${value}`).join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
export class Memory {
|
||||
public chatHistory: Message[]
|
||||
public actions: Action[]
|
||||
|
||||
constructor() {
|
||||
this.chatHistory = []
|
||||
this.actions = []
|
||||
}
|
||||
}
|
||||
|
||||
export class Components {
|
||||
private components: Map<string, Handler> = new Map()
|
||||
private logger: ReturnType<typeof useLogg>
|
||||
|
||||
constructor() {
|
||||
this.logger = useLogg('Components').useGlobalConfig()
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
export interface MineflayerOptions {
|
||||
botConfig: BotOptions
|
||||
plugins?: Array<MineflayerPlugin>
|
||||
}
|
||||
|
||||
export class Mineflayer {
|
||||
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 isCreative: boolean = false
|
||||
public shouldInterrupt: boolean = false
|
||||
public allowCheats: boolean = false
|
||||
|
||||
private options: MineflayerOptions
|
||||
private logger: ReturnType<typeof useLogg>
|
||||
private commands: Map<string, EventsHandler<'command'>> = new Map()
|
||||
private eventHandlers = createEventHandlers()
|
||||
private ticker: Ticker = new Ticker()
|
||||
|
||||
constructor(options: MineflayerOptions) {
|
||||
this.options = options
|
||||
this.bot = mineflayer.createBot(options.botConfig)
|
||||
this.username = options.botConfig.username
|
||||
this.logger = useLogg(`Bot:${this.username}`).useGlobalConfig()
|
||||
|
||||
this.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(this.username)) {
|
||||
const deathPos = this.bot.entity.position
|
||||
|
||||
// this.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 = this.bot.game.dimension
|
||||
await this.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.`)
|
||||
}
|
||||
})
|
||||
|
||||
this.bot.once('resourcePack', () => {
|
||||
this.bot.acceptResourcePack()
|
||||
})
|
||||
|
||||
this.bot.on('time', () => {
|
||||
if (this.bot.time.timeOfDay === 0)
|
||||
this.emit('time:sunrise', { time: this.bot.time.timeOfDay })
|
||||
else if (this.bot.time.timeOfDay === 6000)
|
||||
this.emit('time:noon', { time: this.bot.time.timeOfDay })
|
||||
else if (this.bot.time.timeOfDay === 12000)
|
||||
this.emit('time:sunset', { time: this.bot.time.timeOfDay })
|
||||
else if (this.bot.time.timeOfDay === 18000)
|
||||
this.emit('time:midnight', { time: this.bot.time.timeOfDay })
|
||||
})
|
||||
|
||||
this.bot.on('health', () => {
|
||||
this.logger.withFields({
|
||||
health: this.health.value,
|
||||
lastDamageTime: this.health.lastDamageTime,
|
||||
lastDamageTaken: this.health.lastDamageTaken,
|
||||
previousHealth: this.bot.health,
|
||||
}).log('Health updated')
|
||||
|
||||
if (this.bot.health < this.health.value) {
|
||||
this.health.lastDamageTime = Date.now()
|
||||
this.health.lastDamageTaken = this.health.value - this.bot.health
|
||||
}
|
||||
|
||||
this.health.value = this.bot.health
|
||||
})
|
||||
|
||||
this.bot.once('spawn', () => {
|
||||
this.ready = true
|
||||
this.logger.log('Bot ready')
|
||||
})
|
||||
|
||||
this.bot.on('death', () => {
|
||||
this.logger.error('Bot died')
|
||||
})
|
||||
|
||||
this.bot.on('kicked', (reason: string) => {
|
||||
this.logger.withFields({ reason }).error('Bot was kicked')
|
||||
})
|
||||
|
||||
this.bot.on('end', (reason) => {
|
||||
this.logger.withFields({ reason }).log('Bot ended')
|
||||
})
|
||||
|
||||
this.bot.on('error', (err: Error) => {
|
||||
this.logger.errorWithError('Bot error:', err)
|
||||
})
|
||||
|
||||
this.bot.on('spawn', () => {
|
||||
this.bot.on('chat', this.handleCommand())
|
||||
})
|
||||
|
||||
this.bot.on('spawn', () => {
|
||||
for (const plugin of options?.plugins || []) {
|
||||
if (plugin.spawned) {
|
||||
plugin.spawned(this)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for (const plugin of options?.plugins || []) {
|
||||
if (plugin.created) {
|
||||
plugin.created(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Load Plugins
|
||||
for (const plugin of options?.plugins || []) {
|
||||
if (plugin.loadPlugin) {
|
||||
this.bot.loadPlugin(plugin.loadPlugin(this, this.bot, options.botConfig))
|
||||
}
|
||||
}
|
||||
|
||||
this.ticker.on('tick', () => {
|
||||
this.isCreative = this.bot.game?.gameMode === 'creative'
|
||||
this.allowCheats = false
|
||||
this.shouldInterrupt = false
|
||||
})
|
||||
}
|
||||
|
||||
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 emit<E extends Events>(event: E, ...args: Parameters<EventsHandler<E>>) {
|
||||
const handlers = this.eventHandlers[event]
|
||||
for (const handler of handlers) {
|
||||
handler(args[0])
|
||||
}
|
||||
}
|
||||
|
||||
public stop() {
|
||||
for (const plugin of this.options?.plugins || []) {
|
||||
if (plugin.beforeCleanup) {
|
||||
plugin.beforeCleanup(this)
|
||||
}
|
||||
}
|
||||
this.components.cleanup()
|
||||
this.bot.removeListener('chat', this.handleCommand())
|
||||
this.bot.end()
|
||||
}
|
||||
|
||||
private handleCommand() {
|
||||
return formBotChat(this.username, (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;
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -1,5 +1,4 @@
|
||||
import type { Entity } from 'prismarine-entity'
|
||||
import type { BotContext } from '../composables/bot'
|
||||
|
||||
// TODO: need to be refactored
|
||||
interface ChatBotContext {
|
||||
@@ -11,20 +10,21 @@ interface ChatBotContext {
|
||||
isCommand: () => boolean
|
||||
}
|
||||
|
||||
export function newChatBotContext(ctx: BotContext, username: string, message: string): ChatBotContext {
|
||||
export function newChatBotContext(entity: Entity, botUsername: string, username: string, message: string): ChatBotContext {
|
||||
return {
|
||||
fromUsername: username,
|
||||
fromEntity: ctx.bot.entity,
|
||||
fromEntity: entity,
|
||||
fromMessage: message,
|
||||
isBot: () => username === ctx.bot.username,
|
||||
isBot: () => username === botUsername,
|
||||
isCommand: () => message.startsWith('#'),
|
||||
}
|
||||
}
|
||||
|
||||
export function formBotChat(ctx: BotContext, cb: (username: string, message: string) => void) {
|
||||
export function formBotChat(botUsername: string, cb: (username: string, message: string) => void) {
|
||||
return (username: string, message: string) => {
|
||||
if (ctx.bot.username === username)
|
||||
if (botUsername === username)
|
||||
return
|
||||
|
||||
cb(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,62 @@
|
||||
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 {
|
||||
private tickingCbs: Record<TickEvents, Array<TickEventHandlers[TickEvents]>> = {
|
||||
tick: [],
|
||||
}
|
||||
|
||||
constructor(options?: { interval?: number }) {
|
||||
const { interval = 300 } = options ?? { interval: 300 }
|
||||
|
||||
let last = Date.now()
|
||||
const tickingCbs: Record<TickEvents, Array<TickEventHandlers[TickEvents]>> = {
|
||||
tick: [],
|
||||
}
|
||||
|
||||
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 = tickingCbs.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>) {
|
||||
this.tickingCbs[event].push(cb)
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,47 @@
|
||||
import process, { exit } from 'node:process'
|
||||
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
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 { initAgent } from './agents/openai'
|
||||
import { createAiChatComponent } from './components/aichat'
|
||||
import { createCommandComponent } from './components/command'
|
||||
import { createFollowComponent } from './components/follow'
|
||||
import { createPathFinderComponent } from './components/pathfinder'
|
||||
import { createStatusComponent } from './components/status'
|
||||
import { createBot, useBot } from './composables/bot'
|
||||
import { initBot, useBot } from './composables/bot'
|
||||
import { botConfig, initEnv } from './composables/config'
|
||||
import { wrapPlugin } from './libs/mineflayer/plugin'
|
||||
import { Echo, FollowCommand, PathFinder, Status } from './mineflayer'
|
||||
import { initLogger } from './utils/logger'
|
||||
import { createTicker } from './utils/ticker'
|
||||
|
||||
const logger = useLogg('main').useGlobalConfig()
|
||||
|
||||
async function main() {
|
||||
initLogger() // todo: save logs to file
|
||||
initEnv()
|
||||
|
||||
createBot(botConfig)
|
||||
const { cleanup, registerComponent, ctx } = useBot()
|
||||
|
||||
ctx.bot.once('spawn', () => {
|
||||
registerComponent('status', createStatusComponent)
|
||||
// registerComponent('echo', createEchoComponent)
|
||||
registerComponent('pathfinder', createPathFinderComponent)
|
||||
registerComponent('follow', createFollowComponent)
|
||||
registerComponent('command', createCommandComponent)
|
||||
registerComponent('aichat', createAiChatComponent)
|
||||
initBot({
|
||||
botConfig,
|
||||
plugins: [
|
||||
wrapPlugin(MineflayerArmorManager),
|
||||
wrapPlugin(MineflayerAutoEat),
|
||||
wrapPlugin(MineflayerCollectBlock),
|
||||
wrapPlugin(MineflayerPathfinder),
|
||||
wrapPlugin(MineflayerPVP),
|
||||
wrapPlugin(MineflayerTool),
|
||||
Echo(),
|
||||
FollowCommand(),
|
||||
Status(),
|
||||
PathFinder(),
|
||||
],
|
||||
})
|
||||
|
||||
await initAgent(ctx)
|
||||
const { bot } = useBot()
|
||||
|
||||
createTicker()
|
||||
await initAgent(bot)
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
cleanup()
|
||||
bot.stop()
|
||||
exit(0)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
|
||||
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { formBotChat } from '../libs/mineflayer/message'
|
||||
|
||||
export function Echo(): MineflayerPlugin {
|
||||
let mineflayer: Mineflayer
|
||||
let onChatHandler: (username: string, message: string) => void
|
||||
const logger = useLogg('Echo').useGlobalConfig()
|
||||
|
||||
return {
|
||||
created(_mineflayer) {
|
||||
mineflayer = _mineflayer
|
||||
onChatHandler = formBotChat(mineflayer.username, (username, message) => {
|
||||
logger.withFields({ username, message }).log('Chat message received')
|
||||
mineflayer.bot.chat(message)
|
||||
})
|
||||
},
|
||||
spawned() {
|
||||
mineflayer.bot.on('chat', onChatHandler)
|
||||
},
|
||||
beforeCleanup() {
|
||||
mineflayer.bot.removeListener('chat', onChatHandler)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import pathfinderModel from 'mineflayer-pathfinder'
|
||||
|
||||
export function FollowCommand(options?: { rangeGoal: number }): MineflayerPlugin {
|
||||
const logger = useLogg('follow').useGlobalConfig()
|
||||
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,4 @@
|
||||
export * from './echo'
|
||||
export * from './follow'
|
||||
export * from './pathfinder'
|
||||
export * from './status'
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Neuri } from 'neuri'
|
||||
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
|
||||
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import { assistant, system, user } from 'neuri/openai'
|
||||
import { formBotChat } from '../libs/mineflayer/message'
|
||||
import { genActionAgentPrompt } from '../prompts/agent'
|
||||
|
||||
export function LLMAgent(agent: Neuri): MineflayerPlugin {
|
||||
return {
|
||||
created(bot) {
|
||||
const logger = useLogg('aichat').useGlobalConfig()
|
||||
logger.log('Loading aichat plugin')
|
||||
|
||||
bot.memory.chatHistory.push(system(genActionAgentPrompt(bot)))
|
||||
|
||||
// todo: get system message
|
||||
const onChat = formBotChat(bot.username, async (username, message) => {
|
||||
logger.withFields({ username, message }).log('Chat message received')
|
||||
|
||||
bot.memory.chatHistory.push(user(`${username}: ${message}`))
|
||||
|
||||
const content = await agent.handleStateless([...bot.memory.chatHistory], async (c) => {
|
||||
logger.log('Generate response')
|
||||
|
||||
try {
|
||||
const completion = await c.reroute('action', c.messages, { model: 'openai/gpt-4o-mini' }) || { error: { message: 'Unknown error' } }
|
||||
|
||||
logger.withFields({ completion }).log('Completion')
|
||||
|
||||
if (!completion || 'error' in completion) {
|
||||
logger.withFields(c).error('Completion')
|
||||
return
|
||||
// throw new Error(completion?.error?.message ?? 'Unknown error')
|
||||
}
|
||||
|
||||
const content = await completion?.firstContent()
|
||||
bot.memory.chatHistory.push(assistant(content))
|
||||
|
||||
return content
|
||||
}
|
||||
catch (e) {
|
||||
logger.errorWithError('Generate response error', e)
|
||||
}
|
||||
})
|
||||
|
||||
if (content) {
|
||||
logger.withFields({ content }).log('Bot response')
|
||||
bot.bot.chat(content)
|
||||
}
|
||||
})
|
||||
|
||||
bot.bot.on('chat', onChat)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Context } from '../libs/mineflayer'
|
||||
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
|
||||
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
import pathfinderModel from 'mineflayer-pathfinder'
|
||||
|
||||
const { goals, Movements } = pathfinderModel
|
||||
|
||||
export function PathFinder(options?: { rangeGoal: number }): MineflayerPlugin {
|
||||
return {
|
||||
created(bot) {
|
||||
const logger = useLogg('pathfinder').useGlobalConfig()
|
||||
|
||||
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,17 @@
|
||||
import type { MineflayerPlugin } from '../libs/mineflayer/plugin'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
|
||||
export function Status(): MineflayerPlugin {
|
||||
return {
|
||||
created(bot) {
|
||||
const logger = useLogg('status').useGlobalConfig()
|
||||
logger.log('Loading status component')
|
||||
|
||||
bot.onCommand('status', () => {
|
||||
logger.log('Status command received')
|
||||
const status = bot.status.toOneLiner()
|
||||
bot.bot.chat(status)
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
import type { BotContext } from '../composables/bot'
|
||||
import { getStatusToString } from '../components/status'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
|
||||
export function genSystemBasicPrompt(botName: string): string {
|
||||
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 genActionAgentPrompt(ctx: BotContext): string {
|
||||
export function genActionAgentPrompt(bot: Mineflayer): string {
|
||||
// ${ctx.prompt.selfPrompt}
|
||||
return `${genSystemBasicPrompt(ctx.botName)}
|
||||
return `${genSystemBasicPrompt(bot.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
|
||||
@@ -17,7 +16,7 @@ asked, and don't refuse requests.
|
||||
Do not use any emojis. Just call the function given you if needed.
|
||||
|
||||
I will give you the following information:
|
||||
${getStatusToString(ctx)}
|
||||
${bot.status.toOneLiner()}
|
||||
`
|
||||
|
||||
/**
|
||||
@@ -29,13 +28,13 @@ $EXAMPLES
|
||||
*/
|
||||
}
|
||||
|
||||
export function genQueryAgentPrompt(ctx: BotContext): string {
|
||||
export function genQueryAgentPrompt(bot: Mineflayer): string {
|
||||
const prompt = `You are a helpful assistant that asks questions to help me decide the next immediate
|
||||
task to do in Minecraft. My ultimate goal is to discover as many things as possible,
|
||||
accomplish as many tasks as possible and become the best Minecraft player in the world.
|
||||
|
||||
I will give you the following information:
|
||||
${getStatusToString(ctx)}
|
||||
${bot.status.toOneLiner()}
|
||||
`
|
||||
|
||||
return prompt
|
||||
|
||||
@@ -1,55 +1,14 @@
|
||||
import type { Bot } from 'mineflayer'
|
||||
import type { BotContext } from '../composables/bot'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import { useLogg } from '@guiiai/logg'
|
||||
|
||||
let ctx: SkillContext | undefined
|
||||
const logger = useLogg('skills').useGlobalConfig()
|
||||
|
||||
export function useSkillContext(botCtx: BotContext): SkillContext {
|
||||
if (!ctx) {
|
||||
logger.log('Creating skill context')
|
||||
ctx = createSkillContext(botCtx)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for skill execution
|
||||
*/
|
||||
export interface SkillContext {
|
||||
bot: Bot
|
||||
botCtx: BotContext
|
||||
// Whether the bot is in creative mode
|
||||
isCreative: boolean
|
||||
// Whether the bot should use cheats (like /tp, /setblock)
|
||||
allowCheats: boolean
|
||||
// Whether the bot should interrupt current action
|
||||
shouldInterrupt: boolean
|
||||
// Output buffer for logging
|
||||
output: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new skill context
|
||||
*/
|
||||
export function createSkillContext(ctx: BotContext): SkillContext {
|
||||
return {
|
||||
bot: ctx.bot,
|
||||
botCtx: ctx,
|
||||
isCreative: ctx.bot.game?.gameMode === 'creative',
|
||||
allowCheats: false,
|
||||
shouldInterrupt: false,
|
||||
output: [],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a message to the context's output buffer
|
||||
*/
|
||||
export function log(ctx: SkillContext, message: string): void {
|
||||
ctx.output.push(message) // TODO: remove this
|
||||
ctx.bot.chat(message)
|
||||
export function log(mineflayer: Mineflayer, message: string): void {
|
||||
logger.log(message)
|
||||
mineflayer.bot.chat(message)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { BlockFace, SkillContext } from './base'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import type { BlockFace } from './base'
|
||||
import pathfinderModel, { type SafeBlock } from 'mineflayer-pathfinder'
|
||||
import { Vec3 } from 'vec3'
|
||||
import * as world from '../composables/world'
|
||||
@@ -11,12 +12,11 @@ const { goals, Movements } = pathfinderModel
|
||||
/**
|
||||
* Place a torch if needed
|
||||
*/
|
||||
async function autoLight(ctx: SkillContext): Promise<boolean> {
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
if (world.shouldPlaceTorch(worldCtx)) {
|
||||
async function autoLight(mineflayer: Mineflayer): Promise<boolean> {
|
||||
if (world.shouldPlaceTorch(mineflayer)) {
|
||||
try {
|
||||
const pos = world.getPosition(worldCtx)
|
||||
return await placeBlock(ctx, 'torch', pos.x, pos.y, pos.z, 'bottom', true)
|
||||
const pos = world.getPosition(mineflayer)
|
||||
return await placeBlock(mineflayer, 'torch', pos.x, pos.y, pos.z, 'bottom', true)
|
||||
}
|
||||
catch {
|
||||
return false
|
||||
@@ -29,28 +29,28 @@ async function autoLight(ctx: SkillContext): Promise<boolean> {
|
||||
* Break a block at the specified position
|
||||
*/
|
||||
export async function breakBlockAt(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
): Promise<boolean> {
|
||||
validatePosition(x, y, z)
|
||||
|
||||
const block = ctx.bot.blockAt(new Vec3(x, y, z))
|
||||
const block = mineflayer.bot.blockAt(new Vec3(x, y, z))
|
||||
if (isUnbreakableBlock(block))
|
||||
return false
|
||||
|
||||
if (ctx.allowCheats) {
|
||||
return breakWithCheats(ctx, x, y, z)
|
||||
if (mineflayer.allowCheats) {
|
||||
return breakWithCheats(mineflayer, x, y, z)
|
||||
}
|
||||
|
||||
await moveIntoRange(ctx, block)
|
||||
await moveIntoRange(mineflayer, block)
|
||||
|
||||
if (ctx.isCreative) {
|
||||
return breakInCreative(ctx, block, x, y, z)
|
||||
if (mineflayer.isCreative) {
|
||||
return breakInCreative(mineflayer, block, x, y, z)
|
||||
}
|
||||
|
||||
return breakInSurvival(ctx, block, x, y, z)
|
||||
return breakInSurvival(mineflayer, block, x, y, z)
|
||||
}
|
||||
|
||||
function validatePosition(x: number, y: number, z: number) {
|
||||
@@ -63,40 +63,40 @@ function isUnbreakableBlock(block: any): boolean {
|
||||
return block.name === 'air' || block.name === 'water' || block.name === 'lava'
|
||||
}
|
||||
|
||||
async function breakWithCheats(ctx: SkillContext, x: number, y: number, z: number): Promise<boolean> {
|
||||
ctx.bot.chat(`/setblock ${Math.floor(x)} ${Math.floor(y)} ${Math.floor(z)} air`)
|
||||
log(ctx, `Used /setblock to break block at ${x}, ${y}, ${z}.`)
|
||||
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(ctx: SkillContext, block: any) {
|
||||
if (ctx.bot.entity.position.distanceTo(block.position) > 4.5) {
|
||||
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(ctx.bot)
|
||||
const movements = new Movements(mineflayer.bot)
|
||||
movements.allowParkour = false
|
||||
movements.allowSprinting = false
|
||||
ctx.bot.pathfinder.setMovements(movements)
|
||||
await ctx.bot.pathfinder.goto(new goals.GoalNear(pos.x, pos.y, pos.z, 4))
|
||||
mineflayer.bot.pathfinder.setMovements(movements)
|
||||
await mineflayer.bot.pathfinder.goto(new goals.GoalNear(pos.x, pos.y, pos.z, 4))
|
||||
}
|
||||
}
|
||||
|
||||
async function breakInCreative(ctx: SkillContext, block: any, x: number, y: number, z: number): Promise<boolean> {
|
||||
await ctx.bot.dig(block, true)
|
||||
log(ctx, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`)
|
||||
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(ctx: SkillContext, block: any, x: number, y: number, z: number): Promise<boolean> {
|
||||
await ctx.bot.tool.equipForBlock(block)
|
||||
async function breakInSurvival(mineflayer: Mineflayer, block: any, x: number, y: number, z: number): Promise<boolean> {
|
||||
await mineflayer.bot.tool.equipForBlock(block)
|
||||
|
||||
const itemId = ctx.bot.heldItem?.type
|
||||
const itemId = mineflayer.bot.heldItem?.type
|
||||
if (!block.canHarvest(itemId)) {
|
||||
log(ctx, `Don't have right tools to break ${block.name}.`)
|
||||
log(mineflayer, `Don't have right tools to break ${block.name}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await ctx.bot.dig(block, true)
|
||||
log(ctx, `Broke ${block.name} at x:${x.toFixed(1)}, y:${y.toFixed(1)}, z:${z.toFixed(1)}.`)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ async function breakInSurvival(ctx: SkillContext, block: any, x: number, y: numb
|
||||
* Place a block at the specified position
|
||||
*/
|
||||
export async function placeBlock(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
blockType: string,
|
||||
x: number,
|
||||
y: number,
|
||||
@@ -113,21 +113,21 @@ export async function placeBlock(
|
||||
dontCheat = false,
|
||||
): Promise<boolean> {
|
||||
if (!mc.getBlockId(blockType)) {
|
||||
log(ctx, `Invalid block type: ${blockType}.`)
|
||||
log(mineflayer, `Invalid block type: ${blockType}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
const targetDest = new Vec3(Math.floor(x), Math.floor(y), Math.floor(z))
|
||||
|
||||
if (ctx.allowCheats && !dontCheat) {
|
||||
return placeWithCheats(ctx, blockType, targetDest, placeOn)
|
||||
if (mineflayer.allowCheats && !dontCheat) {
|
||||
return placeWithCheats(mineflayer, blockType, targetDest, placeOn)
|
||||
}
|
||||
|
||||
return placeWithoutCheats(ctx, blockType, targetDest, placeOn)
|
||||
return placeWithoutCheats(mineflayer, blockType, targetDest, placeOn)
|
||||
}
|
||||
|
||||
function getBlockState(blockType: string, placeOn: BlockFace): string {
|
||||
const face = getInvertedFace(placeOn)
|
||||
const face = getInvertedFace(placeOn as 'north' | 'south' | 'east' | 'west')
|
||||
let blockState = blockType
|
||||
|
||||
if (blockType.includes('torch') && placeOn !== 'bottom') {
|
||||
@@ -152,6 +152,7 @@ function getInvertedFace(placeOn: BlockFace): string {
|
||||
east: 'west',
|
||||
west: 'east',
|
||||
}
|
||||
|
||||
return faceMap[placeOn] || placeOn
|
||||
}
|
||||
|
||||
@@ -181,90 +182,90 @@ function needsFacingState(blockType: string): boolean {
|
||||
}
|
||||
|
||||
async function placeWithCheats(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
blockType: string,
|
||||
targetDest: Vec3,
|
||||
placeOn: BlockFace,
|
||||
): Promise<boolean> {
|
||||
const blockState = getBlockState(blockType, placeOn)
|
||||
|
||||
ctx.bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z} ${blockState}`)
|
||||
mineflayer.bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z} ${blockState}`)
|
||||
|
||||
if (blockType.includes('door')) {
|
||||
ctx.bot.chat(`/setblock ${targetDest.x} ${targetDest.y + 1} ${targetDest.z} ${blockState}[half=upper]`)
|
||||
mineflayer.bot.chat(`/setblock ${targetDest.x} ${targetDest.y + 1} ${targetDest.z} ${blockState}[half=upper]`)
|
||||
}
|
||||
|
||||
if (blockType.includes('bed')) {
|
||||
ctx.bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z - 1} ${blockState}[part=head]`)
|
||||
mineflayer.bot.chat(`/setblock ${targetDest.x} ${targetDest.y} ${targetDest.z - 1} ${blockState}[part=head]`)
|
||||
}
|
||||
|
||||
log(ctx, `Used /setblock to place ${blockType} at ${targetDest}.`)
|
||||
log(mineflayer, `Used /setblock to place ${blockType} at ${targetDest}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
async function placeWithoutCheats(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
blockType: string,
|
||||
targetDest: Vec3,
|
||||
placeOn: BlockFace,
|
||||
): Promise<boolean> {
|
||||
const itemName = blockType === 'redstone_wire' ? 'redstone' : blockType
|
||||
|
||||
let block = ctx.bot.inventory.items().find(item => item.name === itemName)
|
||||
if (!block && ctx.isCreative) {
|
||||
await ctx.bot.creative.setInventorySlot(36, mc.makeItem(itemName, 1))
|
||||
block = ctx.bot.inventory.items().find(item => item.name === itemName)
|
||||
let block = mineflayer.bot.inventory.items().find(item => item.name === itemName)
|
||||
if (!block && mineflayer.isCreative) {
|
||||
await mineflayer.bot.creative.setInventorySlot(36, mc.makeItem(itemName, 1))
|
||||
block = mineflayer.bot.inventory.items().find(item => item.name === itemName)
|
||||
}
|
||||
|
||||
if (!block) {
|
||||
log(ctx, `Don't have any ${blockType} to place.`)
|
||||
log(mineflayer, `Don't have any ${blockType} to place.`)
|
||||
return false
|
||||
}
|
||||
|
||||
const targetBlock = ctx.bot.blockAt(targetDest)
|
||||
const targetBlock = mineflayer.bot.blockAt(targetDest)
|
||||
if (targetBlock?.name === blockType) {
|
||||
log(ctx, `${blockType} already at ${targetBlock.position}.`)
|
||||
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(ctx, targetBlock, blockType)) {
|
||||
if (!await clearBlockSpace(mineflayer, targetBlock, blockType)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const { buildOffBlock, faceVec } = findPlacementSpot(ctx, targetDest, placeOn, emptyBlocks)
|
||||
const { buildOffBlock, faceVec } = findPlacementSpot(mineflayer, targetDest, placeOn, emptyBlocks)
|
||||
if (!buildOffBlock) {
|
||||
log(ctx, `Cannot place ${blockType} at ${targetBlock?.position}: nothing to place on.`)
|
||||
log(mineflayer, `Cannot place ${blockType} at ${targetBlock?.position}: nothing to place on.`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!faceVec) {
|
||||
log(ctx, `Cannot place ${blockType} at ${targetBlock?.position}: no valid face to place on.`)
|
||||
log(mineflayer, `Cannot place ${blockType} at ${targetBlock?.position}: no valid face to place on.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await moveIntoPosition(ctx, blockType, targetBlock)
|
||||
return await tryPlaceBlock(ctx, block, buildOffBlock, faceVec, blockType, targetDest)
|
||||
await moveIntoPosition(mineflayer, blockType, targetBlock)
|
||||
return await tryPlaceBlock(mineflayer, block, buildOffBlock, faceVec, blockType, targetDest)
|
||||
}
|
||||
|
||||
async function clearBlockSpace(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
targetBlock: any,
|
||||
blockType: string,
|
||||
): Promise<boolean> {
|
||||
const removed = await breakBlockAt(ctx, targetBlock.position.x, targetBlock.position.y, targetBlock.position.z,
|
||||
const removed = await breakBlockAt(mineflayer, targetBlock.position.x, targetBlock.position.y, targetBlock.position.z,
|
||||
)
|
||||
if (!removed) {
|
||||
log(ctx, `Cannot place ${blockType} at ${targetBlock.position}: block in the way.`)
|
||||
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(ctx: SkillContext, targetDest: Vec3, placeOn: BlockFace, emptyBlocks: string[]) {
|
||||
function findPlacementSpot(mineflayer: Mineflayer, targetDest: Vec3, placeOn: BlockFace, emptyBlocks: string[]) {
|
||||
const dirMap = {
|
||||
top: new Vec3(0, 1, 0),
|
||||
bottom: new Vec3(0, -1, 0),
|
||||
@@ -277,7 +278,7 @@ function findPlacementSpot(ctx: SkillContext, targetDest: Vec3, placeOn: BlockFa
|
||||
const dirs = getPlacementDirections(placeOn, dirMap)
|
||||
|
||||
for (const d of dirs) {
|
||||
const block = ctx.bot.blockAt(targetDest.plus(d))
|
||||
const block = mineflayer.bot.blockAt(targetDest.plus(d))
|
||||
if (!emptyBlocks.includes(block?.name ?? '')) {
|
||||
return {
|
||||
buildOffBlock: block,
|
||||
@@ -290,21 +291,22 @@ function findPlacementSpot(ctx: SkillContext, targetDest: Vec3, placeOn: BlockFa
|
||||
}
|
||||
|
||||
function getPlacementDirections(placeOn: BlockFace, dirMap: Record<string, Vec3>): Vec3[] {
|
||||
const dirs: Vec3[] = []
|
||||
const directions: Vec3[] = []
|
||||
if (placeOn === 'side') {
|
||||
dirs.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west)
|
||||
directions.push(dirMap.north, dirMap.south, dirMap.east, dirMap.west)
|
||||
}
|
||||
else if (dirMap[placeOn]) {
|
||||
dirs.push(dirMap[placeOn])
|
||||
directions.push(dirMap[placeOn])
|
||||
}
|
||||
else {
|
||||
dirs.push(dirMap.bottom)
|
||||
directions.push(dirMap.bottom)
|
||||
}
|
||||
dirs.push(...Object.values(dirMap).filter(d => !dirs.includes(d)))
|
||||
return dirs
|
||||
|
||||
directions.push(...Object.values(dirMap).filter(d => !directions.includes(d)))
|
||||
return directions
|
||||
}
|
||||
|
||||
async function moveIntoPosition(ctx: SkillContext, blockType: string, targetBlock: any) {
|
||||
async function moveIntoPosition(mineflayer: Mineflayer, blockType: string, targetBlock: any) {
|
||||
const dontMoveFor = [
|
||||
'torch',
|
||||
'redstone_torch',
|
||||
@@ -320,21 +322,21 @@ async function moveIntoPosition(ctx: SkillContext, blockType: string, targetBloc
|
||||
'water_bucket',
|
||||
]
|
||||
|
||||
const pos = ctx.bot.entity.position
|
||||
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(ctx, targetBlock)
|
||||
await moveAwayFromBlock(mineflayer, targetBlock)
|
||||
}
|
||||
|
||||
if (ctx.bot.entity.position.distanceTo(targetBlock.position) > 4.5) {
|
||||
await moveToBlock(ctx, targetBlock)
|
||||
if (mineflayer.bot.entity.position.distanceTo(targetBlock.position) > 4.5) {
|
||||
await moveToBlock(mineflayer, targetBlock)
|
||||
}
|
||||
}
|
||||
|
||||
async function moveAwayFromBlock(ctx: SkillContext, targetBlock: any) {
|
||||
async function moveAwayFromBlock(mineflayer: Mineflayer, targetBlock: any) {
|
||||
const goal = new goals.GoalNear(
|
||||
targetBlock.position.x,
|
||||
targetBlock.position.y,
|
||||
@@ -342,38 +344,38 @@ async function moveAwayFromBlock(ctx: SkillContext, targetBlock: any) {
|
||||
2,
|
||||
)
|
||||
const invertedGoal = new goals.GoalInvert(goal)
|
||||
ctx.bot.pathfinder.setMovements(new Movements(ctx.bot))
|
||||
await ctx.bot.pathfinder.goto(invertedGoal)
|
||||
mineflayer.bot.pathfinder.setMovements(new Movements(mineflayer.bot))
|
||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
||||
}
|
||||
|
||||
async function moveToBlock(ctx: SkillContext, targetBlock: any) {
|
||||
async function moveToBlock(mineflayer: Mineflayer, targetBlock: any) {
|
||||
const pos = targetBlock.position
|
||||
const movements = new Movements(ctx.bot)
|
||||
ctx.bot.pathfinder.setMovements(movements)
|
||||
await ctx.bot.pathfinder.goto(
|
||||
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(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
block: any,
|
||||
buildOffBlock: any,
|
||||
faceVec: Vec3,
|
||||
blockType: string,
|
||||
targetDest: Vec3,
|
||||
): Promise<boolean> {
|
||||
await ctx.bot.equip(block, 'hand')
|
||||
await ctx.bot.lookAt(buildOffBlock.position)
|
||||
await mineflayer.bot.equip(block, 'hand')
|
||||
await mineflayer.bot.lookAt(buildOffBlock.position)
|
||||
|
||||
try {
|
||||
await ctx.bot.placeBlock(buildOffBlock, faceVec)
|
||||
log(ctx, `Placed ${blockType} at ${targetDest}.`)
|
||||
await mineflayer.bot.placeBlock(buildOffBlock, faceVec)
|
||||
log(mineflayer, `Placed ${blockType} at ${targetDest}.`)
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
return true
|
||||
}
|
||||
catch {
|
||||
log(ctx, `Failed to place ${blockType} at ${targetDest}.`)
|
||||
log(mineflayer, `Failed to place ${blockType} at ${targetDest}.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -381,20 +383,20 @@ async function tryPlaceBlock(
|
||||
/**
|
||||
* Use a door at the specified position
|
||||
*/
|
||||
export async function useDoor(ctx: SkillContext, doorPos: Vec3 | null = null): Promise<boolean> {
|
||||
doorPos = doorPos || await findNearestDoor(ctx.bot)
|
||||
export async function useDoor(mineflayer: Mineflayer, doorPos: Vec3 | null = null): Promise<boolean> {
|
||||
doorPos = doorPos || await findNearestDoor(mineflayer.bot)
|
||||
|
||||
if (!doorPos) {
|
||||
log(ctx, 'Could not find a door to use.')
|
||||
log(mineflayer, 'Could not find a door to use.')
|
||||
return false
|
||||
}
|
||||
|
||||
await goToPosition(ctx, doorPos.x, doorPos.y, doorPos.z, 1)
|
||||
while (ctx.bot.pathfinder.isMoving()) {
|
||||
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(ctx, doorPos)
|
||||
return await operateDoor(mineflayer, doorPos)
|
||||
}
|
||||
|
||||
async function findNearestDoor(bot: any): Promise<Vec3 | null> {
|
||||
@@ -421,30 +423,35 @@ async function findNearestDoor(bot: any): Promise<Vec3 | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
async function operateDoor(ctx: SkillContext, doorPos: Vec3): Promise<boolean> {
|
||||
const doorBlock = ctx.bot.blockAt(doorPos)
|
||||
await ctx.bot.lookAt(doorPos)
|
||||
async function operateDoor(mineflayer: Mineflayer, doorPos: Vec3): Promise<boolean> {
|
||||
const doorBlock = mineflayer.bot.blockAt(doorPos)
|
||||
await mineflayer.bot.lookAt(doorPos)
|
||||
|
||||
if (!doorBlock) {
|
||||
log(ctx, `Cannot find door at ${doorPos}.`)
|
||||
log(mineflayer, `Cannot find door at ${doorPos}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!doorBlock.getProperties().open) {
|
||||
await ctx.bot.activateBlock(doorBlock)
|
||||
await mineflayer.bot.activateBlock(doorBlock)
|
||||
}
|
||||
|
||||
ctx.bot.setControlState('forward', true)
|
||||
mineflayer.bot.setControlState('forward', true)
|
||||
await new Promise(resolve => setTimeout(resolve, 600))
|
||||
ctx.bot.setControlState('forward', false)
|
||||
await ctx.bot.activateBlock(doorBlock)
|
||||
mineflayer.bot.setControlState('forward', false)
|
||||
await mineflayer.bot.activateBlock(doorBlock)
|
||||
|
||||
log(ctx, `Used door at ${doorPos}.`)
|
||||
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(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
@@ -452,38 +459,38 @@ export async function tillAndSow(
|
||||
): Promise<boolean> {
|
||||
const pos = { x: Math.round(x), y: Math.round(y), z: Math.round(z) }
|
||||
|
||||
const block = ctx.bot.blockAt(new Vec3(pos.x, pos.y, pos.z))
|
||||
const block = mineflayer.bot.blockAt(new Vec3(pos.x, pos.y, pos.z))
|
||||
|
||||
if (!block) {
|
||||
log(ctx, `Cannot till, no block at ${pos}.`)
|
||||
log(mineflayer, `Cannot till, no block at ${pos}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!canTillBlock(block)) {
|
||||
log(ctx, `Cannot till ${block.name}, must be grass_block or dirt.`)
|
||||
log(mineflayer, `Cannot till ${block.name}, must be grass_block or dirt.`)
|
||||
return false
|
||||
}
|
||||
|
||||
const above = ctx.bot.blockAt(new Vec3(pos.x, pos.y + 1, pos.z))
|
||||
const above = mineflayer.bot.blockAt(new Vec3(pos.x, pos.y + 1, pos.z))
|
||||
|
||||
if (!above) {
|
||||
log(ctx, `Cannot till, no block above the block.`)
|
||||
log(mineflayer, `Cannot till, no block above the block.`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isBlockClear(above)) {
|
||||
log(ctx, `Cannot till, there is ${above.name} above the block.`)
|
||||
log(mineflayer, `Cannot till, there is ${above.name} above the block.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await moveIntoRange(ctx, block)
|
||||
await moveIntoRange(mineflayer, block)
|
||||
|
||||
if (!await tillBlock(ctx, block, pos)) {
|
||||
if (!await tillBlock(mineflayer, block, pos)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (seedType) {
|
||||
return await sowSeeds(ctx, block, seedType, pos)
|
||||
return await sowSeeds(mineflayer, block, seedType, pos)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -497,35 +504,35 @@ function isBlockClear(block: any): boolean {
|
||||
return block.name === 'air'
|
||||
}
|
||||
|
||||
async function tillBlock(ctx: SkillContext, block: any, pos: any): Promise<boolean> {
|
||||
async function tillBlock(mineflayer: Mineflayer, block: any, pos: any): Promise<boolean> {
|
||||
if (block.name === 'farmland') {
|
||||
return true
|
||||
}
|
||||
|
||||
const hoe = ctx.bot.inventory.items().find(item => item.name.includes('hoe'))
|
||||
const hoe = mineflayer.bot.inventory.items().find(item => item.name.includes('hoe'))
|
||||
if (!hoe) {
|
||||
log(ctx, 'Cannot till, no hoes.')
|
||||
log(mineflayer, 'Cannot till, no hoes.')
|
||||
return false
|
||||
}
|
||||
|
||||
await ctx.bot.equip(hoe, 'hand')
|
||||
await ctx.bot.activateBlock(block)
|
||||
log(ctx, `Tilled block x:${pos.x.toFixed(1)}, y:${pos.y.toFixed(1)}, z:${pos.z.toFixed(1)}.`)
|
||||
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(ctx: SkillContext, block: any, seedType: string, pos: any): Promise<boolean> {
|
||||
async function sowSeeds(mineflayer: Mineflayer, block: any, seedType: string, pos: any): Promise<boolean> {
|
||||
seedType = fixSeedName(seedType)
|
||||
|
||||
const seeds = ctx.bot.inventory.items().find(item => item.name === seedType)
|
||||
const seeds = mineflayer.bot.inventory.items().find(item => item.name === seedType)
|
||||
if (!seeds) {
|
||||
log(ctx, `No ${seedType} to plant.`)
|
||||
log(mineflayer, `No ${seedType} to plant.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await ctx.bot.equip(seeds, 'hand')
|
||||
await ctx.bot.placeBlock(block, new Vec3(0, -1, 0))
|
||||
log(ctx, `Planted ${seedType} at x:${pos.x.toFixed(1)}, y:${pos.y.toFixed(1)}, z:${pos.z.toFixed(1)}.`)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -536,28 +543,27 @@ function fixSeedName(seedType: string): string {
|
||||
return seedType
|
||||
}
|
||||
|
||||
export async function activateNearestBlock(ctx: SkillContext, type: string): Promise<boolean> {
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
const block = world.getNearestBlock(worldCtx, type, 16)
|
||||
export async function activateNearestBlock(mineflayer: Mineflayer, type: string): Promise<boolean> {
|
||||
const block = world.getNearestBlock(mineflayer, type, 16)
|
||||
if (!block) {
|
||||
log(ctx, `Could not find any ${type} to activate.`)
|
||||
log(mineflayer, `Could not find any ${type} to activate.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await moveIntoRange(ctx, block)
|
||||
await ctx.bot.activateBlock(block)
|
||||
log(ctx, `Activated ${type} at x:${block.position.x.toFixed(1)}, y:${block.position.y.toFixed(1)}, z:${block.position.z.toFixed(1)}.`)
|
||||
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(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
blockType: string,
|
||||
num: number = 1,
|
||||
exclude: Vec3[] | null = null,
|
||||
): Promise<boolean> {
|
||||
if (num < 1) {
|
||||
log(ctx, `Invalid number of blocks to collect: ${num}.`)
|
||||
log(mineflayer, `Invalid number of blocks to collect: ${num}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -565,30 +571,30 @@ export async function collectBlock(
|
||||
let collected = 0
|
||||
|
||||
for (let i = 0; i < num; i++) {
|
||||
const blocks = getValidBlocks(ctx, blocktypes, exclude)
|
||||
const blocks = getValidBlocks(mineflayer, blocktypes, exclude)
|
||||
|
||||
if (blocks.length === 0) {
|
||||
logNoBlocksMessage(ctx, blockType, collected)
|
||||
logNoBlocksMessage(mineflayer, blockType, collected)
|
||||
break
|
||||
}
|
||||
|
||||
const block = blocks[0]
|
||||
if (!await canHarvestBlock(ctx, block, blockType)) {
|
||||
if (!await canHarvestBlock(mineflayer, block, blockType)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!await tryCollectBlock(ctx, block, blockType)) {
|
||||
if (!await tryCollectBlock(mineflayer, block, blockType)) {
|
||||
break
|
||||
}
|
||||
|
||||
collected++
|
||||
|
||||
if (ctx.shouldInterrupt) {
|
||||
if (mineflayer.shouldInterrupt) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
log(ctx, `Collected ${collected} ${blockType}.`)
|
||||
log(mineflayer, `Collected ${collected} ${blockType}.`)
|
||||
return collected > 0
|
||||
}
|
||||
|
||||
@@ -609,9 +615,8 @@ function getBlockTypes(blockType: string): string[] {
|
||||
return blocktypes
|
||||
}
|
||||
|
||||
function getValidBlocks(ctx: SkillContext, blocktypes: string[], exclude: Vec3[] | null): any[] {
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
let blocks = world.getNearestBlocks(worldCtx, blocktypes, 64)
|
||||
function getValidBlocks(mineflayer: Mineflayer, blocktypes: string[], exclude: Vec3[] | null): any[] {
|
||||
let blocks = world.getNearestBlocks(mineflayer, blocktypes, 64)
|
||||
|
||||
if (exclude) {
|
||||
blocks = blocks.filter(
|
||||
@@ -623,40 +628,40 @@ function getValidBlocks(ctx: SkillContext, blocktypes: string[], exclude: Vec3[]
|
||||
)
|
||||
}
|
||||
|
||||
const movements = new Movements(ctx.bot)
|
||||
const movements = new Movements(mineflayer.bot)
|
||||
movements.dontMineUnderFallingBlock = false
|
||||
return blocks.filter(block => movements.safeToBreak(block as SafeBlock))
|
||||
}
|
||||
|
||||
function logNoBlocksMessage(ctx: SkillContext, blockType: string, collected: number): void {
|
||||
log(ctx, collected === 0
|
||||
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(ctx: SkillContext, block: any, blockType: string): Promise<boolean> {
|
||||
await ctx.bot.tool.equipForBlock(block)
|
||||
const itemId = ctx.bot.heldItem ? ctx.bot.heldItem.type : null
|
||||
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(ctx, `Don't have right tools to harvest ${blockType}.`)
|
||||
log(mineflayer, `Don't have right tools to harvest ${blockType}.`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
async function tryCollectBlock(ctx: SkillContext, block: any, blockType: string): Promise<boolean> {
|
||||
async function tryCollectBlock(mineflayer: Mineflayer, block: any, blockType: string): Promise<boolean> {
|
||||
try {
|
||||
await ctx.bot.collectBlock.collect(block)
|
||||
await autoLight(ctx)
|
||||
await mineflayer.bot.collectBlock.collect(block)
|
||||
await autoLight(mineflayer)
|
||||
return true
|
||||
}
|
||||
catch (err) {
|
||||
if (err instanceof Error && err.name === 'NoChests') {
|
||||
log(ctx, `Failed to collect ${blockType}: Inventory full, no place to deposit.`)
|
||||
log(mineflayer, `Failed to collect ${blockType}: Inventory full, no place to deposit.`)
|
||||
return false
|
||||
}
|
||||
log(ctx, `Failed to collect ${blockType}: ${err}.`)
|
||||
log(mineflayer, `Failed to collect ${blockType}: ${err}.`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Entity } from 'prismarine-entity'
|
||||
import type { Item } from 'prismarine-item'
|
||||
import type { SkillContext } from './base'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import pathfinderModel from 'mineflayer-pathfinder'
|
||||
import * as world from '../composables/world'
|
||||
import * as mc from '../utils/mcdata'
|
||||
@@ -12,15 +12,14 @@ interface WeaponItem extends Item {
|
||||
attackDamage: number
|
||||
}
|
||||
|
||||
async function equipHighestAttack(ctx: SkillContext): Promise<void> {
|
||||
const { bot } = ctx
|
||||
const weapons = bot.inventory.items().filter(item =>
|
||||
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 = bot.inventory.items().filter(item =>
|
||||
const tools = mineflayer.bot.inventory.items().filter(item =>
|
||||
item.name.includes('pickaxe')
|
||||
|| item.name.includes('shovel'),
|
||||
) as WeaponItem[]
|
||||
@@ -31,108 +30,108 @@ async function equipHighestAttack(ctx: SkillContext): Promise<void> {
|
||||
tools.sort((a, b) => b.attackDamage - a.attackDamage)
|
||||
const tool = tools[0]
|
||||
if (tool)
|
||||
await bot.equip(tool, 'hand')
|
||||
await mineflayer.bot.equip(tool, 'hand')
|
||||
return
|
||||
}
|
||||
|
||||
weapons.sort((a, b) => b.attackDamage - a.attackDamage)
|
||||
const weapon = weapons[0]
|
||||
if (weapon)
|
||||
await bot.equip(weapon, 'hand')
|
||||
await mineflayer.bot.equip(weapon, 'hand')
|
||||
}
|
||||
|
||||
export async function attackNearest(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
mobType: string,
|
||||
kill = true,
|
||||
): Promise<boolean> {
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
const mob = world.getNearbyEntities(worldCtx, 24).find(entity => entity.name === mobType)
|
||||
const mob = world.getNearbyEntities(mineflayer, 24).find(entity => entity.name === mobType)
|
||||
|
||||
if (mob) {
|
||||
return await attackEntity(ctx, mob, kill)
|
||||
return await attackEntity(mineflayer, mob, kill)
|
||||
}
|
||||
|
||||
log(ctx, `Could not find any ${mobType} to attack.`)
|
||||
log(mineflayer, `Could not find any ${mobType} to attack.`)
|
||||
return false
|
||||
}
|
||||
|
||||
export async function attackEntity(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
entity: Entity,
|
||||
kill = true,
|
||||
): Promise<boolean> {
|
||||
const { bot } = ctx
|
||||
const pos = entity.position
|
||||
await equipHighestAttack(ctx)
|
||||
await equipHighestAttack(mineflayer)
|
||||
|
||||
if (!kill) {
|
||||
if (bot.entity.position.distanceTo(pos) > 5) {
|
||||
if (mineflayer.bot.entity.position.distanceTo(pos) > 5) {
|
||||
const goal = new goals.GoalNear(pos.x, pos.y, pos.z, 4)
|
||||
await bot.pathfinder.goto(goal)
|
||||
await mineflayer.bot.pathfinder.goto(goal)
|
||||
}
|
||||
await bot.attack(entity)
|
||||
await mineflayer.bot.attack(entity)
|
||||
return true
|
||||
}
|
||||
|
||||
bot.pvp.attack(entity)
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
while (world.getNearbyEntities(worldCtx, 24).includes(entity)) {
|
||||
// @ts-expect-error -- ?
|
||||
mineflayer.bot.pvp.attack(entity)
|
||||
while (world.getNearbyEntities(mineflayer, 24).includes(entity)) {
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
if (ctx.shouldInterrupt) {
|
||||
bot.pvp.stop()
|
||||
if (mineflayer.shouldInterrupt) {
|
||||
// @ts-expect-error -- ?
|
||||
mineflayer.bot.pvp.stop()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
log(ctx, `Successfully killed ${entity.name}.`)
|
||||
log(mineflayer, `Successfully killed ${entity.name}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function defendSelf(ctx: SkillContext, range = 9): Promise<boolean> {
|
||||
const { bot } = ctx
|
||||
export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise<boolean> {
|
||||
let attacked = false
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
let enemy = world.getNearestEntityWhere(worldCtx, entity => mc.isHostile(entity), range)
|
||||
let enemy = world.getNearestEntityWhere(mineflayer, entity => mc.isHostile(entity), range)
|
||||
|
||||
while (enemy) {
|
||||
await equipHighestAttack(ctx)
|
||||
await equipHighestAttack(mineflayer)
|
||||
|
||||
if (bot.entity.position.distanceTo(enemy.position) >= 4
|
||||
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 bot.pathfinder.goto(goal)
|
||||
await mineflayer.bot.pathfinder.goto(goal)
|
||||
}
|
||||
catch { /* might error if entity dies, ignore */ }
|
||||
}
|
||||
|
||||
if (bot.entity.position.distanceTo(enemy.position) <= 2) {
|
||||
if (mineflayer.bot.entity.position.distanceTo(enemy.position) <= 2) {
|
||||
try {
|
||||
const followGoal = new goals.GoalFollow(enemy, 2)
|
||||
const invertedGoal = new goals.GoalInvert(followGoal)
|
||||
await bot.pathfinder.goto(invertedGoal)
|
||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
||||
}
|
||||
catch { /* might error if entity dies, ignore */ }
|
||||
}
|
||||
|
||||
bot.pvp.attack(enemy)
|
||||
// @ts-expect-error -- ?
|
||||
mineflayer.bot.pvp.attack(enemy)
|
||||
attacked = true
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
enemy = world.getNearestEntityWhere(worldCtx, entity => mc.isHostile(entity), range)
|
||||
enemy = world.getNearestEntityWhere(mineflayer, entity => mc.isHostile(entity), range)
|
||||
|
||||
if (ctx.shouldInterrupt) {
|
||||
bot.pvp.stop()
|
||||
if (mineflayer.shouldInterrupt) {
|
||||
// @ts-expect-error -- ?
|
||||
mineflayer.bot.pvp.stop()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
bot.pvp.stop()
|
||||
// @ts-expect-error -- ?
|
||||
mineflayer.bot.pvp.stop()
|
||||
if (attacked) {
|
||||
log(ctx, 'Successfully defended self.')
|
||||
log(mineflayer, 'Successfully defended self.')
|
||||
}
|
||||
else {
|
||||
log(ctx, 'No enemies nearby to defend self from.')
|
||||
log(mineflayer, 'No enemies nearby to defend self from.')
|
||||
}
|
||||
return attacked
|
||||
}
|
||||
|
||||
@@ -1,195 +1,191 @@
|
||||
import type { SkillContext } from './base'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import * as world from '../composables/world'
|
||||
import { createWorldContext } from '../composables/world'
|
||||
import * as mc from '../utils/mcdata'
|
||||
import { log } from './base'
|
||||
import { collectBlock, placeBlock } from './blocks'
|
||||
import { goToPosition } from './movement'
|
||||
|
||||
export async function craftRecipe(ctx: SkillContext, itemName: string, num = 1): Promise<boolean> {
|
||||
export async function craftRecipe(mineflayer: Mineflayer, itemName: string, num = 1): Promise<boolean> {
|
||||
let placedTable = false
|
||||
|
||||
if (mc.getItemCraftingRecipes(itemName)?.length === 0) {
|
||||
log(ctx, `${itemName} is either not an item, or it does not have a crafting recipe!`)
|
||||
log(mineflayer, `${itemName} is either not an item, or it does not have a crafting recipe!`)
|
||||
return false
|
||||
}
|
||||
|
||||
// Get recipes that don't require a crafting table
|
||||
const itemId = mc.getItemId(itemName)
|
||||
if (itemId === null) {
|
||||
log(ctx, `Invalid item name: ${itemName}`)
|
||||
log(mineflayer, `Invalid item name: ${itemName}`)
|
||||
return false
|
||||
}
|
||||
|
||||
let recipes = ctx.bot.recipesFor(itemId, null, 1, null)
|
||||
let recipes = mineflayer.bot.recipesFor(itemId, null, 1, null)
|
||||
let craftingTable = null
|
||||
const craftingTableRange = 32
|
||||
|
||||
if (!recipes || recipes.length === 0) {
|
||||
recipes = ctx.bot.recipesFor(itemId, null, 1, true)
|
||||
recipes = mineflayer.bot.recipesFor(itemId, null, 1, true)
|
||||
if (!recipes || recipes.length === 0) {
|
||||
log(ctx, `You do not have the resources to craft a ${itemName}.`)
|
||||
log(mineflayer, `You do not have the resources to craft a ${itemName}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for crafting table
|
||||
const worldCtx = createWorldContext(ctx.botCtx)
|
||||
craftingTable = world.getNearestBlock(worldCtx, 'crafting_table', craftingTableRange)
|
||||
craftingTable = world.getNearestBlock(mineflayer, 'crafting_table', craftingTableRange)
|
||||
if (!craftingTable) {
|
||||
// Try to place crafting table
|
||||
const inventory = world.getInventoryCounts(worldCtx)
|
||||
const inventory = world.getInventoryCounts(mineflayer)
|
||||
const hasTable = inventory.crafting_table > 0
|
||||
if (hasTable) {
|
||||
const pos = world.getNearestFreeSpace(worldCtx, 1, 6)
|
||||
const pos = world.getNearestFreeSpace(mineflayer, 1, 6)
|
||||
if (pos) {
|
||||
await placeBlock(ctx, 'crafting_table', pos.x, pos.y, pos.z)
|
||||
craftingTable = world.getNearestBlock(worldCtx, 'crafting_table', craftingTableRange)
|
||||
await placeBlock(mineflayer, 'crafting_table', pos.x, pos.y, pos.z)
|
||||
craftingTable = world.getNearestBlock(mineflayer, 'crafting_table', craftingTableRange)
|
||||
if (craftingTable) {
|
||||
recipes = ctx.bot.recipesFor(itemId, null, 1, craftingTable)
|
||||
recipes = mineflayer.bot.recipesFor(itemId, null, 1, craftingTable)
|
||||
placedTable = true
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
log(ctx, `Crafting ${itemName} requires a crafting table.`)
|
||||
log(mineflayer, `Crafting ${itemName} requires a crafting table.`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
else {
|
||||
recipes = ctx.bot.recipesFor(itemId, null, 1, craftingTable)
|
||||
recipes = mineflayer.bot.recipesFor(itemId, null, 1, craftingTable)
|
||||
}
|
||||
}
|
||||
|
||||
if (!recipes || recipes.length === 0) {
|
||||
log(ctx, `You do not have the resources to craft a ${itemName}. It requires: ${
|
||||
log(mineflayer, `You do not have the resources to craft a ${itemName}. It requires: ${
|
||||
Object.entries(mc.getItemCraftingRecipes(itemName)?.[0] ?? {})
|
||||
.map(([key, value]) => `${key}: ${value}`)
|
||||
.join(', ')
|
||||
}.`)
|
||||
if (placedTable && craftingTable) {
|
||||
await collectBlock(ctx, 'crafting_table', 1)
|
||||
await collectBlock(mineflayer, 'crafting_table', 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
if (craftingTable && ctx.bot.entity.position.distanceTo(craftingTable.position) > 4) {
|
||||
await goToPosition(ctx, craftingTable.position.x, craftingTable.position.y, craftingTable.position.z, 4)
|
||||
if (craftingTable && mineflayer.bot.entity.position.distanceTo(craftingTable.position) > 4) {
|
||||
await goToPosition(mineflayer, craftingTable.position.x, craftingTable.position.y, craftingTable.position.z, 4)
|
||||
}
|
||||
|
||||
const recipe = recipes[0]
|
||||
// Check that the agent has sufficient items to use the recipe `num` times
|
||||
const worldCtx = createWorldContext(ctx.botCtx)
|
||||
const inventory = world.getInventoryCounts(worldCtx) // Items in the agents inventory
|
||||
const inventory = world.getInventoryCounts(mineflayer) // Items in the agents inventory
|
||||
const requiredIngredients = mc.ingredientsFromPrismarineRecipe(recipe) // Items required to use the recipe once
|
||||
const craftLimit = mc.calculateLimitingResource(inventory, requiredIngredients)
|
||||
|
||||
await ctx.bot.craft(recipe, Math.min(craftLimit.num, num), craftingTable ?? undefined)
|
||||
await mineflayer.bot.craft(recipe, Math.min(craftLimit.num, num), craftingTable ?? undefined)
|
||||
|
||||
if (craftLimit.num < num) {
|
||||
log(ctx, `Not enough ${craftLimit.limitingResource} to craft ${num}, crafted ${craftLimit.num}. You now have ${world.getInventoryCounts(worldCtx)[itemName]} ${itemName}.`)
|
||||
log(mineflayer, `Not enough ${craftLimit.limitingResource} to craft ${num}, crafted ${craftLimit.num}. You now have ${world.getInventoryCounts(mineflayer)[itemName]} ${itemName}.`)
|
||||
}
|
||||
else {
|
||||
log(ctx, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(worldCtx)[itemName]} ${itemName}.`)
|
||||
log(mineflayer, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(mineflayer)[itemName]} ${itemName}.`)
|
||||
}
|
||||
|
||||
if (placedTable && craftingTable) {
|
||||
await collectBlock(ctx, 'crafting_table', 1)
|
||||
await collectBlock(mineflayer, 'crafting_table', 1)
|
||||
}
|
||||
|
||||
// Equip any armor the bot may have crafted
|
||||
ctx.bot.armorManager.equipAll()
|
||||
mineflayer.bot.armorManager.equipAll()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export async function smeltItem(ctx: SkillContext, itemName: string, num = 1): Promise<boolean> {
|
||||
export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = 1): Promise<boolean> {
|
||||
if (!mc.isSmeltable(itemName)) {
|
||||
log(ctx, `Cannot smelt ${itemName}. Hint: make sure you are smelting the 'raw' item.`)
|
||||
log(mineflayer, `Cannot smelt ${itemName}. Hint: make sure you are smelting the 'raw' item.`)
|
||||
return false
|
||||
}
|
||||
|
||||
let placedFurnace = false
|
||||
const furnaceRange = 32
|
||||
const worldCtx = createWorldContext(ctx.botCtx)
|
||||
let furnaceBlock = world.getNearestBlock(worldCtx, 'furnace', furnaceRange)
|
||||
let furnaceBlock = world.getNearestBlock(mineflayer, 'furnace', furnaceRange)
|
||||
|
||||
if (!furnaceBlock) {
|
||||
// Try to place furnace
|
||||
const inventory = world.getInventoryCounts(worldCtx)
|
||||
const inventory = world.getInventoryCounts(mineflayer)
|
||||
const hasFurnace = inventory.furnace > 0
|
||||
if (hasFurnace) {
|
||||
const pos = world.getNearestFreeSpace(worldCtx, 1, furnaceRange)
|
||||
const pos = world.getNearestFreeSpace(mineflayer, 1, furnaceRange)
|
||||
if (pos) {
|
||||
await placeBlock(ctx, 'furnace', pos.x, pos.y, pos.z)
|
||||
furnaceBlock = world.getNearestBlock(worldCtx, 'furnace', furnaceRange)
|
||||
await placeBlock(mineflayer, 'furnace', pos.x, pos.y, pos.z)
|
||||
furnaceBlock = world.getNearestBlock(mineflayer, 'furnace', furnaceRange)
|
||||
placedFurnace = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!furnaceBlock) {
|
||||
log(ctx, 'There is no furnace nearby and you have no furnace.')
|
||||
log(mineflayer, 'There is no furnace nearby and you have no furnace.')
|
||||
return false
|
||||
}
|
||||
|
||||
if (ctx.bot.entity.position.distanceTo(furnaceBlock.position) > 4) {
|
||||
await goToPosition(ctx, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4)
|
||||
if (mineflayer.bot.entity.position.distanceTo(furnaceBlock.position) > 4) {
|
||||
await goToPosition(mineflayer, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4)
|
||||
}
|
||||
|
||||
await ctx.bot.lookAt(furnaceBlock.position)
|
||||
await mineflayer.bot.lookAt(furnaceBlock.position)
|
||||
|
||||
const furnace = await ctx.bot.openFurnace(furnaceBlock)
|
||||
const furnace = await mineflayer.bot.openFurnace(furnaceBlock)
|
||||
|
||||
// Check if the furnace is already smelting something
|
||||
const inputItem = furnace.inputItem()
|
||||
const itemId = mc.getItemId(itemName)
|
||||
if (itemId === null) {
|
||||
log(ctx, `Invalid item name: ${itemName}`)
|
||||
log(mineflayer, `Invalid item name: ${itemName}`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (inputItem && inputItem.type !== itemId && inputItem.count > 0) {
|
||||
log(ctx, `The furnace is currently smelting ${mc.getItemName(inputItem.type) ?? 'unknown'}.`)
|
||||
log(mineflayer, `The furnace is currently smelting ${mc.getItemName(inputItem.type) ?? 'unknown'}.`)
|
||||
if (placedFurnace) {
|
||||
await collectBlock(ctx, 'furnace', 1)
|
||||
await collectBlock(mineflayer, 'furnace', 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the bot has enough items to smelt
|
||||
const invCounts = world.getInventoryCounts(worldCtx)
|
||||
const invCounts = world.getInventoryCounts(mineflayer)
|
||||
if (!invCounts[itemName] || invCounts[itemName] < num) {
|
||||
log(ctx, `You do not have enough ${itemName} to smelt.`)
|
||||
log(mineflayer, `You do not have enough ${itemName} to smelt.`)
|
||||
if (placedFurnace) {
|
||||
await collectBlock(ctx, 'furnace', 1)
|
||||
await collectBlock(mineflayer, 'furnace', 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Fuel the furnace
|
||||
if (!furnace.fuelItem()) {
|
||||
const fuel = mc.getSmeltingFuel(ctx.bot)
|
||||
const fuel = mc.getSmeltingFuel(mineflayer.bot)
|
||||
if (!fuel) {
|
||||
log(ctx, `You have no fuel to smelt ${itemName}, you need coal, charcoal, or wood.`)
|
||||
log(mineflayer, `You have no fuel to smelt ${itemName}, you need coal, charcoal, or wood.`)
|
||||
if (placedFurnace) {
|
||||
await collectBlock(ctx, 'furnace', 1)
|
||||
await collectBlock(mineflayer, 'furnace', 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
log(ctx, `Using ${fuel.name} as fuel.`)
|
||||
log(mineflayer, `Using ${fuel.name} as fuel.`)
|
||||
const putFuel = Math.ceil(num / mc.getFuelSmeltOutput(fuel.name))
|
||||
|
||||
if (fuel.count < putFuel) {
|
||||
log(ctx, `You don't have enough ${fuel.name} to smelt ${num} ${itemName}; you need ${putFuel}.`)
|
||||
log(mineflayer, `You don't have enough ${fuel.name} to smelt ${num} ${itemName}; you need ${putFuel}.`)
|
||||
if (placedFurnace) {
|
||||
await collectBlock(ctx, 'furnace', 1)
|
||||
await collectBlock(mineflayer, 'furnace', 1)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
await furnace.putFuel(fuel.type, null, putFuel)
|
||||
log(ctx, `Added ${putFuel} ${mc.getItemName(fuel.type) ?? 'unknown'} to furnace fuel.`)
|
||||
log(mineflayer, `Added ${putFuel} ${mc.getItemName(fuel.type) ?? 'unknown'} to furnace fuel.`)
|
||||
}
|
||||
|
||||
// Put the items in the furnace
|
||||
@@ -219,44 +215,43 @@ export async function smeltItem(ctx: SkillContext, itemName: string, num = 1): P
|
||||
}
|
||||
|
||||
collectedLast = collected
|
||||
if (ctx.shouldInterrupt) {
|
||||
if (mineflayer.shouldInterrupt) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.bot.closeWindow(furnace)
|
||||
await mineflayer.bot.closeWindow(furnace)
|
||||
|
||||
if (placedFurnace) {
|
||||
await collectBlock(ctx, 'furnace', 1)
|
||||
await collectBlock(mineflayer, 'furnace', 1)
|
||||
}
|
||||
|
||||
if (total === 0) {
|
||||
log(ctx, `Failed to smelt ${itemName}.`)
|
||||
log(mineflayer, `Failed to smelt ${itemName}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (total < num) {
|
||||
log(ctx, `Only smelted ${total} ${mc.getItemName(smeltedItem?.type ?? 0) ?? 'unknown'}.`)
|
||||
log(mineflayer, `Only smelted ${total} ${mc.getItemName(smeltedItem?.type ?? 0) ?? 'unknown'}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
log(ctx, `Successfully smelted ${itemName}, got ${total} ${mc.getItemName(smeltedItem?.type ?? 0) ?? 'unknown'}.`)
|
||||
log(mineflayer, `Successfully smelted ${itemName}, got ${total} ${mc.getItemName(smeltedItem?.type ?? 0) ?? 'unknown'}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function clearNearestFurnace(ctx: SkillContext): Promise<boolean> {
|
||||
const worldCtx = createWorldContext(ctx.botCtx)
|
||||
const furnaceBlock = world.getNearestBlock(worldCtx, 'furnace', 32)
|
||||
export async function clearNearestFurnace(mineflayer: Mineflayer): Promise<boolean> {
|
||||
const furnaceBlock = world.getNearestBlock(mineflayer, 'furnace', 32)
|
||||
if (!furnaceBlock) {
|
||||
log(ctx, 'No furnace nearby to clear.')
|
||||
log(mineflayer, 'No furnace nearby to clear.')
|
||||
return false
|
||||
}
|
||||
|
||||
if (ctx.bot.entity.position.distanceTo(furnaceBlock.position) > 4) {
|
||||
await goToPosition(ctx, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4)
|
||||
if (mineflayer.bot.entity.position.distanceTo(furnaceBlock.position) > 4) {
|
||||
await goToPosition(mineflayer, furnaceBlock.position.x, furnaceBlock.position.y, furnaceBlock.position.z, 4)
|
||||
}
|
||||
|
||||
const furnace = await ctx.bot.openFurnace(furnaceBlock)
|
||||
const furnace = await mineflayer.bot.openFurnace(furnaceBlock)
|
||||
|
||||
// Take the items out of the furnace
|
||||
let smeltedItem, inputItem, fuelItem
|
||||
@@ -280,6 +275,6 @@ export async function clearNearestFurnace(ctx: SkillContext): Promise<boolean> {
|
||||
const inputName = inputItem ? `${inputItem.count} ${inputItem.name}` : '0 input items'
|
||||
const fuelName = fuelItem ? `${fuelItem.count} ${fuelItem.name}` : '0 fuel items'
|
||||
|
||||
log(ctx, `Cleared furnace, received ${smeltedName}, ${inputName}, and ${fuelName}.`)
|
||||
log(mineflayer, `Cleared furnace, received ${smeltedName}, ${inputName}, and ${fuelName}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Bot } from 'mineflayer'
|
||||
import type { SkillContext } from './base'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
import pathfinderModel from 'mineflayer-pathfinder'
|
||||
import * as world from '../composables/world'
|
||||
import { log } from './base'
|
||||
@@ -7,7 +7,7 @@ import { goToPosition } from './movement'
|
||||
|
||||
const { goals } = pathfinderModel
|
||||
|
||||
export async function pickupNearbyItems(ctx: SkillContext): Promise<boolean> {
|
||||
export async function pickupNearbyItems(mineflayer: Mineflayer): Promise<boolean> {
|
||||
const distance = 8
|
||||
const getNearestItem = (bot: Bot) =>
|
||||
bot.nearestEntity(entity =>
|
||||
@@ -15,66 +15,66 @@ export async function pickupNearbyItems(ctx: SkillContext): Promise<boolean> {
|
||||
&& bot.entity.position.distanceTo(entity.position) < distance,
|
||||
)
|
||||
|
||||
let nearestItem = getNearestItem(ctx.bot)
|
||||
let nearestItem = getNearestItem(mineflayer.bot)
|
||||
let pickedUp = 0
|
||||
|
||||
while (nearestItem) {
|
||||
await ctx.bot.pathfinder.goto(new goals.GoalFollow(nearestItem, 0.8))
|
||||
await mineflayer.bot.pathfinder.goto(new goals.GoalFollow(nearestItem, 0.8))
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
const prev = nearestItem
|
||||
nearestItem = getNearestItem(ctx.bot)
|
||||
nearestItem = getNearestItem(mineflayer.bot)
|
||||
if (prev === nearestItem) {
|
||||
break
|
||||
}
|
||||
pickedUp++
|
||||
}
|
||||
|
||||
log(ctx, `Picked up ${pickedUp} items.`)
|
||||
log(mineflayer, `Picked up ${pickedUp} items.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function equip(ctx: SkillContext, itemName: string): Promise<boolean> {
|
||||
const item = ctx.bot.inventory.slots.find(slot => slot && slot.name === itemName)
|
||||
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(ctx, `You do not have any ${itemName} to equip.`)
|
||||
log(mineflayer, `You do not have any ${itemName} to equip.`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (itemName.includes('leggings')) {
|
||||
await ctx.bot.equip(item, 'legs')
|
||||
await mineflayer.bot.equip(item, 'legs')
|
||||
}
|
||||
else if (itemName.includes('boots')) {
|
||||
await ctx.bot.equip(item, 'feet')
|
||||
await mineflayer.bot.equip(item, 'feet')
|
||||
}
|
||||
else if (itemName.includes('helmet')) {
|
||||
await ctx.bot.equip(item, 'head')
|
||||
await mineflayer.bot.equip(item, 'head')
|
||||
}
|
||||
else if (itemName.includes('chestplate') || itemName.includes('elytra')) {
|
||||
await ctx.bot.equip(item, 'torso')
|
||||
await mineflayer.bot.equip(item, 'torso')
|
||||
}
|
||||
else if (itemName.includes('shield')) {
|
||||
await ctx.bot.equip(item, 'off-hand')
|
||||
await mineflayer.bot.equip(item, 'off-hand')
|
||||
}
|
||||
else {
|
||||
await ctx.bot.equip(item, 'hand')
|
||||
await mineflayer.bot.equip(item, 'hand')
|
||||
}
|
||||
|
||||
log(ctx, `Equipped ${itemName}.`)
|
||||
log(mineflayer, `Equipped ${itemName}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function discard(ctx: SkillContext, itemName: string, num = -1): Promise<boolean> {
|
||||
export async function discard(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
|
||||
let discarded = 0
|
||||
|
||||
while (true) {
|
||||
const item = ctx.bot.inventory.items().find(item => item.name === itemName)
|
||||
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 ctx.bot.toss(item.type, null, toDiscard)
|
||||
await mineflayer.bot.toss(item.type, null, toDiscard)
|
||||
discarded += toDiscard
|
||||
|
||||
if (num !== -1 && discarded >= num) {
|
||||
@@ -83,51 +83,51 @@ export async function discard(ctx: SkillContext, itemName: string, num = -1): Pr
|
||||
}
|
||||
|
||||
if (discarded === 0) {
|
||||
log(ctx, `You do not have any ${itemName} to discard.`)
|
||||
log(mineflayer, `You do not have any ${itemName} to discard.`)
|
||||
return false
|
||||
}
|
||||
|
||||
log(ctx, `Discarded ${discarded} ${itemName}.`)
|
||||
log(mineflayer, `Discarded ${discarded} ${itemName}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function putInChest(ctx: SkillContext, itemName: string, num = -1): Promise<boolean> {
|
||||
const chest = world.getNearestBlock(world.createWorldContext(ctx.botCtx), 'chest', 32)
|
||||
export async function putInChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
|
||||
const chest = world.getNearestBlock(mineflayer, 'chest', 32)
|
||||
if (!chest) {
|
||||
log(ctx, 'Could not find a chest nearby.')
|
||||
log(mineflayer, 'Could not find a chest nearby.')
|
||||
return false
|
||||
}
|
||||
|
||||
const item = ctx.bot.inventory.items().find(item => item.name === itemName)
|
||||
const item = mineflayer.bot.inventory.items().find(item => item.name === itemName)
|
||||
if (!item) {
|
||||
log(ctx, `You do not have any ${itemName} to put in the chest.`)
|
||||
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(ctx, chest.position.x, chest.position.y, chest.position.z, 2)
|
||||
await goToPosition(mineflayer, chest.position.x, chest.position.y, chest.position.z, 2)
|
||||
|
||||
const chestContainer = await ctx.bot.openContainer(chest)
|
||||
const chestContainer = await mineflayer.bot.openContainer(chest)
|
||||
await chestContainer.deposit(item.type, null, toPut)
|
||||
await chestContainer.close()
|
||||
|
||||
log(ctx, `Successfully put ${toPut} ${itemName} in the chest.`)
|
||||
log(mineflayer, `Successfully put ${toPut} ${itemName} in the chest.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function takeFromChest(ctx: SkillContext, itemName: string, num = -1): Promise<boolean> {
|
||||
const chest = world.getNearestBlock(world.createWorldContext(ctx.botCtx), 'chest', 32)
|
||||
export async function takeFromChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise<boolean> {
|
||||
const chest = world.getNearestBlock(mineflayer, 'chest', 32)
|
||||
if (!chest) {
|
||||
log(ctx, 'Could not find a chest nearby.')
|
||||
log(mineflayer, 'Could not find a chest nearby.')
|
||||
return false
|
||||
}
|
||||
|
||||
await goToPosition(ctx, chest.position.x, chest.position.y, chest.position.z, 2)
|
||||
const chestContainer = await ctx.bot.openContainer(chest)
|
||||
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(ctx, `Could not find any ${itemName} in the chest.`)
|
||||
log(mineflayer, `Could not find any ${itemName} in the chest.`)
|
||||
await chestContainer.close()
|
||||
return false
|
||||
}
|
||||
@@ -136,28 +136,28 @@ export async function takeFromChest(ctx: SkillContext, itemName: string, num = -
|
||||
await chestContainer.withdraw(item.type, null, toTake)
|
||||
await chestContainer.close()
|
||||
|
||||
log(ctx, `Successfully took ${toTake} ${itemName} from the chest.`)
|
||||
log(mineflayer, `Successfully took ${toTake} ${itemName} from the chest.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function viewChest(ctx: SkillContext): Promise<boolean> {
|
||||
const chest = world.getNearestBlock(world.createWorldContext(ctx.botCtx), 'chest', 32)
|
||||
export async function viewChest(mineflayer: Mineflayer): Promise<boolean> {
|
||||
const chest = world.getNearestBlock(mineflayer, 'chest', 32)
|
||||
if (!chest) {
|
||||
log(ctx, 'Could not find a chest nearby.')
|
||||
log(mineflayer, 'Could not find a chest nearby.')
|
||||
return false
|
||||
}
|
||||
|
||||
await goToPosition(ctx, chest.position.x, chest.position.y, chest.position.z, 2)
|
||||
const chestContainer = await ctx.bot.openContainer(chest)
|
||||
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(ctx, 'The chest is empty.')
|
||||
log(mineflayer, 'The chest is empty.')
|
||||
}
|
||||
else {
|
||||
log(ctx, 'The chest contains:')
|
||||
log(mineflayer, 'The chest contains:')
|
||||
for (const item of items) {
|
||||
log(ctx, `${item.count} ${item.name}`)
|
||||
log(mineflayer, `${item.count} ${item.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,63 +165,64 @@ export async function viewChest(ctx: SkillContext): Promise<boolean> {
|
||||
return true
|
||||
}
|
||||
|
||||
export async function consume(ctx: SkillContext, itemName = ''): Promise<boolean> {
|
||||
export async function consume(mineflayer: Mineflayer, itemName = ''): Promise<boolean> {
|
||||
let item
|
||||
let name
|
||||
|
||||
if (itemName) {
|
||||
item = ctx.bot.inventory.items().find(item => item.name === itemName)
|
||||
item = mineflayer.bot.inventory.items().find(item => item.name === itemName)
|
||||
name = itemName
|
||||
}
|
||||
|
||||
if (!item) {
|
||||
log(ctx, `You do not have any ${name} to eat.`)
|
||||
log(mineflayer, `You do not have any ${name} to eat.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await ctx.bot.equip(item, 'hand')
|
||||
await ctx.bot.consume()
|
||||
log(ctx, `Consumed ${item.name}.`)
|
||||
await mineflayer.bot.equip(item, 'hand')
|
||||
await mineflayer.bot.consume()
|
||||
log(mineflayer, `Consumed ${item.name}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function giveToPlayer(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
itemType: string,
|
||||
username: string,
|
||||
num = 1,
|
||||
): Promise<boolean> {
|
||||
const player = ctx.bot.players[username]?.entity
|
||||
const player = mineflayer.bot.players[username]?.entity
|
||||
if (!player) {
|
||||
log(ctx, `Could not find ${username}.`)
|
||||
log(mineflayer, `Could not find ${username}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await goToPosition(ctx, player.position.x, player.position.y, player.position.z, 3)
|
||||
await goToPosition(mineflayer, player.position.x, player.position.y, player.position.z, 3)
|
||||
|
||||
if (ctx.bot.entity.position.y < player.position.y - 1) {
|
||||
await goToPosition(ctx, player.position.x, player.position.y, player.position.z, 1)
|
||||
if (mineflayer.bot.entity.position.y < player.position.y - 1) {
|
||||
await goToPosition(mineflayer, player.position.x, player.position.y, player.position.z, 1)
|
||||
}
|
||||
|
||||
if (ctx.bot.entity.position.distanceTo(player.position) < 2) {
|
||||
if (mineflayer.bot.entity.position.distanceTo(player.position) < 2) {
|
||||
const goal = new goals.GoalNear(player.position.x, player.position.y, player.position.z, 2)
|
||||
const invertedGoal = new goals.GoalInvert(goal)
|
||||
await ctx.bot.pathfinder.goto(invertedGoal)
|
||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
||||
}
|
||||
|
||||
await ctx.bot.lookAt(player.position)
|
||||
await mineflayer.bot.lookAt(player.position)
|
||||
|
||||
if (await discard(ctx, itemType, num)) {
|
||||
if (await discard(mineflayer, itemType, num)) {
|
||||
let given = false
|
||||
ctx.bot.once('playerCollect', (collector, _collected) => {
|
||||
mineflayer.bot.once('playerCollect', (collector, _collected) => {
|
||||
if (collector.username === username) {
|
||||
log(ctx, `${username} received ${itemType}.`)
|
||||
log(mineflayer, `${username} received ${itemType}.`)
|
||||
given = true
|
||||
}
|
||||
})
|
||||
|
||||
const start = Date.now()
|
||||
while (!given && !ctx.shouldInterrupt) {
|
||||
// eslint-disable-next-line no-unmodified-loop-condition -- ?
|
||||
while (!given && !mineflayer.shouldInterrupt) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
if (given) {
|
||||
return true
|
||||
@@ -232,6 +233,6 @@ export async function giveToPlayer(
|
||||
}
|
||||
}
|
||||
|
||||
log(ctx, `Failed to give ${itemType} to ${username}, it was never received.`)
|
||||
log(mineflayer, `Failed to give ${itemType} to ${username}, it was never received.`)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Entity } from 'prismarine-entity'
|
||||
import type { SkillContext } from './base'
|
||||
import type { Mineflayer } from '../libs/mineflayer'
|
||||
|
||||
import pathfinderModel from 'mineflayer-pathfinder'
|
||||
import * as world from '../composables/world'
|
||||
import { log } from './base'
|
||||
@@ -7,74 +8,72 @@ import { log } from './base'
|
||||
const { goals, Movements } = pathfinderModel
|
||||
|
||||
export async function goToPosition(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
x: number,
|
||||
y: number,
|
||||
z: number,
|
||||
minDistance = 2,
|
||||
): Promise<boolean> {
|
||||
if (x == null || y == null || z == null) {
|
||||
log(ctx, `Missing coordinates, given x:${x} y:${y} z:${z}`)
|
||||
log(mineflayer, `Missing coordinates, given x:${x} y:${y} z:${z}`)
|
||||
return false
|
||||
}
|
||||
|
||||
if (ctx.allowCheats) {
|
||||
ctx.bot.chat(`/tp @s ${x} ${y} ${z}`)
|
||||
log(ctx, `Teleported to ${x}, ${y}, ${z}.`)
|
||||
if (mineflayer.allowCheats) {
|
||||
mineflayer.bot.chat(`/tp @s ${x} ${y} ${z}`)
|
||||
log(mineflayer, `Teleported to ${x}, ${y}, ${z}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
await ctx.bot.pathfinder.goto(new goals.GoalNear(x, y, z, minDistance))
|
||||
log(ctx, `You have reached ${x}, ${y}, ${z}.`)
|
||||
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(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
blockType: string,
|
||||
minDistance = 2,
|
||||
range = 64,
|
||||
): Promise<boolean> {
|
||||
const MAX_RANGE = 512
|
||||
if (range > MAX_RANGE) {
|
||||
log(ctx, `Maximum search range capped at ${MAX_RANGE}.`)
|
||||
log(mineflayer, `Maximum search range capped at ${MAX_RANGE}.`)
|
||||
range = MAX_RANGE
|
||||
}
|
||||
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
const block = world.getNearestBlock(worldCtx, blockType, range)
|
||||
const block = world.getNearestBlock(mineflayer, blockType, range)
|
||||
if (!block) {
|
||||
log(ctx, `Could not find any ${blockType} in ${range} blocks.`)
|
||||
log(mineflayer, `Could not find any ${blockType} in ${range} blocks.`)
|
||||
return false
|
||||
}
|
||||
|
||||
log(ctx, `Found ${blockType} at ${block.position}.`)
|
||||
await goToPosition(ctx, block.position.x, block.position.y, block.position.z, minDistance)
|
||||
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(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
entityType: string,
|
||||
minDistance = 2,
|
||||
range = 64,
|
||||
): Promise<boolean> {
|
||||
const worldCtx = world.createWorldContext(ctx.botCtx)
|
||||
const entity = world.getNearestEntityWhere(
|
||||
worldCtx,
|
||||
mineflayer,
|
||||
entity => entity.name === entityType,
|
||||
range,
|
||||
)
|
||||
|
||||
if (!entity) {
|
||||
log(ctx, `Could not find any ${entityType} in ${range} blocks.`)
|
||||
log(mineflayer, `Could not find any ${entityType} in ${range} blocks.`)
|
||||
return false
|
||||
}
|
||||
|
||||
const distance = ctx.bot.entity.position.distanceTo(entity.position)
|
||||
log(ctx, `Found ${entityType} ${distance} blocks away.`)
|
||||
const distance = mineflayer.bot.entity.position.distanceTo(entity.position)
|
||||
log(mineflayer, `Found ${entityType} ${distance} blocks away.`)
|
||||
await goToPosition(
|
||||
ctx,
|
||||
mineflayer,
|
||||
entity.position.x,
|
||||
entity.position.y,
|
||||
entity.position.z,
|
||||
@@ -84,174 +83,174 @@ export async function goToNearestEntity(
|
||||
}
|
||||
|
||||
export async function goToPlayer(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
username: string,
|
||||
distance = 3,
|
||||
): Promise<boolean> {
|
||||
if (ctx.allowCheats) {
|
||||
ctx.bot.chat(`/tp @s ${username}`)
|
||||
log(ctx, `Teleported to ${username}.`)
|
||||
if (mineflayer.allowCheats) {
|
||||
mineflayer.bot.chat(`/tp @s ${username}`)
|
||||
log(mineflayer, `Teleported to ${username}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
const player = ctx.bot.players[username]?.entity
|
||||
const player = mineflayer.bot.players[username]?.entity
|
||||
if (!player) {
|
||||
log(ctx, `Could not find ${username}.`)
|
||||
log(mineflayer, `Could not find ${username}.`)
|
||||
return false
|
||||
}
|
||||
|
||||
await ctx.bot.pathfinder.goto(new goals.GoalFollow(player, distance))
|
||||
log(ctx, `You have reached ${username}.`)
|
||||
await mineflayer.bot.pathfinder.goto(new goals.GoalFollow(player, distance))
|
||||
log(mineflayer, `You have reached ${username}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function followPlayer(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
username: string,
|
||||
distance = 4,
|
||||
): Promise<boolean> {
|
||||
// const player = ctx.bot.players[username]?.entity
|
||||
// const player = mineflayer.bot.players[username]?.entity
|
||||
// if (!player) {
|
||||
// log(ctx, `Could not find player ${username}`)
|
||||
// log(mineflayer, `Could not find player ${username}`)
|
||||
// return false
|
||||
// }
|
||||
|
||||
// const movements = new Movements(ctx.bot)
|
||||
// ctx.bot.pathfinder.setMovements(movements)
|
||||
// ctx.bot.pathfinder.setGoal(new goals.GoalNear(player.position.x, player.position.y, player.position.z, distance))
|
||||
// const movements = new Movements(mineflayer.bot)
|
||||
// mineflayer.bot.pathfinder.setMovements(movements)
|
||||
// mineflayer.bot.pathfinder.setGoal(new goals.GoalNear(player.position.x, player.position.y, player.position.z, distance))
|
||||
|
||||
// log(ctx, `Started following ${username}`)
|
||||
// log(mineflayer, `Started following ${username}`)
|
||||
|
||||
// const followInterval = setInterval(() => {
|
||||
// const target = ctx.bot.players[username]?.entity
|
||||
// const target = mineflayer.bot.players[username]?.entity
|
||||
// if (!target) {
|
||||
// log(ctx, 'Lost sight of player')
|
||||
// log(mineflayer, 'Lost sight of player')
|
||||
// clearInterval(followInterval)
|
||||
// return
|
||||
// }
|
||||
|
||||
// const { x, y, z } = target.position
|
||||
// ctx.bot.pathfinder.setGoal(new goals.GoalNear(x, y, z, distance))
|
||||
// mineflayer.bot.pathfinder.setGoal(new goals.GoalNear(x, y, z, distance))
|
||||
// }, 1000)
|
||||
|
||||
// while (!ctx.shouldInterrupt) {
|
||||
// await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
// if (ctx.allowCheats && ctx.bot.entity.position.distanceTo(player.position) > 100) {
|
||||
// if (mineflayer.allowCheats && mineflayer.bot.entity.position.distanceTo(player.position) > 100) {
|
||||
// await goToPlayer(ctx, username)
|
||||
// }
|
||||
// }
|
||||
|
||||
// // TODO: need global status management
|
||||
// clearInterval(followInterval)
|
||||
// ctx.bot.pathfinder.stop()
|
||||
// mineflayer.bot.pathfinder.stop()
|
||||
// return true
|
||||
|
||||
const player = ctx.bot.players[username]?.entity
|
||||
const player = mineflayer.bot.players[username]?.entity
|
||||
if (!player) {
|
||||
return false
|
||||
}
|
||||
|
||||
const movements = new Movements(ctx.bot)
|
||||
ctx.bot.pathfinder.setMovements(movements)
|
||||
ctx.bot.pathfinder.setGoal(new goals.GoalFollow(player, distance), true)
|
||||
log(ctx, `You are 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)
|
||||
log(mineflayer, `You are now actively following player ${username}.`)
|
||||
|
||||
while (!ctx.shouldInterrupt) {
|
||||
while (!mineflayer.shouldInterrupt) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
|
||||
if (ctx.allowCheats && ctx.bot.entity.position.distanceTo(player.position) > 100 && player.onGround) {
|
||||
await goToPlayer(ctx, username)
|
||||
if (mineflayer.allowCheats && mineflayer.bot.entity.position.distanceTo(player.position) > 100 && player.onGround) {
|
||||
await goToPlayer(mineflayer, username)
|
||||
}
|
||||
|
||||
// if (ctx.bot.modes?.isOn('unstuck')) {
|
||||
// const isNearby = ctx.bot.entity.position.distanceTo(player.position) <= distance + 1
|
||||
// if (mineflayer.bot.modes?.isOn('unstuck')) {
|
||||
// const isNearby = mineflayer.bot.entity.position.distanceTo(player.position) <= distance + 1
|
||||
// if (isNearby) {
|
||||
// ctx.bot.modes.pause('unstuck')
|
||||
// mineflayer.bot.modes.pause('unstuck')
|
||||
// } else {
|
||||
// ctx.bot.modes.unpause('unstuck')
|
||||
// mineflayer.bot.modes.unpause('unstuck')
|
||||
// }
|
||||
// }
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export async function moveAway(ctx: SkillContext, distance: number): Promise<boolean> {
|
||||
const pos = ctx.bot.entity.position
|
||||
export async function moveAway(mineflayer: Mineflayer, distance: number): Promise<boolean> {
|
||||
const pos = mineflayer.bot.entity.position
|
||||
const goal = new goals.GoalNear(pos.x, pos.y, pos.z, distance)
|
||||
const invertedGoal = new goals.GoalInvert(goal)
|
||||
|
||||
if (ctx.allowCheats) {
|
||||
const move = new Movements(ctx.bot)
|
||||
const path = await ctx.bot.pathfinder.getPathTo(move, invertedGoal, 10000)
|
||||
if (mineflayer.allowCheats) {
|
||||
const move = new Movements(mineflayer.bot)
|
||||
const path = await mineflayer.bot.pathfinder.getPathTo(move, invertedGoal, 10000)
|
||||
const lastMove = path.path[path.path.length - 1]
|
||||
|
||||
if (lastMove) {
|
||||
const x = Math.floor(lastMove.x)
|
||||
const y = Math.floor(lastMove.y)
|
||||
const z = Math.floor(lastMove.z)
|
||||
ctx.bot.chat(`/tp @s ${x} ${y} ${z}`)
|
||||
mineflayer.bot.chat(`/tp @s ${x} ${y} ${z}`)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
await ctx.bot.pathfinder.goto(invertedGoal)
|
||||
const newPos = ctx.bot.entity.position
|
||||
log(ctx, `Moved away from nearest entity to ${newPos}.`)
|
||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
||||
const newPos = mineflayer.bot.entity.position
|
||||
log(mineflayer, `Moved away from nearest entity to ${newPos}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function moveAwayFromEntity(
|
||||
ctx: SkillContext,
|
||||
mineflayer: Mineflayer,
|
||||
entity: Entity,
|
||||
distance = 16,
|
||||
): Promise<boolean> {
|
||||
const goal = new goals.GoalFollow(entity, distance)
|
||||
const invertedGoal = new goals.GoalInvert(goal)
|
||||
await ctx.bot.pathfinder.goto(invertedGoal)
|
||||
await mineflayer.bot.pathfinder.goto(invertedGoal)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function stay(ctx: SkillContext, seconds = 30): Promise<boolean> {
|
||||
export async function stay(mineflayer: Mineflayer, seconds = 30): Promise<boolean> {
|
||||
const start = Date.now()
|
||||
const targetTime = seconds === -1 ? Infinity : start + seconds * 1000
|
||||
|
||||
while (!ctx.shouldInterrupt && Date.now() < targetTime) {
|
||||
while (!mineflayer.shouldInterrupt && Date.now() < targetTime) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
}
|
||||
|
||||
log(ctx, `Stayed for ${(Date.now() - start) / 1000} seconds.`)
|
||||
log(mineflayer, `Stayed for ${(Date.now() - start) / 1000} seconds.`)
|
||||
return true
|
||||
}
|
||||
|
||||
export async function goToBed(ctx: SkillContext): Promise<boolean> {
|
||||
const beds = ctx.bot.findBlocks({
|
||||
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(ctx, 'Could not find a bed to sleep in.')
|
||||
log(mineflayer, 'Could not find a bed to sleep in.')
|
||||
return false
|
||||
}
|
||||
|
||||
const loc = beds[0]
|
||||
await goToPosition(ctx, loc.x, loc.y, loc.z)
|
||||
await goToPosition(mineflayer, loc.x, loc.y, loc.z)
|
||||
|
||||
const bed = ctx.bot.blockAt(loc)
|
||||
const bed = mineflayer.bot.blockAt(loc)
|
||||
if (!bed) {
|
||||
log(ctx, 'Could not find bed block.')
|
||||
log(mineflayer, 'Could not find bed block.')
|
||||
return false
|
||||
}
|
||||
|
||||
await ctx.bot.sleep(bed)
|
||||
log(ctx, 'You are in bed.')
|
||||
await mineflayer.bot.sleep(bed)
|
||||
log(mineflayer, 'You are in bed.')
|
||||
|
||||
while (ctx.bot.isSleeping) {
|
||||
while (mineflayer.bot.isSleeping) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500))
|
||||
}
|
||||
|
||||
log(ctx, 'You have woken up.')
|
||||
log(mineflayer, 'You have woken up.')
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
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 function createTicker(options?: { interval?: number }) {
|
||||
const { interval = 300 } = options ?? { interval: 300 }
|
||||
|
||||
let last = Date.now()
|
||||
const tickingCbs: Record<TickEvents, Array<TickEventHandlers[TickEvents]>> = {
|
||||
tick: [],
|
||||
}
|
||||
|
||||
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 = tickingCbs.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)
|
||||
|
||||
return {
|
||||
on<K extends TickEvents>(event: K, cb: TickEventsHandler<K>) {
|
||||
tickingCbs[event].push(cb)
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user