diff --git a/package.json b/package.json index e7699a0..ee33776 100644 --- a/package.json +++ b/package.json @@ -11,5 +11,8 @@ "openai": "^4.4.0", "patch-package": "^8.0.0", "yargs": "^17.7.2" + }, + "scripts": { + "postinstall": "patch-package" } } diff --git a/patches/mineflayer+4.14.0.patch b/patches/mineflayer+4.14.0.patch index 517c55e..3a36ebb 100644 --- a/patches/mineflayer+4.14.0.patch +++ b/patches/mineflayer+4.14.0.patch @@ -13,7 +13,7 @@ index fdaec6b..e471e70 100644 if (oldBlock.type === newBlock.type) { [oldBlock, newBlock] = await onceWithCleanup(bot, `blockUpdate:${dest}`, { - timeout: 5000, -+ timeout: 300, ++ timeout: 500, // Condition to wait to receive block update actually changing the block type, in case the bot receives block updates with no changes // oldBlock and newBlock will both be null when the world unloads checkCondition: (oldBlock, newBlock) => !oldBlock || !newBlock || oldBlock.type !== newBlock.type diff --git a/src/agent/agent.js b/src/agent/agent.js index a4181c3..730d584 100644 --- a/src/agent/agent.js +++ b/src/agent/agent.js @@ -5,6 +5,7 @@ import { Examples } from './examples.js'; import { Coder } from './coder.js'; import { containsCommand, commandExists, executeCommand } from './commands.js'; import { Events } from './events.js'; +import { initModes } from './modes.js'; export class Agent { @@ -20,7 +21,10 @@ export class Agent { this.bot = initBot(name); - this.events = new Events(this, this.history.events) + this.events = new Events(this, this.history.events); + + initModes(this); + this.idle = true; this.bot.on('login', async () => { @@ -49,7 +53,7 @@ export class Agent { this.bot.autoEat.options = { priority: 'foodPoints', startAt: 14, - bannedFood: [] + bannedFood: ["rotten_flesh", "spider_eye", "poisonous_potato", "pufferfish", "chicken"] }; if (init_message) { @@ -58,9 +62,17 @@ export class Agent { this.bot.chat('Hello world! I am ' + this.name); this.bot.emit('finished_executing'); } + + this.startUpdateLoop(); }); } + cleanChat(message) { + // newlines are interpreted as separate chats, which triggers spam filters. replace them with spaces + message = message.replaceAll('\n', ' '); + return this.bot.chat(message); + } + async handleMessage(source, message) { if (!!source && !!message) await this.history.add(source, message); @@ -75,8 +87,8 @@ export class Agent { let truncated_msg = message.substring(0, message.indexOf(user_command_name)).trim(); this.history.add(source, truncated_msg); } - if (execute_res) - this.bot.chat(execute_res); + if (execute_res) + this.cleanChat(execute_res); return; } @@ -97,7 +109,7 @@ export class Agent { let pre_message = res.substring(0, res.indexOf(command_name)).trim(); - this.bot.chat(`${pre_message} *used ${command_name.substring(1)}*`); + this.cleanChat(`${pre_message} *used ${command_name.substring(1)}*`); let execute_res = await executeCommand(this, res); console.log('Agent executed:', command_name, 'and got:', execute_res); @@ -108,7 +120,7 @@ export class Agent { break; } else { // conversation response - this.bot.chat(res); + this.cleanChat(res); console.log('Purely conversational response:', res); break; } @@ -117,4 +129,36 @@ export class Agent { this.history.save(); this.bot.emit('finished_executing'); } + + startUpdateLoop() { + this.bot.on('error' , (err) => { + console.error('Error event!', err); + }); + this.bot.on('end', (reason) => { + console.warn('Bot disconnected! Killing agent process.', reason) + process.exit(1); + }); + this.bot.on('death', () => { + this.coder.stop(); + }); + this.bot.on('kicked', (reason) => { + console.warn('Bot kicked!', reason); + process.exit(1); + }); + this.bot.on('messagestr', async (message, _, jsonMsg) => { + if (jsonMsg.translate && jsonMsg.translate.startsWith('death') && message.startsWith(this.name)) { + console.log('Agent died: ', message); + this.handleMessage('system', `You died with the final message: '${message}'. Previous actions were stopped and you have respawned. Notify the user and perform any necessary actions.`); + } + }); + + this.self_defense = true; + this.defending = false; + this._pause_defending = false; + + // set interval every 300ms to update the bot's state + this.update_interval = setInterval(async () => { + this.bot.modes.update(); + }, 300); + } } diff --git a/src/agent/coder.js b/src/agent/coder.js index 1cbfe76..6c4e6fe 100644 --- a/src/agent/coder.js +++ b/src/agent/coder.js @@ -89,7 +89,7 @@ export class Coder { async generateCode(agent_history) { - let system_message = "You are a minecraft bot that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world queries to write your code in a codeblock. Example response: ``` // your code here ``` You will then be given a response to your code. If you are satisfied with the response, respond without a codeblock in a conversational way. If something went wrong, write another codeblock and try to fix the problem."; + let system_message = "You are a minecraft mineflayer bot that plays minecraft by writing javascript codeblocks. Given the conversation between you and the user, use the provided skills and world functions to write your code in a codeblock. Example response: ``` // your code here ``` You will then be given a response to your code. If you are satisfied with the response, respond without a codeblock in a conversational way. If something went wrong, write another codeblock and try to fix the problem."; system_message += getSkillDocs(); system_message += "\n\nExamples:\nUser zZZn98: come here \nAssistant: I am going to navigate to zZZn98. ```\nawait skills.goToPlayer(bot, 'zZZn98');```\nSystem: Code execution finished successfully.\nAssistant: Done."; @@ -144,6 +144,9 @@ export class Coder { role: 'system', content: code_return.message }); + + if (this.agent.bot.interrupt_code) + return; } return diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 76577b3..236a3db 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -1,13 +1,15 @@ import * as skills from '../skills.js'; import * as world from '../world.js'; -function wrapExecution(func) { +function wrapExecution(func, timeout=-1) { return async function (agent, ...args) { + agent.idle = false; let code_return = await agent.coder.execute(async () => { await func(agent, ...args); - }, -1); // no timeout + }, timeout); if (code_return.interrupted && !code_return.timedout) return; + agent.idle = true; return code_return.message; } } @@ -17,7 +19,9 @@ export const actionsList = [ name: '!newAction', description: 'Perform new and unknown custom behaviors that are not available as a command by writing code.', perform: async function (agent) { + agent.idle = false; let res = await agent.coder.generateCode(agent.history); + agent.idle = true; if (res) return '\n' + res + '\n'; } @@ -27,9 +31,28 @@ export const actionsList = [ description: 'Force stop all actions and commands that are currently executing.', perform: async function (agent) { await agent.coder.stop(); + agent.coder.clear(); + agent.idle = true; return 'Agent stopped.'; } }, + { + 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)', + params: { + 'mode_name': '(string) The name of the mode to enable.', + 'on': '(bool) Whether to enable or disable the mode.' + }, + perform: async function (agent, mode_name, on) { + const modes = agent.bot.modes; + if (!modes.exists(mode_name)) + return `Mode ${mode_name} does not exist.` + modes.getDocs(); + if (modes.isOn(mode_name) === on) + return `Mode ${mode_name} is already ${on ? 'on' : 'off'}.`; + modes.setOn(mode_name, on); + return `Mode ${mode_name} is now ${on ? 'on' : 'off'}.`; + } + }, { name: '!goToPlayer', description: 'Go to the given player. Ex: !goToPlayer("steve")', @@ -40,7 +63,7 @@ export const actionsList = [ }, { name: '!followPlayer', - description: 'Endlessly follow the given player. Ex: !followPlayer("stevie")', + description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on. Ex: !followPlayer("stevie")', params: {'player_name': '(string) The name of the player to follow.'}, perform: wrapExecution(async (agent, player_name) => { await skills.followPlayer(agent.bot, player_name); @@ -55,7 +78,7 @@ export const actionsList = [ }, perform: wrapExecution(async (agent, type, num) => { await skills.collectBlock(agent.bot, type, num); - }) + }, 10) // 10 minute timeout }, { name: '!craftRecipe', @@ -84,15 +107,7 @@ export const actionsList = [ description: 'Attack and kill the nearest entity of a given type.', params: {'type': '(string) The type of entity to attack.'}, perform: wrapExecution(async (agent, type) => { - await skills.attackMob(agent.bot, type, true); - }) - }, - { - name: '!defend', - description: 'Follow the given player and attack any nearby monsters.', - params: {'player_name': '(string) The name of the player to defend.'}, - perform: wrapExecution(async (agent, player_name) => { - await skills.defendPlayer(agent.bot, player_name); + await skills.attackNearest(agent.bot, type, true); }) }, { diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index 6e95765..495603e 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -1,4 +1,4 @@ -import { getNearestBlock, getNearbyMobTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from '../world.js'; +import { getNearestBlock, getNearbyEntityTypes, getNearbyPlayerNames, getNearbyBlockTypes, getInventoryCounts } from '../world.js'; import { getAllItems, getBiomeName } from '../../utils/mcdata.js'; const pad = (str) => { @@ -19,6 +19,12 @@ export const queryList = [ res += `\n- Health: ${Math.round(bot.health)} / 20`; res += `\n- Hunger: ${Math.round(bot.food)} / 20`; res += `\n- Biome: ${getBiomeName(bot)}`; + let weather = "clear"; + if (bot.rainState > 0) + weather = "Rain"; + if (bot.thunderState > 0) + weather = "Thunderstorm"; + res += `\n- Weather: ${weather}`; // let block = bot.blockAt(pos); // res += `\n- Artficial light: ${block.skyLight}`; // res += `\n- Sky light: ${block.light}`; @@ -95,7 +101,7 @@ export const queryList = [ for (const entity of getNearbyPlayerNames(bot)) { res += `\n- player: ${entity}`; } - for (const entity of getNearbyMobTypes(bot)) { + for (const entity of getNearbyEntityTypes(bot)) { res += `\n- mob: ${entity}`; } if (res == 'NEARBY_ENTITIES') { @@ -104,11 +110,18 @@ export const queryList = [ return pad(res); } }, + { + name: "!modes", + description: "Get all available modes and see which are on/off.", + perform: function (agent) { + return agent.bot.modes.getDocs(); + } + }, { name: "!currentAction", description: "Get the currently executing code.", perform: function (agent) { return pad("Current code:\n`" + agent.coder.current_code +"`"); } - } + }, ]; \ No newline at end of file diff --git a/src/agent/modes.js b/src/agent/modes.js new file mode 100644 index 0000000..a2ac1a2 --- /dev/null +++ b/src/agent/modes.js @@ -0,0 +1,200 @@ +import * as skills from './skills.js'; +import * as world from './world.js'; + +// a mode is a function that is called every tick to respond immediately to the world +// it has the following fields: +// on: whether 'update' is called every tick +// active: whether an action has been triggered by the mode and hasn't yet finished +// 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 +const modes = [ + { + name: 'self_defense', + description: 'Automatically attack nearby enemies. Interrupts other actions.', + on: true, + active: false, + update: function (agent) { + if (this.active) return; + const enemy = world.getNearestEntityWhere(agent.bot, entity => skills.isHostile(entity), 8); + if (enemy) { + agent.bot.chat(`Fighting ${enemy.name}!`); + execute(this, agent, async () => { + await skills.defendSelf(agent.bot, 8); + }); + } + } + }, + { + name: 'hunting', + description: 'Automatically hunt nearby animals when idle.', + on: true, + active: false, + update: function (agent) { + if (agent.idle) { + const huntable = world.getNearestEntityWhere(agent.bot, entity => skills.isHuntable(entity), 8); + if (huntable) { + execute(this, agent, async () => { + agent.bot.chat(`Hunting ${huntable.name}!`); + await skills.attackEntity(agent.bot, huntable); + }); + } + } + } + }, + { + name: 'item_collecting', + description: 'Automatically collect nearby items when idle.', + on: true, + active: false, + update: function (agent) { + if (agent.idle) { + let item = world.getNearestEntityWhere(agent.bot, entity => entity.name === 'item', 8); + if (item) { + execute(this, agent, async () => { + // wait 2 seconds for the item to settle + await new Promise(resolve => setTimeout(resolve, 2000)); + await skills.pickupNearbyItem(agent.bot); + }); + } + } + } + }, + { + name: 'torch_placing', + description: 'Automatically place torches when idle and there are no torches nearby.', + on: true, + active: false, + update: function (agent) { + if (this.active) return; + if (agent.idle) { + // TODO: check light level instead of nearby torches, block.light is broken + const near_torch = world.getNearestBlock(agent.bot, 'torch', 8); + if (!near_torch) { + let torches = agent.bot.inventory.items().filter(item => item.name.includes('torch')); + if (torches.length > 0) { + const torch = torches[0]; + const pos = agent.bot.entity.position; + execute(this, agent, async () => { + await skills.placeBlock(agent.bot, torch.name, pos.x, pos.y, pos.z); + }); + } + } + } + } + }, + { + name: 'idle_staring', + description: 'Non-functional animation to look around at entities when idle.', + on: true, + active: false, + + staring: false, + last_entity: null, + next_change: 0, + update: function (agent) { + if (agent.idle) { + this.active = true; + const entity = agent.bot.nearestEntity(); + let entity_in_view = entity && entity.position.distanceTo(agent.bot.entity.position) < 10 && entity.name !== 'enderman'; + if (entity_in_view && entity !== this.last_entity) { + this.staring = true; + this.last_entity = entity; + this.next_change = Date.now() + Math.random() * 1000 + 4000; + } + if (entity_in_view && this.staring) { + let isbaby = entity.type !== 'player' && entity.metadata[16]; + let height = isbaby ? entity.height/2 : entity.height; + agent.bot.lookAt(entity.position.offset(0, height, 0)); + } + if (!entity_in_view) + this.last_entity = null; + if (Date.now() > this.next_change) { + // look in random direction + this.staring = Math.random() < 0.3; + if (!this.staring) { + const yaw = Math.random() * Math.PI * 2; + const pitch = (Math.random() * Math.PI/2) - Math.PI/4; + agent.bot.look(yaw, pitch, false); + } + this.next_change = Date.now() + Math.random() * 10000 + 2000; + } + } + else + this.active = false; + } + }, +]; + +async function execute(mode, agent, func, timeout=-1) { + mode.active = true; + await agent.coder.stop(); + agent.idle = false; + let code_return = await agent.coder.execute(async () => { + await func(); + }, timeout); + mode.active = false; + agent.idle = true; + console.log(`Mode ${mode.name} finished executing, code_return: ${code_return.message}`); +} + +class ModeController { + constructor(agent) { + this.agent = agent; + this.modes_list = modes; + this.modes_map = {}; + for (let mode of this.modes_list) { + this.modes_map[mode.name] = mode; + } + } + + exists(mode_name) { + return this.modes_map[mode_name] != null; + } + + setOn(mode_name, on) { + this.modes_map[mode_name].on = on; + } + + isOn(mode_name) { + return this.modes_map[mode_name].on; + } + + pause(mode_name) { + this.modes_map[mode_name].paused = true; + } + + getDocs() { + let res = 'Available Modes:'; + for (let mode of this.modes_list) { + let on = mode.on ? 'ON' : 'OFF'; + res += `\n- ${mode.name}(${on}): ${mode.description}`; + } + return res; + } + + update() { + if (this.agent.idle) { + // other actions might pause a mode to override it + // when idle, unpause all modes + for (let mode of this.modes_list) { + if (mode.paused) console.log(`Unpausing mode ${mode.name}`); + mode.paused = false; + } + } + for (let mode of this.modes_list) { + if (mode.on && !mode.paused) { + mode.update(this.agent); + if (mode.active) { + break; + } + } + } + } +} + +export function initModes(agent) { + // the mode controller is added to the bot object so it is accessible from anywhere the bot is used + agent.bot.modes = new ModeController(agent); +} \ No newline at end of file diff --git a/src/agent/skills.js b/src/agent/skills.js index 1148728..af9e96b 100644 --- a/src/agent/skills.js +++ b/src/agent/skills.js @@ -1,10 +1,12 @@ import { getItemId, getItemName } from "../utils/mcdata.js"; -import { getNearestBlocks, getNearestBlock, getInventoryCounts, getInventoryStacks, getNearbyMobs, getNearbyBlocks } from "./world.js"; +import { getNearestBlocks, getNearestBlock, getInventoryCounts, getNearestEntityWhere, getNearbyEntities, getNearbyBlocks } from "./world.js"; import pf from 'mineflayer-pathfinder'; import Vec3 from 'vec3'; -export function log(bot, message) { +export function log(bot, message, chat=false) { bot.output += message + '\n'; + if (chat) + bot.chat(message); } @@ -175,7 +177,8 @@ function equipHighestAttack(bot) { bot.equip(weapon, 'hand'); } -export async function attackMob(bot, mobType, kill=true) { + +export async function attackNearest(bot, mobType, kill=true) { /** * Attack mob of the given type. * @param {MinecraftBot} bot, reference to the minecraft bot. @@ -183,42 +186,92 @@ export async function attackMob(bot, mobType, kill=true) { * @param {boolean} kill, whether or not to continue attacking until the mob is dead. Defaults to true. * @returns {Promise} true if the mob was attacked, false if the mob type was not found. * @example - * await skills.attackMob(bot, "zombie", true); + * await skills.attackNearest(bot, "zombie", true); **/ const mob = bot.nearestEntity(entity => entity.name && entity.name.toLowerCase() === mobType.toLowerCase()); if (mob) { - let pos = mob.position; - console.log(bot.entity.position.distanceTo(pos)) - - equipHighestAttack(bot) - - if (!kill) { - if (bot.entity.position.distanceTo(pos) > 5) { - console.log('moving to mob...') - await goToPosition(bot, pos.x, pos.y, pos.z); - } - console.log('attacking mob...') - await bot.attack(mob); - } - else { - bot.pvp.attack(mob); - while (getNearbyMobs(bot, 16).includes(mob)) { - await new Promise(resolve => setTimeout(resolve, 1000)); - if (bot.interrupt_code) { - bot.pvp.stop(); - return false; - } - } - log(bot, `Successfully killed ${mobType}.`); - await pickupNearbyItem(bot); - return true; - } - + return await attackEntity(bot, mob, kill); } log(bot, 'Could not find any '+mobType+' to attack.'); return false; } +export async function attackEntity(bot, entity, kill=true) { + /** + * Attack mob of the given type. + * @param {MinecraftBot} bot, reference to the minecraft bot. + * @param {Entity} entity, the entity to attack. + * @returns {Promise} true if the entity was attacked, false if interrupted + * @example + * await skills.attackEntity(bot, entity); + **/ + + let pos = entity.position; + console.log(bot.entity.position.distanceTo(pos)) + + equipHighestAttack(bot) + + if (!kill) { + if (bot.entity.position.distanceTo(pos) > 5) { + console.log('moving to mob...') + await goToPosition(bot, pos.x, pos.y, pos.z); + } + console.log('attacking mob...') + await bot.attack(entity); + } + else { + bot.pvp.attack(entity); + while (getNearbyEntities(bot, 16).includes(entity)) { + await new Promise(resolve => setTimeout(resolve, 1000)); + if (bot.interrupt_code) { + bot.pvp.stop(); + return false; + } + } + log(bot, `Successfully killed ${entity.name}.`); + await pickupNearbyItem(bot); + return true; + } +} + +export async function defendSelf(bot, range=8) { + /** + * Defend yourself from all nearby hostile mobs until there are no more. + * @param {MinecraftBot} bot, reference to the minecraft bot. + * @param {number} range, the range to look for mobs. Defaults to 8. + * @returns {Promise} true if the bot found any enemies and has killed them, false if no entities were found. + * @example + * await skills.defendSelf(bot); + * **/ + bot.modes.pause('self_defense'); + let attacked = false; + let enemy = getNearestEntityWhere(bot, entity => isHostile(entity), range); + while (enemy) { + equipHighestAttack(bot); + if (bot.entity.position.distanceTo(enemy.position) > 4 && enemy.name !== 'creeper' && enemy.name !== 'phantom') { + try { + bot.pathfinder.setMovements(new pf.Movements(bot)); + await bot.pathfinder.goto(new pf.goals.GoalFollow(enemy, 2), true); + } catch (err) {/* might error if entity dies, ignore */} + } + bot.pvp.attack(enemy); + attacked = true; + await new Promise(resolve => setTimeout(resolve, 500)); + enemy = getNearestEntityWhere(bot, entity => isHostile(entity), range); + if (bot.interrupt_code) { + bot.pvp.stop(); + return false; + } + } + bot.pvp.stop(); + if (attacked) + log(bot, `Successfully defended self.`); + else + log(bot, `No enemies nearby to defend self from.`); + return attacked; +} + + export async function collectBlock(bot, blockType, num=1) { /** @@ -252,6 +305,7 @@ export async function collectBlock(bot, blockType, num=1) { try { await bot.collectBlock.collect(block); collected++; + autoLight(bot); } catch (err) { if (err.name === 'NoChests') { @@ -263,6 +317,7 @@ export async function collectBlock(bot, blockType, num=1) { continue; } } + if (bot.interrupt_code) break; } @@ -286,7 +341,7 @@ export async function pickupNearbyItem(bot) { return false; } bot.pathfinder.setMovements(new pf.Movements(bot)); - await bot.pathfinder.goto(new pf.goals.GoalNear(nearestItem.position.x, nearestItem.position.y, nearestItem.position.z, 1)); + await bot.pathfinder.goto(new pf.goals.GoalFollow(nearestItem, 0.8), true); log(bot, `Successfully picked up a dropped item.`); return true; } @@ -347,41 +402,24 @@ export async function placeBlock(bot, blockType, x, y, z) { log(bot, `Cannot place ${blockType} at ${targetBlock.position}: nothing to place on.`); return false; } - console.log("Placing on: ", buildOffBlock.position, buildOffBlock.name) let block = bot.inventory.items().find(item => item.name === blockType); if (!block) { log(bot, `Don't have any ${blockType} to place.`); return false; } - - // too close - let blockAbove = bot.blockAt(targetBlock.position.plus(Vec3(0,1,0))) - if (bot.entity.position.distanceTo(targetBlock.position) < 1 || bot.entity.position.distanceTo(blockAbove.position) < 1) { - console.log('moving away from block...') - let found = false; - for(let i = 0; i < 10; i++) { - console.log('looking for block...') - const randomDirection = new Vec3((Math.random() > 0.5 ? 1 : -1), 0, (Math.random() > 0.5 ? 1 : -1)); - const pos = targetBlock.position.add(randomDirection.scale(1.2)); - if (bot.blockAt(pos).name === 'air') { - console.log('found good position') - bot.pathfinder.setMovements(new pf.Movements(bot)); - await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 1.2)); - found = true; - break; - } - } - if (!found) { - console.log('could not find good position') - log(bot, `Was too close to place ${blockType} at ${targetBlock.position}.`) - return false; - } + const pos = bot.entity.position; + const pos_above = pos.plus(Vec3(0,1,0)); + const dont_move_for = ['torch', 'redstone_torch', 'redstone', 'lever', 'button', 'rail', 'detector_rail', 'powered_rail', 'activator_rail', 'tripwire_hook', 'tripwire']; + if (!dont_move_for.includes(blockType) && (pos.distanceTo(targetBlock.position) < 1 || pos_above.distanceTo(targetBlock.position) < 1)) { + // too close + let goal = new pf.goals.GoalNear(targetBlock.position.x, targetBlock.position.y, targetBlock.position.z, 2); + let inverted_goal = new pf.goals.GoalInvert(goal); + bot.pathfinder.setMovements(new pf.Movements(bot)); + await bot.pathfinder.goto(inverted_goal); } - // too far if (bot.entity.position.distanceTo(targetBlock.position) > 4.5) { - // move close until it is within 6 blocks - console.log('moving closer to block...') + // too far let pos = targetBlock.position; bot.pathfinder.setMovements(new pf.Movements(bot)); await bot.pathfinder.goto(new pf.goals.GoalNear(pos.x, pos.y, pos.z, 4)); @@ -390,9 +428,6 @@ export async function placeBlock(bot, blockType, x, y, z) { await bot.equip(block, 'hand'); await bot.lookAt(buildOffBlock.position); - console.log("placing block...") - - console.log('entities:', buildOffBlock.blockEntity, targetBlock.blockEntity) // will throw error if an entity is in the way, and sometimes even if the block was placed try { await bot.placeBlock(buildOffBlock, faceVec); @@ -507,6 +542,7 @@ export async function giveToPlayer(bot, itemType, username, num=1) { return true; } + export async function goToPosition(bot, x, y, z, min_distance=2) { /** * Navigate to the given position. @@ -540,6 +576,7 @@ export async function goToPlayer(bot, username) { * @example * await skills.goToPlayer(bot, "player"); **/ + bot.modes.pause('self_defense'); let player = bot.players[username].entity if (!player) { log(bot, `Could not find ${username}.`); @@ -547,7 +584,7 @@ export async function goToPlayer(bot, username) { } bot.pathfinder.setMovements(new pf.Movements(bot)); - await bot.pathfinder.goto(new pf.goals.GoalFollow(player, 2), true); + await bot.pathfinder.goto(new pf.goals.GoalFollow(player, 3), true); log(bot, `You have reached ${username}.`); } @@ -562,75 +599,53 @@ export async function followPlayer(bot, username) { * @example * await skills.followPlayer(bot, "player"); **/ + bot.modes.pause('self_defense'); + bot.modes.pause('hunting'); + let player = bot.players[username].entity if (!player) return false; - bot.pathfinder.setMovements(new pf.Movements(bot)); - bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, 2), true); - log(bot, `You are now actively following player ${username}.`); - - while (!bot.interrupt_code) { - await new Promise(resolve => setTimeout(resolve, 1000)); - } - - return true; -} - -export async function defendPlayer(bot, username) { - /** - * Defend the given player endlessly, attacking any nearby monsters. Will not return until the code is manually stopped. - * @param {MinecraftBot} bot, reference to the minecraft bot. - * @param {string} username, the username of the player to defend. - * @returns {Promise} true if the player was found, false otherwise. - * @example - * await skills.defendPlayer(bot, "bob"); - **/ - let player = bot.players[username].entity - if (!player) - return false; - - const follow_distance = 3; - const attack_distance = 12; - const return_distance = 16; + const follow_distance = 4; + const attack_distance = 8; bot.pathfinder.setMovements(new pf.Movements(bot)); bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, follow_distance), true); - log(bot, `Actively defending player ${username}.`); + log(bot, `You are now actively following player ${username}.`); while (!bot.interrupt_code) { - if (bot.entity.position.distanceTo(player.position) < return_distance) { - const mobs = getNearbyMobs(bot, attack_distance).filter(mob => mob.type === 'mob' || mob.type === 'hostile'); - const mob = mobs.sort((a, b) => a.position.distanceTo(player.position) - b.position.distanceTo(player.position))[0]; // get closest to player - if (mob) { - bot.pathfinder.stop(); - log(bot, `Found ${mob.name}, attacking!`); - bot.chat(`Found ${mob.name}, attacking!`); - equipHighestAttack(bot); - bot.pvp.attack(mob); - while (getNearbyMobs(bot, attack_distance).includes(mob)) { - await new Promise(resolve => setTimeout(resolve, 500)); - console.log('attacking...') - if (bot.interrupt_code) - return; - if (bot.entity.position.distanceTo(player.position) > return_distance) { - console.log('stopping pvp...'); - bot.pvp.stop(); - break; - } - } - console.log('resuming pathfinder...') - bot.pathfinder.setMovements(new pf.Movements(bot)); - bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, 5), true); - await new Promise(resolve => setTimeout(resolve, 3000)); + let acted = false; + if (bot.modes.isOn('self_defense')) { + const enemy = getNearestEntityWhere(bot, entity => isHostile(entity), attack_distance); + if (enemy) { + log(bot, `Found ${enemy.name}, attacking!`, true); + await defendSelf(bot, 8); + acted = true; } } + if (bot.modes.isOn('hunting')) { + const animal = getNearestEntityWhere(bot, entity => isHuntable(entity), attack_distance); + if (animal) { + log(bot, `Hunting ${animal.name}!`, true); + await attackEntity(bot, animal, true); + acted = true; + } + } + if (bot.entity.position.distanceTo(player.position) < follow_distance) { + acted = autoLight(bot); + } + + if (acted) { // if we did something then resume following + bot.pathfinder.setMovements(new pf.Movements(bot)); + bot.pathfinder.setGoal(new pf.goals.GoalFollow(player, follow_distance), true); + } await new Promise(resolve => setTimeout(resolve, 500)); } return true; } + export async function goToBed(bot) { /** * Sleep in the nearest bed. @@ -661,3 +676,34 @@ export async function goToBed(bot) { log(bot, `You have woken up.`); return true; } + + +export function isHuntable(mob) { + if (!mob || !mob.name) return false; + const animals = ['chicken', 'cod', 'cow', 'llama', 'mooshroom', 'pig', 'pufferfish', 'rabbit', 'salmon', 'sheep', 'squid', 'tropical_fish', 'turtle']; + return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby +} + + +export function isHostile(mob) { + if (!mob || !mob.name) return false; + return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; +} + + +async function autoLight(bot) { + if (bot.modes.isOn('torch_placing') && !bot.interrupt_code) { + let nearest_torch = getNearestBlock(bot, 'torch', 8); + if (!nearest_torch) { + let has_torch = bot.inventory.items().find(item => item.name === 'torch'); + if (has_torch) { + try { + log(bot, `Placing torch at ${bot.entity.position}.`); + await placeBlock(bot, 'torch', bot.entity.position.x, bot.entity.position.y, bot.entity.position.z); + return true; + } catch (err) {return true;} + } + } + } + return false; +} diff --git a/src/agent/world.js b/src/agent/world.js index 131e36d..cfb2e64 100644 --- a/src/agent/world.js +++ b/src/agent/world.js @@ -63,8 +63,7 @@ export function getNearbyBlocks(bot, maxDistance) { } -export function getNearbyMobs(bot, maxDistance) { - if (maxDistance == null) maxDistance = 16; +export function getNearbyEntities(bot, maxDistance=16) { let entities = []; for (const entity of Object.values(bot.entities)) { const distance = entity.position.distanceTo(bot.entity.position); @@ -79,6 +78,10 @@ export function getNearbyMobs(bot, maxDistance) { return res; } +export function getNearestEntityWhere(bot, predicate, maxDistance=16) { + return bot.nearestEntity(entity => predicate(entity) && bot.entity.position.distanceTo(entity.position) < maxDistance); +} + export function getNearbyPlayers(bot, maxDistance) { if (maxDistance == null) maxDistance = 16; @@ -121,13 +124,15 @@ export function getInventoryCounts(bot) { * let hasWoodenPickaxe = inventory['wooden_pickaxe'] > 0; **/ let inventory = {}; - for (const item of getInventoryStacks(bot)) { - if (inventory.hasOwnProperty(item.name)) { - inventory[item.name] = inventory[item.name] + item.count; - } else { - inventory[item.name] = item.count; + for (const item of bot.inventory.items()) { + if (item != null) { + if (inventory[item.name] == null) { + inventory[item.name] = 0; + } + inventory[item.name] += item.count; } } + console.log(inventory) return inventory; } @@ -145,15 +150,15 @@ export function getPosition(bot) { } -export function getNearbyMobTypes(bot) { +export function getNearbyEntityTypes(bot) { /** * Get a list of all nearby mob types. * @param {Bot} bot - The bot to get nearby mobs for. * @returns {string[]} - A list of all nearby mobs. * @example - * let mobs = world.getNearbyMobTypes(bot); + * let mobs = world.getNearbyEntityTypes(bot); **/ - let mobs = getNearbyMobs(bot, 16); + let mobs = getNearbyEntities(bot, 16); let found = []; for (let i = 0; i < mobs.length; i++) { if (!found.includes(mobs[i].name)) { diff --git a/src/examples.json b/src/examples.json index 6a73f41..570ef6c 100644 --- a/src/examples.json +++ b/src/examples.json @@ -70,5 +70,15 @@ {"role": "assistant", "content": "!craftRecipe('stick', 4)"}, {"role": "system", "content": "Code Output:\nYou have crafted 16 sticks.\nCode execution finished successfully."}, {"role": "assistant", "content": "I've crafted 16 sticks!"} + ], + + [ + {"role": "user", "content": "brung00: build a house"}, + {"role": "assistant", "content": "Sure, I'll try to build a house where I am. !newAction"} + ], + + [ + {"role": "user", "content": "reter: place a crafting table"}, + {"role": "assistant", "content": "Okay! !placeHere('crafting_table')"} ] ] \ No newline at end of file diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 425f914..0e5f884 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -7,15 +7,21 @@ import { plugin as autoEat } from 'mineflayer-auto-eat'; import plugin from 'mineflayer-armor-manager'; const armorManager = plugin; -const mc_version = '1.19.3' +const mc_version = '1.20.1' const mcdata = minecraftData(mc_version); export function initBot(username) { let bot = createBot({ + username: username, + host: 'localhost', port: 55916, - username: username, + + // host: '000.111.222.333', + // port: 55920, + // auth: 'microsoft', + version: mc_version, }); bot.loadPlugin(pathfinder);