diff --git a/patches/mineflayer-collectblock+1.4.1.patch b/patches/mineflayer-collectblock+1.4.1.patch index 22440f4..1df504b 100644 --- a/patches/mineflayer-collectblock+1.4.1.patch +++ b/patches/mineflayer-collectblock+1.4.1.patch @@ -1,8 +1,8 @@ diff --git a/node_modules/mineflayer-collectblock/lib/CollectBlock.js b/node_modules/mineflayer-collectblock/lib/CollectBlock.js -index 2c11e8c..4697873 100644 +index 2c11e8c..bb49c11 100644 --- a/node_modules/mineflayer-collectblock/lib/CollectBlock.js +++ b/node_modules/mineflayer-collectblock/lib/CollectBlock.js -@@ -77,7 +77,7 @@ function mineBlock(bot, block, options) { +@@ -77,10 +77,11 @@ function mineBlock(bot, block, options) { } yield bot.tool.equipForBlock(block, equipToolOptions); // @ts-expect-error @@ -11,7 +11,20 @@ index 2c11e8c..4697873 100644 options.targets.removeTarget(block); return; } -@@ -195,6 +195,8 @@ class CollectBlock { ++ + const tempEvents = new TemporarySubscriber_1.TemporarySubscriber(bot); + tempEvents.subscribeTo('itemDrop', (entity) => { + if (entity.position.distanceTo(block.position.offset(0.5, 0.5, 0.5)) <= 0.5) { +@@ -92,7 +93,7 @@ function mineBlock(bot, block, options) { + // Waiting for items to drop + yield new Promise(resolve => { + let remainingTicks = 10; +- tempEvents.subscribeTo('physicTick', () => { ++ tempEvents.subscribeTo('physicsTick', () => { + remainingTicks--; + if (remainingTicks <= 0) { + tempEvents.cleanup(); +@@ -195,6 +196,8 @@ class CollectBlock { throw (0, Util_1.error)('UnresolvedDependency', 'The mineflayer-collectblock plugin relies on the mineflayer-tool plugin to run!'); } if (this.movements != null) { diff --git a/patches/mineflayer-pvp+1.3.2.patch b/patches/mineflayer-pvp+1.3.2.patch new file mode 100644 index 0000000..7ac96b5 --- /dev/null +++ b/patches/mineflayer-pvp+1.3.2.patch @@ -0,0 +1,13 @@ +diff --git a/node_modules/mineflayer-pvp/lib/PVP.js b/node_modules/mineflayer-pvp/lib/PVP.js +index 758c2b3..7c7220e 100644 +--- a/node_modules/mineflayer-pvp/lib/PVP.js ++++ b/node_modules/mineflayer-pvp/lib/PVP.js +@@ -48,7 +48,7 @@ class PVP { + this.meleeAttackRate = new TimingSolver_1.MaxDamageOffset(); + this.bot = bot; + this.movements = new mineflayer_pathfinder_1.Movements(bot, require('minecraft-data')(bot.version)); +- this.bot.on('physicTick', () => this.update()); ++ this.bot.on('physicsTick', () => this.update()); + this.bot.on('entityGone', e => { if (e === this.target) + this.stop(); }); + } diff --git a/src/agent/commands/actions.js b/src/agent/commands/actions.js index 45778d4..0e87c78 100644 --- a/src/agent/commands/actions.js +++ b/src/agent/commands/actions.js @@ -23,7 +23,7 @@ export const actionsList = [ name: '!newAction', description: 'Perform new and unknown custom behaviors that are not available as a command.', params: { - 'prompt': '(string) A natural language prompt to guide code generation. Make a detailed step-by-step plan.' + 'prompt': { type: 'string', description: 'A natural language prompt to guide code generation. Make a detailed step-by-step plan.' } }, perform: async function (agent, prompt) { // just ignore prompt - it is now in context in chat history @@ -75,8 +75,8 @@ export const actionsList = [ name: '!goToPlayer', description: 'Go to the given player.', params: { - 'player_name': '(string) The name of the player to go to.', - 'closeness': '(number) How close to get to the player.' + 'player_name': {type: 'string', description: 'The name of the player to go to.'}, + 'closeness': {type: 'float', description: 'How close to get to the player.', domain: [0, Infinity]} }, perform: wrapExecution(async (agent, player_name, closeness) => { return await skills.goToPlayer(agent.bot, player_name, closeness); @@ -86,8 +86,8 @@ export const actionsList = [ name: '!followPlayer', description: 'Endlessly follow the given player. Will defend that player if self_defense mode is on.', params: { - 'player_name': '(string) The name of the player to follow.', - 'follow_dist': '(number) The distance to follow from.' + 'player_name': {type: 'string', description: 'name of the player to follow.'}, + 'follow_dist': {type: 'float', description: 'The distance to follow from.', domain: [0, Infinity]} }, perform: wrapExecution(async (agent, player_name, follow_dist) => { await skills.followPlayer(agent.bot, player_name, follow_dist); @@ -97,9 +97,9 @@ export const actionsList = [ name: '!goToBlock', description: 'Go to the nearest block of a given type.', params: { - 'type': '(string) The block type to go to.', - 'closeness': '(number) How close to get to the block.', - 'search_range': '(number) The distance to search for the block.' + 'type': { type: 'BlockName', description: 'The block type to go to.' }, + 'closeness': { type: 'float', description: 'How close to get to the block.', domain: [0, Infinity] }, + 'search_range': { type: 'float', description: 'The distance to search for the block.', domain: [0, Infinity] } }, perform: wrapExecution(async (agent, type, closeness, range) => { await skills.goToNearestBlock(agent.bot, type, closeness, range); @@ -108,7 +108,7 @@ export const actionsList = [ { name: '!moveAway', description: 'Move away from the current location in any direction by a given distance.', - params: {'distance': '(number) The distance to move away.'}, + params: {'distance': { type: 'float', description: 'The distance to move away.', domain: [0, Infinity] }}, perform: wrapExecution(async (agent, distance) => { await skills.moveAway(agent.bot, distance); }) @@ -116,7 +116,7 @@ export const actionsList = [ { name: '!rememberHere', description: 'Save the current location with a given name.', - params: {'name': '(string) The name to remember the location as.'}, + params: {'name': { type: 'string', description: 'The name to remember the location as.' }}, perform: async function (agent, name) { const pos = agent.bot.entity.position; agent.memory_bank.rememberPlace(name, pos.x, pos.y, pos.z); @@ -126,7 +126,7 @@ export const actionsList = [ { name: '!goToPlace', description: 'Go to a saved location.', - params: {'name': '(string) The name of the location to go to.'}, + params: {'name': { type: 'string', description: 'The name of the location to go to.' }}, perform: wrapExecution(async (agent, name) => { const pos = agent.memory_bank.recallPlace(name); if (!pos) { @@ -140,9 +140,9 @@ export const actionsList = [ name: '!givePlayer', description: 'Give the specified item to the given player.', params: { - 'player_name': '(string) The name of the player to give the item to.', - 'item_name': '(string) The name of the item to give.' , - 'num': '(number) The number of items to give.' + 'player_name': { type: 'string', description: 'The name of the player to give the item to.' }, + 'item_name': { type: 'ItemName', description: 'The name of the item to give.' }, + 'num': { type: 'int', description: 'The number of items to give.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: wrapExecution(async (agent, player_name, item_name, num) => { await skills.giveToPlayer(agent.bot, item_name, player_name, num); @@ -151,7 +151,7 @@ export const actionsList = [ { name: '!equip', description: 'Equip the given item.', - params: {'item_name': '(string) The name of the item to equip.'}, + params: {'item_name': { type: 'ItemName', description: 'The name of the item to equip.' }}, perform: wrapExecution(async (agent, item_name) => { await skills.equip(agent.bot, item_name); }) @@ -160,8 +160,8 @@ export const actionsList = [ name: '!putInChest', description: 'Put the given item in the nearest chest.', params: { - 'item_name': '(string) The name of the item to put in the chest.', - 'num': '(number) The number of items to put in the chest.' + 'item_name': { type: 'ItemName', description: 'The name of the item to put in the chest.' }, + 'num': { type: 'int', description: 'The number of items to put in the chest.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: wrapExecution(async (agent, item_name, num) => { await skills.putInChest(agent.bot, item_name, num); @@ -171,8 +171,8 @@ export const actionsList = [ name: '!takeFromChest', description: 'Take the given items from the nearest chest.', params: { - 'item_name': '(string) The name of the item to take.', - 'num': '(number) The number of items to take.' + 'item_name': { type: 'ItemName', description: 'The name of the item to take.' }, + 'num': { type: 'int', description: 'The number of items to take.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: wrapExecution(async (agent, item_name, num) => { await skills.takeFromChest(agent.bot, item_name, num); @@ -190,8 +190,8 @@ export const actionsList = [ name: '!discard', description: 'Discard the given item from the inventory.', params: { - 'item_name': '(string) The name of the item to discard.', - 'num': '(number) The number of items to discard.', + 'item_name': { type: 'ItemName', description: 'The name of the item to discard.' }, + 'num': { type: 'int', description: 'The number of items to discard.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: wrapExecution(async (agent, item_name, num) => { const start_loc = agent.bot.entity.position; @@ -204,8 +204,8 @@ export const actionsList = [ name: '!collectBlocks', description: 'Collect the nearest blocks of a given type.', params: { - 'type': '(string) The block type to collect.', - 'num': '(number) The number of blocks to collect.' + 'type': { type: 'BlockName', description: 'The block type to collect.' }, + 'num': { type: 'int', description: 'The number of blocks to collect.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: wrapExecution(async (agent, type, num) => { await skills.collectBlock(agent.bot, type, num); @@ -215,7 +215,7 @@ export const actionsList = [ name: '!collectAllBlocks', description: 'Collect all the nearest blocks of a given type until told to stop.', params: { - 'type': '(string) The block type to collect.' + 'type': { type: 'BlockName', description: 'The block type to collect.' } }, perform: wrapExecution(async (agent, type) => { let success = await skills.collectBlock(agent.bot, type, 1); @@ -227,8 +227,8 @@ export const actionsList = [ name: '!craftRecipe', description: 'Craft the given recipe a given number of times.', params: { - 'recipe_name': '(string) The name of the output item to craft.', - 'num': '(number) 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.' + 'recipe_name': { type: 'ItemName', description: 'The name of the output item to craft.' }, + 'num': { type: 'int', description: '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.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: wrapExecution(async (agent, recipe_name, num) => { await skills.craftRecipe(agent.bot, recipe_name, num); @@ -238,8 +238,8 @@ export const actionsList = [ name: '!smeltItem', description: 'Smelt the given item the given number of times.', params: { - 'item_name': '(string) The name of the input item to smelt.', - 'num': '(number) The number of times to smelt the item.' + 'item_name': { type: 'string', description: 'The name of the input item to smelt.' }, + 'num': { type: 'int', description: 'The number of times to smelt the item.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: async function (agent, item_name, num) { let response = await wrapExecution(async (agent) => { @@ -257,7 +257,7 @@ export const actionsList = [ { name: '!placeHere', description: 'Place a given block in the current location. Do NOT use to build structures, only use for single blocks/torches.', - params: {'type': '(string) The block type to place.'}, + params: {type: 'string', description: 'The block type to place.'}, perform: wrapExecution(async (agent, type) => { let pos = agent.bot.entity.position; await skills.placeBlock(agent.bot, type, pos.x, pos.y, pos.z); @@ -266,7 +266,7 @@ export const actionsList = [ { name: '!attack', description: 'Attack and kill the nearest entity of a given type.', - params: {'type': '(string) The type of entity to attack.'}, + params: {'type': 'string', description: 'The type of entity to attack.'}, perform: wrapExecution(async (agent, type) => { await skills.attackNearest(agent.bot, type, true); }) @@ -281,7 +281,7 @@ export const actionsList = [ { name: '!activate', description: 'Activate the nearest object of a given type.', - params: {'type': '(string) The type of object to activate.'}, + params: {'type': { type: 'BlockName', description: 'The type of object to activate.' }}, perform: wrapExecution(async (agent, type) => { await skills.activateNearestBlock(agent.bot, type); }) @@ -297,8 +297,8 @@ export const actionsList = [ name: '!setMode', description: 'Set a mode to on or off. A mode is an automatic behavior that constantly checks and responds to the environment.', params: { - 'mode_name': '(string) The name of the mode to enable.', - 'on': '(bool) Whether to enable or disable the mode.' + 'mode_name': { type: 'string', description: 'The name of the mode to enable.' }, + 'on': { type: 'boolean', description: 'Whether to enable or disable the mode.' } }, perform: async function (agent, mode_name, on) { const modes = agent.bot.modes; @@ -314,7 +314,7 @@ export const actionsList = [ name: '!goal', description: 'Set a goal prompt to endlessly work towards with continuous self-prompting.', params: { - 'selfPrompt': '(string) The goal prompt.', + 'selfPrompt': { type: 'string', description: 'The goal prompt.' }, }, perform: async function (agent, prompt) { agent.self_prompter.start(prompt); // don't await, don't return @@ -332,8 +332,8 @@ export const actionsList = [ name: '!npcGoal', description: 'Set a simple goal for an item or building to automatically work towards. Do not use for complex goals.', params: { - 'name': '(string) The name of the goal to set. Can be item or building name. If empty will automatically choose a goal.', - 'quantity': '(number) The quantity of the goal to set. Default is 1.' + 'name': { type: 'string', description: 'The name of the goal to set. Can be item or building name. If empty will automatically choose a goal.' }, + 'quantity': { type: 'int', description: 'The quantity of the goal to set. Default is 1.', domain: [1, Number.MAX_SAFE_INTEGER] } }, perform: async function (agent, name=null, quantity=1) { await agent.npc.setGoal(name, quantity); diff --git a/src/agent/commands/index.js b/src/agent/commands/index.js index 199c5e9..cc2c847 100644 --- a/src/agent/commands/index.js +++ b/src/agent/commands/index.js @@ -1,6 +1,8 @@ +import { getBlockId, getItemId } from "../../utils/mcdata.js"; import { actionsList } from './actions.js'; import { queryList } from './queries.js'; +let suppressNoDomainWarning = false; const commandList = queryList.concat(actionsList); const commandMap = {}; @@ -28,36 +30,131 @@ export function commandExists(commandName) { return commandMap[commandName] !== undefined; } +/** + * Converts a string into a boolean. + * @param {string} input + * @returns {boolean | null} the boolean or `null` if it could not be parsed. + * */ +function parseBoolean(input) { + switch(input.toLowerCase()) { + case 'false': //These are interpreted as flase; + case 'f': + case '0': + case 'off': + return false; + case 'true': //These are interpreted as true; + case 't': + case '1': + case 'on': + return true; + default: + return null; + } +} + +/** + * @param {number} value - the value to check + * @param {number} lowerBound + * @param {number} upperBound + * @param {string} endpointType - The type of the endpoints represented as a two character string. `'[)'` `'()'` + */ +function checkInInterval(number, lowerBound, upperBound, endpointType) { + switch (endpointType) { + case '[)': + return lowerBound <= number && number < upperBound; + case '()': + return lowerBound < number && number < upperBound; + case '(]': + return lowerBound < number && number <= upperBound; + case '[]': + return lowerBound <= number && number <= upperBound; + default: + throw new Error('Unknown endpoint type:', endpointType) + } +} + + + // todo: handle arrays? +/** + * Returns an object containing the command, the command name, and the comand parameters. + * If parsing unsuccessful, returns an error message as a string. + * @param {string} message - A message from a player or language model containing a command. + * @returns {string | Object} + */ function parseCommandMessage(message) { const commandMatch = message.match(commandRegex); - if (commandMatch) { - const commandName = "!"+commandMatch[1]; - if (!commandMatch[2]) - return { commandName, args: [] }; - let args = commandMatch[2].match(argRegex); - if (args) { - for (let i = 0; i < args.length; i++) { - args[i] = args[i].trim(); - } + if (!commandMatch) return `Command is incorrectly formatted`; - for (let i = 0; i < args.length; i++) { - let arg = args[i]; - if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) { - args[i] = arg.substring(1, arg.length-1); - } else if (!isNaN(arg)) { - args[i] = Number(arg); - } else if (arg === 'true' || arg === 'false') { - args[i] = arg === 'true'; - } - } + const commandName = "!"+commandMatch[1]; + + let args; + if (commandMatch[2]) args = commandMatch[2].match(argRegex); + else args = []; + + const command = getCommand(commandName); + if(!command) return `${commandName} is not a command.` + + const params = commandParams(command); + const paramNames = commandParamNames(command); + + if (args.length !== params.length) + return `Command ${command.name} was given ${args.length} args, but requires ${params.length} args.`; + + + for (let i = 0; i < args.length; i++) { + const param = params[i]; + //Remove any extra characters + let arg = args[i].trim(); + if ((arg.startsWith('"') && arg.endsWith('"')) || (arg.startsWith("'") && arg.endsWith("'"))) { + arg = arg.substring(1, arg.length-1); } - else - args = []; + //Convert to the correct type + switch(param.type) { + case 'int': + arg = Number.parseInt(arg); break; + case 'float': + arg = Number.parseFloat(arg); break; + case 'boolean': + arg = parseBoolean(arg); break; + case 'BlockName': + case 'ItemName': + if (arg.endsWith('plank')) + arg += 's'; // catches common mistakes like "oak_plank" instead of "oak_planks" + case 'string': + break; + default: + throw new Error(`Command '${commandName}' parameter '${paramNames[i]}' has an unknown type: ${param.type}`); + } + if(arg === null || Number.isNaN(arg)) + return `Error: Param '${paramNames[i]}' must be of type ${param.type}.` - return { commandName, args }; + if(typeof arg === 'number') { //Check the domain of numbers + const domain = param.domain; + if(domain) { + /** + * Javascript has a built in object for sets but not intervals. + * Currently the interval (lowerbound,upperbound] is represented as an Array: `[lowerbound, upperbound, '(]']` + */ + if (!domain[2]) domain[2] = '[)'; //By default, lower bound is included. Upper is not. + + if(!checkInInterval(arg, ...domain)) { + return `Error: Param '${paramNames[i]}' must be an element of ${domain[2][0]}${domain[0]}, ${domain[1]}${domain[2][1]}.`; + //Alternatively arg could be set to the nearest value in the domain. + } + } else if (!suppressNoDomainWarning) { + console.warn(`Command '${commandName}' parameter '${paramNames[i]}' has no domain set. Expect any value [-Infinity, Infinity].`) + suppressNoDomainWarning = true; //Don't spam console. Only give the warning once. + } + } else if(param.type === 'BlockName') { //Check that there is a block with this name + if(getBlockId(arg) == null) return `Invalid block type: ${arg}.` + } else if(param.type === 'ItemName') { //Check that there is an item with this name + if(getItemId(arg) == null) return `Invalid item type: ${arg}.` + } + args[i] = arg; } - return null; + + return { commandName, args }; } export function truncCommandMessage(message) { @@ -72,15 +169,36 @@ export function isAction(name) { return actionsList.find(action => action.name === name) !== undefined; } -function numParams(command) { +/** + * @param {Object} command + * @returns {Object[]} The command's parameters. + */ +function commandParams(command) { if (!command.params) - return 0; - return Object.keys(command.params).length; + return []; + return Object.values(command.params); +} + +/** + * @param {Object} command + * @returns {string[]} The names of the command's parameters. + */ +function commandParamNames(command) { + if (!command.params) + return []; + return Object.keys(command.params); +} + +function numParams(command) { + return commandParams(command).length; } export async function executeCommand(agent, message) { let parsed = parseCommandMessage(message); - if (parsed) { + if (typeof parsed === 'string') + return parsed; //The command was incorrectly formatted or an invalid input was given. + else { + console.log('parsed command:', parsed); const command = getCommand(parsed.commandName); const is_action = isAction(command.name); let numArgs = 0; @@ -99,11 +217,18 @@ export async function executeCommand(agent, message) { return result; } } - else - return `Command is incorrectly formatted`; } export function getCommandDocs() { + const typeTranslations = { + //This was added to keep the prompt the same as before type checks were implemented. + //If the language model is giving invalid inputs changing this might help. + 'float': 'number', + 'int': 'number', + 'BlockName': 'string', + 'ItemName': 'string', + 'boolean': 'bool' + } let docs = `\n*COMMAND DOCS\n You can use the following commands to perform actions and get information about the world. Use the commands with the syntax: !commandName or !commandName("arg1", 1.2, ...) if the command takes arguments.\n Do not use codeblocks. Only use one command in each response, trailing commands and comments will be ignored.\n`; @@ -112,7 +237,7 @@ export function getCommandDocs() { if (command.params) { docs += 'Params:\n'; for (let param in command.params) { - docs += param + ': ' + command.params[param] + '\n'; + docs += `${param}: (${typeTranslations[command.params[param].type]??command.params[param].type}) ${command.params[param].description}\n`; } } } diff --git a/src/agent/commands/queries.js b/src/agent/commands/queries.js index 322efb5..05938bc 100644 --- a/src/agent/commands/queries.js +++ b/src/agent/commands/queries.js @@ -66,6 +66,23 @@ export const queryList = [ else if (agent.bot.game.gameMode === 'creative') { res += '\n(You have infinite items in creative mode. You do not need to gather resources!!)'; } + + let helmet = bot.inventory.slots[5]; + let chestplate = bot.inventory.slots[6]; + let leggings = bot.inventory.slots[7]; + let boots = bot.inventory.slots[8]; + res += '\nWEARING: '; + if (helmet) + res += `\nHead: ${helmet.name}`; + if (chestplate) + res += `\nTorso: ${chestplate.name}`; + if (leggings) + res += `\nLegs: ${leggings.name}`; + if (boots) + res += `\nFeet: ${boots.name}`; + if (!helmet && !chestplate && !leggings && !boots) + res += 'None'; + return pad(res); } }, diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 0782562..5aa20a8 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -32,7 +32,6 @@ async function equipHighestAttack(bot) { await bot.equip(weapon, 'hand'); } - export async function craftRecipe(bot, itemName, num=1) { /** * Attempt to craft the given item name from a recipe. May craft many items. @@ -44,14 +43,13 @@ export async function craftRecipe(bot, itemName, num=1) { **/ let placedTable = false; - if (itemName.endsWith('plank')) - itemName += 's'; // catches common mistakes like "oak_plank" instead of "oak_planks" - // get recipes that don't require a crafting table let recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, null); let craftingTable = null; const craftingTableRange = 32; - if (!recipes || recipes.length === 0) { + placeTable: if (!recipes || recipes.length === 0) { + recipes = bot.recipesFor(mc.getItemId(itemName), null, 1, true); + if(!recipes || recipes.length === 0) break placeTable; //Don't bother going to the table if we don't have the required resources. // Look for crafting table craftingTable = world.getNearestBlock(bot, 'crafting_table', craftingTableRange); @@ -69,7 +67,7 @@ export async function craftRecipe(bot, itemName, num=1) { } } else { - log(bot, `You either do not have enough resources to craft ${itemName} or it requires a crafting table.`) + log(bot, `Crafting ${itemName} requires a crafting table.`) return false; } } @@ -91,11 +89,22 @@ export async function craftRecipe(bot, itemName, num=1) { const recipe = recipes[0]; console.log('crafting...'); - await bot.craft(recipe, num, craftingTable); - log(bot, `Successfully crafted ${itemName}, you now have ${world.getInventoryCounts(bot)[itemName]} ${itemName}.`); + //Check that the agent has sufficient items to use the recipe `num` times. + const inventory = world.getInventoryCounts(bot); //Items in the agents inventory + const requiredIngredients = mc.ingredientsFromPrismarineRecipe(recipe); //Items required to use the recipe once. + const craftLimit = mc.calculateLimitingResource(inventory, requiredIngredients); + + await bot.craft(recipe, Math.min(craftLimit.num, num), craftingTable); + if(craftLimit.num item.name === itemName); + let item = bot.inventory.slots.find(slot => slot && slot.name === itemName); if (!item) { log(bot, `You do not have any ${itemName} to equip.`); return false; @@ -678,12 +687,13 @@ export async function equip(bot, itemName) { else if (itemName.includes('helmet')) { await bot.equip(item, 'head'); } - else if (itemName.includes('chestplate')) { + else if (itemName.includes('chestplate') || itemName.includes('elytra')) { await bot.equip(item, 'torso'); } else { await bot.equip(item, 'hand'); } + log(bot, `Equipped ${itemName}.`); return true; } diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index c260325..377b1c7 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -13,6 +13,11 @@ const mc_version = settings.minecraft_version; const mcdata = minecraftData(mc_version); const Item = prismarine_items(mc_version); +/** + * @typedef {string} ItemName + * @typedef {string} BlockName +*/ + export const WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak']; export const MATCHING_WOOD_BLOCKS = [ 'log', @@ -241,4 +246,53 @@ export function getBlockTool(blockName) { export function makeItem(name, amount=1) { return new Item(getItemId(name), amount); +} + +/** + * Returns the number of ingredients required to use the recipe once. + * + * @param {Recipe} recipe + * @returns {Object} an object describing the number of each ingredient. + */ +export function ingredientsFromPrismarineRecipe(recipe) { + let requiredIngedients = {}; + if (recipe.inShape) + for (const ingredient of recipe.inShape.flat()) { + if(ingredient.id<0) continue; //prismarine-recipe uses id -1 as an empty crafting slot + const ingredientName = getItemName(ingredient.id); + requiredIngedients[ingredientName] ??=0; + requiredIngedients[ingredientName] += ingredient.count; + } + if (recipe.ingredients) + for (const ingredient of recipe.ingredients) { + if(ingredient.id<0) continue; + const ingredientName = getItemName(ingredient.id); + requiredIngedients[ingredientName] ??=0; + requiredIngedients[ingredientName] -= ingredient.count; + //Yes, the `-=` is intended. + //prismarine-recipe uses positive numbers for the shaped ingredients but negative for unshaped. + //Why this is the case is beyond my understanding. + } + return requiredIngedients; +} + +/** + * Calculates the number of times an action, such as a crafing recipe, can be completed before running out of resources. + * @template T - doesn't have to be an item. This could be any resource. + * @param {Object.} availableItems - The resources available; e.g, `{'cobble_stone': 7, 'stick': 10}` + * @param {Object.} requiredItems - The resources required to complete the action once; e.g, `{'cobble_stone': 3, 'stick': 2}` + * @param {boolean} discrete - Is the action discrete? + * @returns {{num: number, limitingResource: (T | null)}} the number of times the action can be completed and the limmiting resource; e.g `{num: 2, limitingResource: 'cobble_stone'}` + */ +export function calculateLimitingResource(availableItems, requiredItems, discrete=true) { + let limitingResource = null; + let num = Infinity; + for (const itemType in requiredItems) { + if (availableItems[itemType] < requiredItems[itemType] * num) { + limitingResource = itemType; + num = availableItems[itemType] / requiredItems[itemType]; + } + } + if(discrete) num = Math.floor(num); + return {num, limitingResource} } \ No newline at end of file