diff --git a/src/agent/agent.js b/src/agent/agent.js index cb1d9c7..1ca99a6 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -75,6 +75,10 @@ export class Agent { const user_command_name = containsCommand(message); if (user_command_name) { + if (!commandExists(user_command_name)) { + this.bot.chat(`Command '${user_command_name}' does not exist.`); + return; + } this.bot.chat(`*${source} used ${user_command_name.substring(1)}*`); let execute_res = await executeCommand(this, message); if (user_command_name === '!newAction') { @@ -165,10 +169,18 @@ export class Agent { } }); - // set interval every 300ms to update the bot's state - this.update_interval = setInterval(async () => { - this.bot.modes.update(); - }, 300); + // This update loop ensures that each update() is called one at a time, even if it takes longer than the interval + const INTERVAL = 300; + setTimeout(async () => { + while (true) { + let start = Date.now(); + await this.bot.modes.update(); + let remaining = INTERVAL - (Date.now() - start); + if (remaining > 0) { + await new Promise((resolve) => setTimeout(resolve, remaining)); + } + } + }, INTERVAL); } isIdle() { diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 119de1f..4596b04 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -29,6 +29,21 @@ export const actionsList = [ return 'Agent stopped.'; } }, + { + name: '!restart', + description: 'Restart the agent process.', + perform: async function (agent) { + process.exit(1); + } + }, + { + name: '!clear', + description: 'Clear the chat history.', + perform: async function (agent) { + agent.history.clear(); + return agent.name + "'s chat history was cleared, starting new conversation from scratch."; + } + }, { name: '!setMode', description: 'Set a mode to on or off. A mode is an automatic behavior that constantly checks and responds to the environment. Ex: !setMode("hunting", true)', diff --git a/src/agent/history.js b/src/agent/history.js index 03245ce..1e020d6 100644 --- a/src/agent/history.js +++ b/src/agent/history.js @@ -112,4 +112,9 @@ export class History { console.error(`No file for profile '${load_path}' for agent ${this.name}.`); } } + + clear() { + this.turns = []; + this.memory = ''; + } } \ No newline at end of file diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 9cc8583..d2a76e9 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -331,8 +331,12 @@ export async function collectBlock(bot, blockType, num=1) { log(bot, `Invalid number of blocks to collect: ${num}.`); return false; } + let blocktypes = [blockType]; + if (blockType.endsWith('ore')) + blocktypes.push('deepslate_'+blockType); + let collected = 0; - const blocks = world.getNearestBlocks(bot, blockType, 64, num); + const blocks = world.getNearestBlocks(bot, blocktypes, 64, num); if (blocks.length === 0) { log(bot, `Could not find any ${blockType} to collect.`); return false; diff --git a/src/agent/library/world.js b/src/agent/library/world.js index 3ab4e27..52c4b74 100644 --- a/src/agent/library/world.js +++ b/src/agent/library/world.js @@ -1,3 +1,4 @@ +import pf from 'mineflayer-pathfinder'; import { getAllBlockIds } from '../../utils/mcdata.js'; @@ -48,9 +49,12 @@ export function getNearestBlocks(bot, block_types, distance=16, count=1) { * @example * let woodBlocks = world.getNearestBlocks(bot, ['oak_log', 'birch_log'], 16, 1); **/ + // if blocktypes is not a list, make it a list + if (!Array.isArray(block_types)) + block_types = [block_types]; let block_locs = bot.findBlocks({ matching: (block) => { - return block && block_types.includes(block.name); + return block && block_types.some(name => name === block.name); }, maxDistance: distance, count: count @@ -243,6 +247,20 @@ export function getNearbyBlockTypes(bot, distance=16) { return found; } +export async function isClearPath(bot, target) { + /** + * Check if there is a path to the target that requires no digging or placing blocks. + * @param {Bot} bot - The bot to get the path for. + * @param {Entity} target - The target to path to. + * @returns {boolean} - True if there is a clear path, false otherwise. + */ + let movements = new pf.Movements(bot) + movements.canDig = false; + movements.canPlaceOn = false; + let goal = new pf.goals.GoalNear(target.position.x, target.position.y, target.position.z, 1); + let path = await bot.pathfinder.getPathTo(movements, goal, 100); + return path.status === 'success'; +} export function getBiomeName(bot) { /** diff --git a/src/agent/modes.js b/src/agent/modes.js index adf87ee..20ae23c 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -10,17 +10,20 @@ import * as mc from '../utils/mcdata.js'; // paused: whether the mode is paused by another action that overrides the behavior (eg followplayer implements its own self defense) // update: the function that is called every tick (if on is true) // when a mode is active, it will trigger an action to be performed but won't wait for it to return output + // the order of this list matters! first modes will be prioritized +// while update functions are async, they should *not* be awaited longer than ~100ms as it will block the update loop +// to perform longer actions, use the execute function which won't block the update loop const modes = [ { name: 'self_defense', description: 'Automatically attack nearby enemies. Interrupts other actions.', on: true, active: false, - update: function (agent) { + update: async function (agent) { if (this.active) return; const enemy = world.getNearestEntityWhere(agent.bot, entity => mc.isHostile(entity), 8); - if (enemy) { + if (enemy && await world.isClearPath(agent.bot, enemy)) { agent.bot.chat(`Fighting ${enemy.name}!`); execute(this, agent, async () => { await skills.defendSelf(agent.bot, 8); @@ -33,10 +36,10 @@ const modes = [ description: 'Automatically hunt nearby animals when idle.', on: true, active: false, - update: function (agent) { + update: async function (agent) { if (agent.isIdle()) { const huntable = world.getNearestEntityWhere(agent.bot, entity => mc.isHuntable(entity), 8); - if (huntable) { + if (huntable && await world.isClearPath(agent.bot, huntable)) { execute(this, agent, async () => { agent.bot.chat(`Hunting ${huntable.name}!`); await skills.attackEntity(agent.bot, huntable); @@ -50,10 +53,10 @@ const modes = [ description: 'Automatically collect nearby items when idle.', on: true, active: false, - update: function (agent) { + update: async function (agent) { if (agent.isIdle()) { let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8); - if (item) { + if (item && await world.isClearPath(agent.bot, item)) { execute(this, agent, async () => { // wait 2 seconds for the item to settle await new Promise(resolve => setTimeout(resolve, 2000)); @@ -173,7 +176,7 @@ class ModeController { return res; } - update() { + async update() { if (this.agent.isIdle()) { // other actions might pause a mode to override it // when idle, unpause all modes @@ -184,7 +187,7 @@ class ModeController { } for (let mode of this.modes_list) { if (mode.on && !mode.paused) { - mode.update(this.agent); + await mode.update(this.agent); if (mode.active) { break; } diff --git a/src/examples.json b/src/examples.json index 570ef6c..863a731 100644 --- a/src/examples.json +++ b/src/examples.json @@ -78,7 +78,9 @@ ], [ - {"role": "user", "content": "reter: place a crafting table"}, - {"role": "assistant", "content": "Okay! !placeHere('crafting_table')"} + {"role": "user", "content": "umpire_man: follow me without hunting"}, + {"role": "assistant", "content": "Okay, first I'll turn off hunting mode !setMode('hunting', false)"}, + {"role": "system", "content": "'hunting' mode disabled."}, + {"role": "assistant", "content": "Now I'll follow you !followPlayer('umpire_man')"} ] ] \ No newline at end of file diff --git a/src/examples_coder.json b/src/examples_coder.json index fd73422..9e20073 100644 --- a/src/examples_coder.json +++ b/src/examples_coder.json @@ -6,10 +6,10 @@ {"role": "assistant", "content": "I collected 9 oak logs, what next?"} ], [ - {"role": "user", "content": "bobby: Craft a plank"}, - {"role": "assistant", "content": "```\nawait skills.craftRecipe(bot, 'spruce_planks');\n```"}, - {"role": "system", "content": "Successfully crafted 4 spruce_planks."}, - {"role": "assistant", "content": "I crafted 4 spruce planks!"} + {"role": "user", "content": "bobby: cook some chicken"}, + {"role": "assistant", "content": "```\nawait skills.smeltItem(bot, 'chicken', 8);\n```"}, + {"role": "system", "content": "Successfully smelted 8 chicken into 8 cooked_chicken."}, + {"role": "assistant", "content": "I have cooked 8 chicken."} ], [ {"role": "user", "content": "zZZn98: come here"}, @@ -30,5 +30,9 @@ {"role": "assistant", "content": "```let pos = bot.entity.position;\nfor (let i = 0; i < 5; i++) {\n await skills.placeBlock(bot, 'dirt', pos.x, pos.y + i, pos.z);\n}\n```"}, {"role": "system", "content": "Successfully placed 5 dirt."}, {"role": "assistant", "content": "I built a little tower!"} + ], + [ + {"role": "user", "content": "brug: build a dirt house"}, + {"role": "assistant", "content": "```const position = world.getPosition(bot);\nconst startX = position.x;\nconst startY = position.y;\nconst startZ = position.z;\nconst width = 7;\nconst depth = 7;\nconst height = 4;\n\n// Build the walls\nfor (let x = startX; x < startX + width; x++) {\n for (let y = startY; y < startY + height; y++) {\n for (let z = startZ; z < startZ + depth; z++) {\n if (x === startX || x === startX + width - 1 || y === startY || y === startY + height - 1 || z === startZ || z === startZ + depth - 1) {\n await skills.placeBlock(bot, 'oak_planks', x, y, z); \n }\n }\n }\n}\n// place a torch in the middle of the room\nawait skills.placeBlock(bot, 'torch', startX + width / 2, startY + height / 2, startZ + 1);```"} ] ] \ No newline at end of file