From b0e5100c93b3d9ddb9466c7fe1c1431a19442cdf Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Thu, 9 Jan 2025 01:47:51 +0800 Subject: [PATCH] refactor: better structure v1 --- services/minecraft/src/agents/actions.test.ts | 20 +- services/minecraft/src/agents/actions.ts | 174 ++++--- services/minecraft/src/agents/openai.test.ts | 10 +- services/minecraft/src/agents/openai.ts | 13 +- services/minecraft/src/components/aichat.ts | 58 --- services/minecraft/src/components/command.ts | 46 -- services/minecraft/src/components/echo.ts | 20 - services/minecraft/src/components/follow.ts | 72 --- .../minecraft/src/components/pathfinder.ts | 44 -- services/minecraft/src/components/status.ts | 41 -- services/minecraft/src/composables/bot.ts | 351 +------------- services/minecraft/src/composables/command.ts | 10 - services/minecraft/src/composables/events.ts | 9 - services/minecraft/src/composables/world.ts | 115 ++--- .../mineflayer}/command.ts | 0 .../minecraft/src/libs/mineflayer/index.ts | 441 ++++++++++++++++++ .../chat.ts => libs/mineflayer/message.ts} | 12 +- .../minecraft/src/libs/mineflayer/plugin.ts | 15 + .../minecraft/src/libs/mineflayer/ticker.ts | 62 +++ services/minecraft/src/main.ts | 47 +- services/minecraft/src/mineflayer/echo.ts | 27 ++ services/minecraft/src/mineflayer/follow.ts | 65 +++ services/minecraft/src/mineflayer/index.ts | 4 + .../minecraft/src/mineflayer/llm-agent.ts | 56 +++ .../minecraft/src/mineflayer/pathfinder.ts | 40 ++ services/minecraft/src/mineflayer/status.ts | 17 + services/minecraft/src/prompts/agent.ts | 13 +- services/minecraft/src/skills/base.ts | 49 +- services/minecraft/src/skills/blocks.ts | 309 ++++++------ services/minecraft/src/skills/combat.ts | 79 ++-- services/minecraft/src/skills/crafting.ts | 131 +++--- services/minecraft/src/skills/inventory.ts | 129 ++--- services/minecraft/src/skills/movement.ts | 155 +++--- services/minecraft/src/utils/ticker.ts | 58 --- 34 files changed, 1328 insertions(+), 1364 deletions(-) delete mode 100644 services/minecraft/src/components/aichat.ts delete mode 100644 services/minecraft/src/components/command.ts delete mode 100644 services/minecraft/src/components/echo.ts delete mode 100644 services/minecraft/src/components/follow.ts delete mode 100644 services/minecraft/src/components/pathfinder.ts delete mode 100644 services/minecraft/src/components/status.ts delete mode 100644 services/minecraft/src/composables/command.ts delete mode 100644 services/minecraft/src/composables/events.ts rename services/minecraft/src/{middlewares => libs/mineflayer}/command.ts (100%) create mode 100644 services/minecraft/src/libs/mineflayer/index.ts rename services/minecraft/src/{middlewares/chat.ts => libs/mineflayer/message.ts} (54%) create mode 100644 services/minecraft/src/libs/mineflayer/plugin.ts create mode 100644 services/minecraft/src/libs/mineflayer/ticker.ts create mode 100644 services/minecraft/src/mineflayer/echo.ts create mode 100644 services/minecraft/src/mineflayer/follow.ts create mode 100644 services/minecraft/src/mineflayer/index.ts create mode 100644 services/minecraft/src/mineflayer/llm-agent.ts create mode 100644 services/minecraft/src/mineflayer/pathfinder.ts create mode 100644 services/minecraft/src/mineflayer/status.ts delete mode 100644 services/minecraft/src/utils/ticker.ts diff --git a/services/minecraft/src/agents/actions.test.ts b/services/minecraft/src/agents/actions.test.ts index fea5b5da2..a15527d4a 100644 --- a/services/minecraft/src/agents/actions.test.ts +++ b/services/minecraft/src/agents/actions.test.ts @@ -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((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((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)) diff --git a/services/minecraft/src/agents/actions.ts b/services/minecraft/src/agents/actions.ts index 2bc91da67..3590434bf 100644 --- a/services/minecraft/src/agents/actions.ts +++ b/services/minecraft/src/agents/actions.ts @@ -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 - -export interface Action { - readonly name: string - readonly description: string - readonly schema: z.ZodObject - 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) diff --git a/services/minecraft/src/agents/openai.test.ts b/services/minecraft/src/agents/openai.test.ts index be16e442c..4f7547eb0 100644 --- a/services/minecraft/src/agents/openai.test.ts +++ b/services/minecraft/src/agents/openai.test.ts @@ -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((resolve) => { - ctx.bot.once('spawn', async () => { + bot.bot.once('spawn', async () => { const text = await agent.handle( messages( system(genSystemBasicPrompt('airi')), diff --git a/services/minecraft/src/agents/openai.ts b/services/minecraft/src/agents/openai.ts index b94623317..066874e6c 100644 --- a/services/minecraft/src/agents/openai.ts +++ b/services/minecraft/src/agents/openai.ts @@ -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>() const logger = useLogg('openai').useGlobalConfig() -export async function initAgent(ctx: BotContext): Promise { +export async function initAgent(mineflayer: Mineflayer): Promise { 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 { +export async function initActionAgent(mineflayer: Mineflayer): Promise { logger.log('Initializing action agent') let actionAgent = agent('action') @@ -46,8 +45,8 @@ export async function initActionAgent(ctx: BotContext): Promise { 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 }, ) diff --git a/services/minecraft/src/components/aichat.ts b/services/minecraft/src/components/aichat.ts deleted file mode 100644 index 7f6aef685..000000000 --- a/services/minecraft/src/components/aichat.ts +++ /dev/null @@ -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) - }, - } -} diff --git a/services/minecraft/src/components/command.ts b/services/minecraft/src/components/command.ts deleted file mode 100644 index a81211212..000000000 --- a/services/minecraft/src/components/command.ts +++ /dev/null @@ -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) - }, - } -} diff --git a/services/minecraft/src/components/echo.ts b/services/minecraft/src/components/echo.ts deleted file mode 100644 index 0280cb4ce..000000000 --- a/services/minecraft/src/components/echo.ts +++ /dev/null @@ -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) - }, - } -} diff --git a/services/minecraft/src/components/follow.ts b/services/minecraft/src/components/follow.ts deleted file mode 100644 index 466d7e7d1..000000000 --- a/services/minecraft/src/components/follow.ts +++ /dev/null @@ -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) - }, - } -} diff --git a/services/minecraft/src/components/pathfinder.ts b/services/minecraft/src/components/pathfinder.ts deleted file mode 100644 index 589a57120..000000000 --- a/services/minecraft/src/components/pathfinder.ts +++ /dev/null @@ -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 - }, - } -} diff --git a/services/minecraft/src/components/status.ts b/services/minecraft/src/components/status.ts deleted file mode 100644 index 560e852df..000000000 --- a/services/minecraft/src/components/status.ts +++ /dev/null @@ -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() - -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 { - 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 - }, - } -} diff --git a/services/minecraft/src/composables/bot.ts b/services/minecraft/src/composables/bot.ts index 34cd302bd..13f7f1202 100644 --- a/services/minecraft/src/composables/bot.ts +++ b/services/minecraft/src/composables/bot.ts @@ -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 - - prompt: { - selfPrompt: string - } - - memory: { - chatHistory: Message[] - actions: Action[] - // getSummary: () => string - } - - status: Map - // status: { - // position: Position - // health: number - // weather: string - // timeOfDay: string - // } - - health: { - value: number - lastDamageTime: number - lastDamageTaken: number - } - - emit: (event: BotInternalEvents) => any - eventListeners: Record> -} - -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: - // - was slain by - // - 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 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, } } diff --git a/services/minecraft/src/composables/command.ts b/services/minecraft/src/composables/command.ts deleted file mode 100644 index 256009a18..000000000 --- a/services/minecraft/src/composables/command.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { CommandContext } from '../middlewares/command' - -export const commands = new Map 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) -} diff --git a/services/minecraft/src/composables/events.ts b/services/minecraft/src/composables/events.ts deleted file mode 100644 index e00c57096..000000000 --- a/services/minecraft/src/composables/events.ts +++ /dev/null @@ -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 = BotInternalEventHandlers[K] diff --git a/services/minecraft/src/composables/world.ts b/services/minecraft/src/composables/world.ts index 095444c55..fae3c94ec 100644 --- a/services/minecraft/src/composables/world.ts +++ b/services/minecraft/src/composables/world.ts @@ -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 { - return getInventoryStacks(ctx).reduce((counts, item) => { +export function getInventoryCounts(mineflayer: Mineflayer): Record { + return getInventoryStacks(mineflayer).reduce((counts, item) => { counts[item.name] = (counts[item.name] || 0) + item.count return counts }, {} as Record) } -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 { - const movements = new pf.Movements(ctx.bot) +export async function isClearPath(mineflayer: Mineflayer, target: Entity): Promise { + 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 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 } diff --git a/services/minecraft/src/middlewares/command.ts b/services/minecraft/src/libs/mineflayer/command.ts similarity index 100% rename from services/minecraft/src/middlewares/command.ts rename to services/minecraft/src/libs/mineflayer/command.ts diff --git a/services/minecraft/src/libs/mineflayer/index.ts b/services/minecraft/src/libs/mineflayer/index.ts new file mode 100644 index 000000000..f52c8a764 --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/index.ts @@ -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 + '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 = EventHandlers[K] + +export type Handler = (ctx: Context) => void | Promise + +function createEventHandlers(): Record>> { + 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 + +export interface Action { + readonly name: string + readonly description: string + readonly schema: z.ZodObject + 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 = new Map() + private logger: ReturnType + + 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 +} + +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 + private commands: Map> = 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: + // - was slain by + // - 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) { + this.ticker.on(event, cb) + } + + public emit(event: E, ...args: Parameters>) { + 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 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; + } +} diff --git a/services/minecraft/src/middlewares/chat.ts b/services/minecraft/src/libs/mineflayer/message.ts similarity index 54% rename from services/minecraft/src/middlewares/chat.ts rename to services/minecraft/src/libs/mineflayer/message.ts index ae255d2e7..aa35fc7e4 100644 --- a/services/minecraft/src/middlewares/chat.ts +++ b/services/minecraft/src/libs/mineflayer/message.ts @@ -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) } } diff --git a/services/minecraft/src/libs/mineflayer/plugin.ts b/services/minecraft/src/libs/mineflayer/plugin.ts new file mode 100644 index 000000000..a65f52eef --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/plugin.ts @@ -0,0 +1,15 @@ +import type { Bot, BotOptions, Plugin } from 'mineflayer' +import type { Mineflayer } from '.' + +export interface MineflayerPlugin { + created?: (mineflayer: Mineflayer) => void | Promise + loadPlugin?: (mineflayer: Mineflayer, bot: Bot, options: BotOptions) => Plugin + spawned?: (mineflayer: Mineflayer) => void | Promise + beforeCleanup?: (mineflayer: Mineflayer) => void | Promise +} + +export function wrapPlugin(plugin: Plugin): MineflayerPlugin { + return { + loadPlugin: () => (plugin), + } +} diff --git a/services/minecraft/src/libs/mineflayer/ticker.ts b/services/minecraft/src/libs/mineflayer/ticker.ts new file mode 100644 index 000000000..8f95372e9 --- /dev/null +++ b/services/minecraft/src/libs/mineflayer/ticker.ts @@ -0,0 +1,62 @@ +export interface TickContext { + delta: number + nextTick: () => Promise +} + +export interface TickEventHandlers { + tick: (ctx: TickContext) => void +} + +export type TickEvents = keyof TickEventHandlers +export type TickEventsHandler = 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> = { + tick: [], + } + + constructor(options?: { interval?: number }) { + const { interval = 300 } = options ?? { interval: 300 } + + let last = Date.now() + const tickingCbs: Record> = { + tick: [], + } + + setTimeout(async () => { + while (true) { + const start = Date.now() + const nextTickPromise = new Promise((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(event: K, cb: TickEventsHandler) { + this.tickingCbs[event].push(cb) + } +} diff --git a/services/minecraft/src/main.ts b/services/minecraft/src/main.ts index 84729b0ce..612881806 100644 --- a/services/minecraft/src/main.ts +++ b/services/minecraft/src/main.ts @@ -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) }) } diff --git a/services/minecraft/src/mineflayer/echo.ts b/services/minecraft/src/mineflayer/echo.ts new file mode 100644 index 000000000..aa6799a9a --- /dev/null +++ b/services/minecraft/src/mineflayer/echo.ts @@ -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) + }, + } +} diff --git a/services/minecraft/src/mineflayer/follow.ts b/services/minecraft/src/mineflayer/follow.ts new file mode 100644 index 000000000..7b5c5d8da --- /dev/null +++ b/services/minecraft/src/mineflayer/follow.ts @@ -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() + }) + }, + } +} diff --git a/services/minecraft/src/mineflayer/index.ts b/services/minecraft/src/mineflayer/index.ts new file mode 100644 index 000000000..7363af280 --- /dev/null +++ b/services/minecraft/src/mineflayer/index.ts @@ -0,0 +1,4 @@ +export * from './echo' +export * from './follow' +export * from './pathfinder' +export * from './status' diff --git a/services/minecraft/src/mineflayer/llm-agent.ts b/services/minecraft/src/mineflayer/llm-agent.ts new file mode 100644 index 000000000..430d68ff6 --- /dev/null +++ b/services/minecraft/src/mineflayer/llm-agent.ts @@ -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) + }, + } +} diff --git a/services/minecraft/src/mineflayer/pathfinder.ts b/services/minecraft/src/mineflayer/pathfinder.ts new file mode 100644 index 000000000..cb2ae6ae7 --- /dev/null +++ b/services/minecraft/src/mineflayer/pathfinder.ts @@ -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) + }, + } +} diff --git a/services/minecraft/src/mineflayer/status.ts b/services/minecraft/src/mineflayer/status.ts new file mode 100644 index 000000000..2500835b1 --- /dev/null +++ b/services/minecraft/src/mineflayer/status.ts @@ -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) + }) + }, + } +} diff --git a/services/minecraft/src/prompts/agent.ts b/services/minecraft/src/prompts/agent.ts index a6623537b..662a3c8cf 100644 --- a/services/minecraft/src/prompts/agent.ts +++ b/services/minecraft/src/prompts/agent.ts @@ -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 diff --git a/services/minecraft/src/skills/base.ts b/services/minecraft/src/skills/base.ts index 8bbcdd9bb..7991de9c6 100644 --- a/services/minecraft/src/skills/base.ts +++ b/services/minecraft/src/skills/base.ts @@ -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) } /** diff --git a/services/minecraft/src/skills/blocks.ts b/services/minecraft/src/skills/blocks.ts index 1e4469d76..8446229c5 100644 --- a/services/minecraft/src/skills/blocks.ts +++ b/services/minecraft/src/skills/blocks.ts @@ -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 { - const worldCtx = world.createWorldContext(ctx.botCtx) - if (world.shouldPlaceTorch(worldCtx)) { +async function autoLight(mineflayer: Mineflayer): Promise { + 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 { * Break a block at the specified position */ export async function breakBlockAt( - ctx: SkillContext, + mineflayer: Mineflayer, x: number, y: number, z: number, ): Promise { 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 { - 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 { + 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 { - 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 { + 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 { - await ctx.bot.tool.equipForBlock(block) +async function breakInSurvival(mineflayer: Mineflayer, block: any, x: number, y: number, z: number): Promise { + 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 { 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 { 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 { 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 { - 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): 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 { - 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 { - doorPos = doorPos || await findNearestDoor(ctx.bot) +export async function useDoor(mineflayer: Mineflayer, doorPos: Vec3 | null = null): Promise { + 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 { @@ -421,30 +423,35 @@ async function findNearestDoor(bot: any): Promise { return null } -async function operateDoor(ctx: SkillContext, doorPos: Vec3): Promise { - const doorBlock = ctx.bot.blockAt(doorPos) - await ctx.bot.lookAt(doorPos) +async function operateDoor(mineflayer: Mineflayer, doorPos: Vec3): Promise { + 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 { 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 { +async function tillBlock(mineflayer: Mineflayer, block: any, pos: any): Promise { 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 { +async function sowSeeds(mineflayer: Mineflayer, block: any, seedType: string, pos: any): Promise { 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 { - const worldCtx = world.createWorldContext(ctx.botCtx) - const block = world.getNearestBlock(worldCtx, type, 16) +export async function activateNearestBlock(mineflayer: Mineflayer, type: string): Promise { + 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 { 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 { - 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 { + 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 { +async function tryCollectBlock(mineflayer: Mineflayer, block: any, blockType: string): Promise { 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 } } diff --git a/services/minecraft/src/skills/combat.ts b/services/minecraft/src/skills/combat.ts index 2ae048487..9717b8866 100644 --- a/services/minecraft/src/skills/combat.ts +++ b/services/minecraft/src/skills/combat.ts @@ -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 { - const { bot } = ctx - const weapons = bot.inventory.items().filter(item => +async function equipHighestAttack(mineflayer: Mineflayer): Promise { + 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 { 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 { - 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 { - 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 { - const { bot } = ctx +export async function defendSelf(mineflayer: Mineflayer, range = 9): Promise { 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 } diff --git a/services/minecraft/src/skills/crafting.ts b/services/minecraft/src/skills/crafting.ts index 5e3824ab1..79c9137f3 100644 --- a/services/minecraft/src/skills/crafting.ts +++ b/services/minecraft/src/skills/crafting.ts @@ -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 { +export async function craftRecipe(mineflayer: Mineflayer, itemName: string, num = 1): Promise { 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 { +export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = 1): Promise { 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 { - const worldCtx = createWorldContext(ctx.botCtx) - const furnaceBlock = world.getNearestBlock(worldCtx, 'furnace', 32) +export async function clearNearestFurnace(mineflayer: Mineflayer): Promise { + 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 { 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 } diff --git a/services/minecraft/src/skills/inventory.ts b/services/minecraft/src/skills/inventory.ts index 981126363..b010a85b0 100644 --- a/services/minecraft/src/skills/inventory.ts +++ b/services/minecraft/src/skills/inventory.ts @@ -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 { +export async function pickupNearbyItems(mineflayer: Mineflayer): Promise { const distance = 8 const getNearestItem = (bot: Bot) => bot.nearestEntity(entity => @@ -15,66 +15,66 @@ export async function pickupNearbyItems(ctx: SkillContext): Promise { && 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 { - const item = ctx.bot.inventory.slots.find(slot => slot && slot.name === itemName) +export async function equip(mineflayer: Mineflayer, itemName: string): Promise { + 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 { +export async function discard(mineflayer: Mineflayer, itemName: string, num = -1): Promise { 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 { - const chest = world.getNearestBlock(world.createWorldContext(ctx.botCtx), 'chest', 32) +export async function putInChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise { + 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 { - const chest = world.getNearestBlock(world.createWorldContext(ctx.botCtx), 'chest', 32) +export async function takeFromChest(mineflayer: Mineflayer, itemName: string, num = -1): Promise { + 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 { - const chest = world.getNearestBlock(world.createWorldContext(ctx.botCtx), 'chest', 32) +export async function viewChest(mineflayer: Mineflayer): Promise { + 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 { return true } -export async function consume(ctx: SkillContext, itemName = ''): Promise { +export async function consume(mineflayer: Mineflayer, itemName = ''): Promise { 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 { - 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 } diff --git a/services/minecraft/src/skills/movement.ts b/services/minecraft/src/skills/movement.ts index 115d054aa..c7b648c1c 100644 --- a/services/minecraft/src/skills/movement.ts +++ b/services/minecraft/src/skills/movement.ts @@ -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 { 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 { 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 { - 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 { - 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 { - // 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 { - const pos = ctx.bot.entity.position +export async function moveAway(mineflayer: Mineflayer, distance: number): Promise { + 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 { 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 { +export async function stay(mineflayer: Mineflayer, seconds = 30): Promise { 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 { - const beds = ctx.bot.findBlocks({ +export async function goToBed(mineflayer: Mineflayer): Promise { + 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 } diff --git a/services/minecraft/src/utils/ticker.ts b/services/minecraft/src/utils/ticker.ts deleted file mode 100644 index 735187c80..000000000 --- a/services/minecraft/src/utils/ticker.ts +++ /dev/null @@ -1,58 +0,0 @@ -export interface TickContext { - delta: number - nextTick: () => Promise -} - -export interface TickEventHandlers { - tick: (ctx: TickContext) => void -} - -export type TickEvents = keyof TickEventHandlers -export type TickEventsHandler = 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> = { - tick: [], - } - - setTimeout(async () => { - while (true) { - const start = Date.now() - const nextTickPromise = new Promise((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(event: K, cb: TickEventsHandler) { - tickingCbs[event].push(cb) - }, - } -}