From 7a3a5b3f222b8c7990a07f30c27e0d2214a20425 Mon Sep 17 00:00:00 2001 From: Nimi Date: Tue, 12 Nov 2024 11:20:30 -0600 Subject: [PATCH 1/2] LAN search, better error handling for joining servers --- main.js | 50 +++++++++++++++++++++++++- src/utils/mcserver.js | 84 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 src/utils/mcserver.js diff --git a/main.js b/main.js index 5e8cc97..0c93d2e 100644 --- a/main.js +++ b/main.js @@ -2,6 +2,8 @@ import { AgentProcess } from './src/process/agent-process.js'; import settings from './settings.js'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; +import { serverInfo, findServers } from './src/utils/mcserver.js'; +import mc from 'minecraft-protocol'; function parseArguments() { return yargs(hideBin(process.argv)) @@ -18,12 +20,58 @@ function getProfiles(args) { return args.profiles || settings.profiles; } -function main() { +async function getServer() { + let server = null; + let serverString = ""; + let serverVersion = ""; + + // Search for server + if (settings.port == -1) + { + console.log("No port provided. Searching for LAN server..."); + + await findServers(settings.host, true).then((servers) => { + if (servers.length > 0) + server = servers[0]; + }); + + if (server == null) + throw new Error(`No server found on LAN.`); + } + else + server = await serverInfo(settings.host, settings.port); + + // Server not found + if (server == null) + throw new Error(`Server not found. (Host: ${settings.host}, Port: ${settings.port}) Check the host and port in settings.js.`); + + serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`; + + if (settings.minecraft_version === "auto") + serverVersion = server.version; + else + serverVersion = settings.minecraft_version; + + // Server version unsupported / mismatch + if (mc.supportedVersions.indexOf(serverVersion) === -1) + throw new Error(`A server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`); + else if (settings.minecraft_version !== "auto" && server.version !== settings.minecraft_version) + throw new Error(`A server was found ${serverString}, but version is incorrect. Expected ${settings.minecraft_version}, but found ${server.version}.`); + else + console.log(`Server found. ${serverString}`); + + return server; +} + +async function main() { const args = parseArguments(); const profiles = getProfiles(args); console.log(profiles); const { load_memory, init_message } = settings; + // Get server + const server = await getServer(); + for (let i=0; i} - A Promise that resolves to an array of server info objects. + */ +export async function serverInfo(ip, port, timeout = 100) { + return new Promise((resolve) => { + + setTimeout(() => { + resolve(null); // Resolve as null if no response within timeout + }, timeout); + + mc.ping({ + host: ip, + port + }, (err, response) => { + if (err) return resolve(null); + + const serverInfo = { + host: ip, + port, + name: response.description.text || 'No description provided.', + ping: response.latency, + version: response.version.name + }; + + resolve(serverInfo); + }); + }); +} + +/** + * Scans the IP address for Minecraft LAN servers and collects their info. + * @param {string} ip - The IP address to scan. + * @param {boolean} earlyExit - Whether to exit early after finding a server. + * @param {number} timeout - The connection timeout in ms. + * @returns {Promise} - A Promise that resolves to an array of server info objects. + */ +export async function findServers(ip, earlyExit = false, timeout = 100) { + const servers = []; + const startPort = 49000; + const endPort = 65000; + + const checkPort = (port) => { + return new Promise((resolve) => { + const socket = net.createConnection({ host: ip, port, timeout }, () => { + socket.end(); + resolve(port); // Port is open + }); + + socket.on('error', () => resolve(null)); // Port is closed + socket.on('timeout', () => { + socket.destroy(); + resolve(null); + }); + }); + }; + + // Surpress console output + const originalConsoleLog = console.log; + console.log = () => { }; + + for (let port = startPort; port <= endPort; port++) { + const openPort = await checkPort(port); + if (openPort) { + const server = await serverInfo(ip, port); + if (server) { + servers.push(server); + + if (earlyExit) break; + } + } + } + + // Restore console output + console.log = originalConsoleLog; + + return servers; +} \ No newline at end of file From 418a2470de1bab9e2338e99513db22ef0dbb0f0f Mon Sep 17 00:00:00 2001 From: Nimi Date: Thu, 14 Nov 2024 15:00:53 -0600 Subject: [PATCH 2/2] Server/Minecraft data now agent-level Also refactored mcdata as a class. --- main.js | 50 +-- src/agent/agent.js | 13 +- src/agent/commands/index.js | 6 +- src/agent/commands/queries.js | 2 +- src/agent/library/skills.js | 2 +- src/agent/library/world.js | 2 +- src/agent/modes.js | 2 +- src/agent/npc/build_goal.js | 2 +- src/agent/npc/controller.js | 2 +- src/agent/npc/item_goal.js | 2 +- src/agent/npc/utils.js | 3 +- src/process/agent-process.js | 9 +- src/process/init-agent.js | 14 +- src/utils/mcdata.js | 602 +++++++++++++++++----------------- src/utils/mcserver.js | 44 +++ 15 files changed, 396 insertions(+), 359 deletions(-) diff --git a/main.js b/main.js index 0c93d2e..666ab4b 100644 --- a/main.js +++ b/main.js @@ -2,8 +2,7 @@ import { AgentProcess } from './src/process/agent-process.js'; import settings from './settings.js'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { serverInfo, findServers } from './src/utils/mcserver.js'; -import mc from 'minecraft-protocol'; +import { getServer } from './src/utils/mcserver.js'; function parseArguments() { return yargs(hideBin(process.argv)) @@ -20,49 +19,6 @@ function getProfiles(args) { return args.profiles || settings.profiles; } -async function getServer() { - let server = null; - let serverString = ""; - let serverVersion = ""; - - // Search for server - if (settings.port == -1) - { - console.log("No port provided. Searching for LAN server..."); - - await findServers(settings.host, true).then((servers) => { - if (servers.length > 0) - server = servers[0]; - }); - - if (server == null) - throw new Error(`No server found on LAN.`); - } - else - server = await serverInfo(settings.host, settings.port); - - // Server not found - if (server == null) - throw new Error(`Server not found. (Host: ${settings.host}, Port: ${settings.port}) Check the host and port in settings.js.`); - - serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`; - - if (settings.minecraft_version === "auto") - serverVersion = server.version; - else - serverVersion = settings.minecraft_version; - - // Server version unsupported / mismatch - if (mc.supportedVersions.indexOf(serverVersion) === -1) - throw new Error(`A server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`); - else if (settings.minecraft_version !== "auto" && server.version !== settings.minecraft_version) - throw new Error(`A server was found ${serverString}, but version is incorrect. Expected ${settings.minecraft_version}, but found ${server.version}.`); - else - console.log(`Server found. ${serverString}`); - - return server; -} - async function main() { const args = parseArguments(); const profiles = getProfiles(args); @@ -71,10 +27,10 @@ async function main() { // Get server const server = await getServer(); - + for (let i=0; i { diff --git a/src/agent/library/skills.js b/src/agent/library/skills.js index 2b8d233..6a85303 100644 --- a/src/agent/library/skills.js +++ b/src/agent/library/skills.js @@ -1,4 +1,4 @@ -import * as mc from "../../utils/mcdata.js"; +import { mc } from '../../utils/mcdata.js'; import * as world from "./world.js"; import pf from 'mineflayer-pathfinder'; import Vec3 from 'vec3'; diff --git a/src/agent/library/world.js b/src/agent/library/world.js index 01d54c3..673d799 100644 --- a/src/agent/library/world.js +++ b/src/agent/library/world.js @@ -1,5 +1,5 @@ import pf from 'mineflayer-pathfinder'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; export function getNearestFreeSpace(bot, size=1, distance=8) { diff --git a/src/agent/modes.js b/src/agent/modes.js index 2c0b3e0..a33a090 100644 --- a/src/agent/modes.js +++ b/src/agent/modes.js @@ -1,6 +1,6 @@ import * as skills from './library/skills.js'; import * as world from './library/world.js'; -import * as mc from '../utils/mcdata.js'; +import { mc } from '../utils/mcdata.js'; import settings from '../../settings.js' import { handleTranslation } from '../utils/translator.js'; diff --git a/src/agent/npc/build_goal.js b/src/agent/npc/build_goal.js index ebca78f..c529727 100644 --- a/src/agent/npc/build_goal.js +++ b/src/agent/npc/build_goal.js @@ -1,7 +1,7 @@ import { Vec3 } from 'vec3'; import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; import { blockSatisfied, getTypeOfGeneric, rotateXZ } from './utils.js'; diff --git a/src/agent/npc/controller.js b/src/agent/npc/controller.js index 9af3f3e..0dbca62 100644 --- a/src/agent/npc/controller.js +++ b/src/agent/npc/controller.js @@ -5,7 +5,7 @@ import { BuildGoal } from './build_goal.js'; import { itemSatisfied, rotateXZ } from './utils.js'; import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; export class NPCContoller { diff --git a/src/agent/npc/item_goal.js b/src/agent/npc/item_goal.js index 40589ba..7283de8 100644 --- a/src/agent/npc/item_goal.js +++ b/src/agent/npc/item_goal.js @@ -1,6 +1,6 @@ import * as skills from '../library/skills.js'; import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; +import { mc } from '../../utils/mcdata.js'; import { itemSatisfied } from './utils.js'; diff --git a/src/agent/npc/utils.js b/src/agent/npc/utils.js index 53d8486..2b3b825 100644 --- a/src/agent/npc/utils.js +++ b/src/agent/npc/utils.js @@ -1,6 +1,5 @@ import * as world from '../library/world.js'; -import * as mc from '../../utils/mcdata.js'; - +import { mc } from '../../utils/mcdata.js'; export function getTypeOfGeneric(bot, block_name) { // Get type of wooden block diff --git a/src/process/agent-process.js b/src/process/agent-process.js index 5135de1..a5f08d1 100644 --- a/src/process/agent-process.js +++ b/src/process/agent-process.js @@ -3,7 +3,7 @@ import { spawn } from 'child_process'; export class AgentProcess { static runningCount = 0; - start(profile, load_memory=false, init_message=null, count_id=0) { + start(profile, load_memory=false, init_message=null, server_host=null, server_port=0, server_version=null, count_id=0) { let args = ['src/process/init-agent.js', this.name]; args.push('-p', profile); args.push('-c', count_id); @@ -12,6 +12,11 @@ export class AgentProcess { if (init_message) args.push('-m', init_message); + // Pass server/version info to agent + args.push('--server_host', server_host); + args.push('--server_port', server_port); + args.push('--server_version', server_version); + const agentProcess = spawn('node', args, { stdio: 'inherit', stderr: 'inherit', @@ -34,7 +39,7 @@ export class AgentProcess { return; } console.log('Restarting agent...'); - this.start(profile, true, 'Agent process restarted.', count_id); + this.start(profile, true, 'Agent process restarted.', server_host, server_port, server_version, count_id); last_restart = Date.now(); } }); diff --git a/src/process/init-agent.js b/src/process/init-agent.js index 829f437..bc554f5 100644 --- a/src/process/init-agent.js +++ b/src/process/init-agent.js @@ -38,6 +38,18 @@ const argv = yargs(args) type: 'number', default: 0, description: 'identifying count for multi-agent scenarios', + }) + .option('server_host', { + type: 'string', + description: 'minecraft server host', + }) + .option('server_port', { + type: 'number', + description: 'minecraft server port', + }) + .option('server_version', { + type: 'string', + description: 'minecraft version' }).argv; // Wrap agent start in async IIFE with proper error handling @@ -45,7 +57,7 @@ const argv = yargs(args) try { console.log('Starting agent with profile:', argv.profile); const agent = new Agent(); - await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.count_id); + await agent.start(argv.profile, argv.load_memory, argv.init_message, argv.server_host, argv.server_port, argv.server_version, argv.count_id); } catch (error) { console.error('Failed to start agent process:', { message: error.message || 'No error message', diff --git a/src/utils/mcdata.js b/src/utils/mcdata.js index 04a535a..2a8d7fa 100644 --- a/src/utils/mcdata.js +++ b/src/utils/mcdata.js @@ -9,317 +9,331 @@ import { plugin as autoEat } from 'mineflayer-auto-eat'; import plugin from 'mineflayer-armor-manager'; const armorManager = plugin; -const mc_version = settings.minecraft_version; -const mcdata = minecraftData(mc_version); -const Item = prismarine_items(mc_version); +class MinecraftData { + constructor() { + this.mcdata = null; + this.Item = null; -/** - * @typedef {string} ItemName - * @typedef {string} BlockName -*/ - -export const WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak']; -export const MATCHING_WOOD_BLOCKS = [ - 'log', - 'planks', - 'sign', - 'boat', - 'fence_gate', - 'door', - 'fence', - 'slab', - 'stairs', - 'button', - 'pressure_plate', - 'trapdoor' -] -export const WOOL_COLORS = [ - 'white', - 'orange', - 'magenta', - 'light_blue', - 'yellow', - 'lime', - 'pink', - 'gray', - 'light_gray', - 'cyan', - 'purple', - 'blue', - 'brown', - 'green', - 'red', - 'black' -] - - -export function initBot(username) { - let bot = createBot({ - username: username, - - host: settings.host, - port: settings.port, - auth: settings.auth, - - version: mc_version, - }); - bot.loadPlugin(pathfinder); - bot.loadPlugin(pvp); - bot.loadPlugin(collectblock); - bot.loadPlugin(autoEat); - bot.loadPlugin(armorManager); // auto equip armor - - return bot; -} - -export function isHuntable(mob) { - if (!mob || !mob.name) return false; - const animals = ['chicken', 'cow', 'llama', 'mooshroom', 'pig', 'rabbit', 'sheep']; - 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'; -} - -export function getItemId(itemName) { - let item = mcdata.itemsByName[itemName]; - if (item) { - return item.id; + this.server_version = null; + this.server_port = null; + this.server_host = null; } - return null; -} -export function getItemName(itemId) { - let item = mcdata.items[itemId] - if (item) { - return item.name; + init(host, port, version) { + this.server_version = version; + this.server_port = port; + this.server_host = host; + + this.mcdata = minecraftData(this.server_version); + this.Item = prismarine_items(this.server_version); } - return null; -} -export function getBlockId(blockName) { - let block = mcdata.blocksByName[blockName]; - if (block) { - return block.id; + initBot(username) { + let bot = createBot({ + username: username, + host: this.server_host, + port: this.server_port, + auth: settings.auth, + version: this.server_version, + }); + bot.loadPlugin(pathfinder); + bot.loadPlugin(pvp); + bot.loadPlugin(collectblock); + bot.loadPlugin(autoEat); + bot.loadPlugin(armorManager); // auto equip armor + return bot; } - return null; -} -export function getBlockName(blockId) { - let block = mcdata.blocks[blockId] - if (block) { - return block.name; + isHuntable(mob) { + if (!mob || !mob.name) return false; + const animals = ['chicken', 'cow', 'llama', 'mooshroom', 'pig', 'rabbit', 'sheep']; + return animals.includes(mob.name.toLowerCase()) && !mob.metadata[16]; // metadata 16 is not baby } - return null; -} -export function getAllItems(ignore) { - if (!ignore) { - ignore = []; + isHostile(mob) { + if (!mob || !mob.name) return false; + return (mob.type === 'mob' || mob.type === 'hostile') && mob.name !== 'iron_golem' && mob.name !== 'snow_golem'; } - let items = [] - for (const itemId in mcdata.items) { - const item = mcdata.items[itemId]; - if (!ignore.includes(item.name)) { - items.push(item); + + getItemId(itemName) { + let item = this.mcdata.itemsByName[itemName]; + if (item) { + return item.id; } - } - return items; -} - -export function getAllItemIds(ignore) { - const items = getAllItems(ignore); - let itemIds = []; - for (const item of items) { - itemIds.push(item.id); - } - return itemIds; -} - -export function getAllBlocks(ignore) { - if (!ignore) { - ignore = []; - } - let blocks = [] - for (const blockId in mcdata.blocks) { - const block = mcdata.blocks[blockId]; - if (!ignore.includes(block.name)) { - blocks.push(block); - } - } - return blocks; -} - -export function getAllBlockIds(ignore) { - const blocks = getAllBlocks(ignore); - let blockIds = []; - for (const block of blocks) { - blockIds.push(block.id); - } - return blockIds; -} - -export function getAllBiomes() { - return mcdata.biomes; -} - -export function getItemCraftingRecipes(itemName) { - let itemId = getItemId(itemName); - if (!mcdata.recipes[itemId]) { return null; } - let recipes = []; - for (let r of mcdata.recipes[itemId]) { - let recipe = {}; - let ingredients = []; - if (r.ingredients) { - ingredients = r.ingredients; - } else if (r.inShape) { - ingredients = r.inShape.flat(); + getItemName(itemId) { + let item = this.mcdata.items[itemId] + if (item) { + return item.name; } - for (let ingredient of ingredients) { - let ingredientName = getItemName(ingredient); - if (ingredientName === null) continue; - if (!recipe[ingredientName]) - recipe[ingredientName] = 0; - recipe[ingredientName]++; - } - recipes.push(recipe); - } - - return recipes; -} - -export function isSmeltable(itemName) { - const misc_smeltables = ['beef', 'chicken', 'cod', 'mutton', 'porkchop', 'rabbit', 'salmon', 'tropical_fish', 'potato', 'kelp', 'sand', 'cobblestone', 'clay_ball']; - return itemName.includes('raw') || itemName.includes('log') || misc_smeltables.includes(itemName); -} - -export function getSmeltingFuel(bot) { - let fuel = bot.inventory.items().find(i => i.name === 'coal' || i.name === 'charcoal') - if (fuel) - return fuel; - fuel = bot.inventory.items().find(i => i.name.includes('log') || i.name.includes('planks')) - if (fuel) - return fuel; - return bot.inventory.items().find(i => i.name === 'coal_block' || i.name === 'lava_bucket'); -} - -export function getFuelSmeltOutput(fuelName) { - if (fuelName === 'coal' || fuelName === 'charcoal') - return 8; - if (fuelName.includes('log') || fuelName.includes('planks')) - return 1.5 - if (fuelName === 'coal_block') - return 80; - if (fuelName === 'lava_bucket') - return 100; - return 0; -} - -export function getItemSmeltingIngredient(itemName) { - return { - baked_potato: 'potato', - steak: 'raw_beef', - cooked_chicken: 'raw_chicken', - cooked_cod: 'raw_cod', - cooked_mutton: 'raw_mutton', - cooked_porkchop: 'raw_porkchop', - cooked_rabbit: 'raw_rabbit', - cooked_salmon: 'raw_salmon', - dried_kelp: 'kelp', - iron_ingot: 'raw_iron', - gold_ingot: 'raw_gold', - copper_ingot: 'raw_copper', - glass: 'sand' - }[itemName]; -} - -export function getItemBlockSources(itemName) { - let itemId = getItemId(itemName); - let sources = []; - for (let block of getAllBlocks()) { - if (block.drops.includes(itemId)) { - sources.push(block.name); - } - } - return sources; -} - -export function getItemAnimalSource(itemName) { - return { - raw_beef: 'cow', - raw_chicken: 'chicken', - raw_cod: 'cod', - raw_mutton: 'sheep', - raw_porkchop: 'pig', - raw_rabbit: 'rabbit', - raw_salmon: 'salmon', - leather: 'cow', - wool: 'sheep' - }[itemName]; -} - -export function getBlockTool(blockName) { - let block = mcdata.blocksByName[blockName]; - if (!block || !block.harvestTools) { return null; } - return getItemName(Object.keys(block.harvestTools)[0]); // Double check first tool is always simplest -} -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]; + getBlockId(blockName) { + let block = this.mcdata.blocksByName[blockName]; + if (block) { + return block.id; } + return null; } - if(discrete) num = Math.floor(num); - return {num, limitingResource} -} \ No newline at end of file + + getBlockName(blockId) { + let block = this.mcdata.blocks[blockId] + if (block) { + return block.name; + } + return null; + } + + getAllItems(ignore) { + if (!ignore) { + ignore = []; + } + let items = [] + for (const itemId in this.mcdata.items) { + const item = this.mcdata.items[itemId]; + if (!ignore.includes(item.name)) { + items.push(item); + } + } + return items; + } + + getAllItemIds(ignore) { + const items = this.getAllItems(ignore); + let itemIds = []; + for (const item of items) { + itemIds.push(item.id); + } + return itemIds; + } + + getAllBlocks(ignore) { + if (!ignore) { + ignore = []; + } + let blocks = [] + for (const blockId in this.mcdata.blocks) { + const block = this.mcdata.blocks[blockId]; + if (!ignore.includes(block.name)) { + blocks.push(block); + } + } + return blocks; + } + + getAllBlockIds(ignore) { + const blocks = this.getAllBlocks(ignore); + let blockIds = []; + for (const block of blocks) { + blockIds.push(block.id); + } + return blockIds; + } + + getAllBiomes() { + return this.mcdata.biomes; + } + + getItemCraftingRecipes(itemName) { + let itemId = this.getItemId(itemName); + if (!this.mcdata.recipes[itemId]) { + return null; + } + + let recipes = []; + for (let r of this.mcdata.recipes[itemId]) { + let recipe = {}; + let ingredients = []; + if (r.ingredients) { + ingredients = r.ingredients; + } else if (r.inShape) { + ingredients = r.inShape.flat(); + } + for (let ingredient of ingredients) { + let ingredientName = this.getItemName(ingredient); + if (ingredientName === null) continue; + if (!recipe[ingredientName]) + recipe[ingredientName] = 0; + recipe[ingredientName]++; + } + recipes.push(recipe); + } + + return recipes; + } + + isSmeltable(itemName) { + const misc_smeltables = ['beef', 'chicken', 'cod', 'mutton', 'porkchop', 'rabbit', 'salmon', 'tropical_fish', 'potato', 'kelp', 'sand', 'cobblestone', 'clay_ball']; + return itemName.includes('raw') || itemName.includes('log') || misc_smeltables.includes(itemName); + } + + getSmeltingFuel(bot) { + let fuel = bot.inventory.items().find(i => i.name === 'coal' || i.name === 'charcoal') + if (fuel) + return fuel; + fuel = bot.inventory.items().find(i => i.name.includes('log') || i.name.includes('planks')) + if (fuel) + return fuel; + return bot.inventory.items().find(i => i.name === 'coal_block' || i.name === 'lava_bucket'); + } + + getFuelSmeltOutput(fuelName) { + if (fuelName === 'coal' || fuelName === 'charcoal') + return 8; + if (fuelName.includes('log') || fuelName.includes('planks')) + return 1.5 + if (fuelName === 'coal_block') + return 80; + if (fuelName === 'lava_bucket') + return 100; + return 0; + } + + getItemSmeltingIngredient(itemName) { + return { + baked_potato: 'potato', + steak: 'raw_beef', + cooked_chicken: 'raw_chicken', + cooked_cod: 'raw_cod', + cooked_mutton: 'raw_mutton', + cooked_porkchop: 'raw_porkchop', + cooked_rabbit: 'raw_rabbit', + cooked_salmon: 'raw_salmon', + dried_kelp: 'kelp', + iron_ingot: 'raw_iron', + gold_ingot: 'raw_gold', + copper_ingot: 'raw_copper', + glass: 'sand' + }[itemName]; + } + + getItemBlockSources(itemName) { + let itemId = this.getItemId(itemName); + let sources = []; + for (let block of this.getAllBlocks()) { + if (block.drops.includes(itemId)) { + sources.push(block.name); + } + } + return sources; + } + + getItemAnimalSource(itemName) { + return { + raw_beef: 'cow', + raw_chicken: 'chicken', + raw_cod: 'cod', + raw_mutton: 'sheep', + raw_porkchop: 'pig', + raw_rabbit: 'rabbit', + raw_salmon: 'salmon', + leather: 'cow', + wool: 'sheep' + }[itemName]; + } + + getBlockTool(blockName) { + let block = this.mcdata.blocksByName[blockName]; + if (!block || !block.harvestTools) { + return null; + } + return this.getItemName(Object.keys(block.harvestTools)[0]); // Double check first tool is always simplest + } + + makeItem(name, amount=1) { + return new this.Item(this.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. + */ + 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 = this.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 = this.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'}` + */ + 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} + } + + /** + * @typedef {string} ItemName + * @typedef {string} BlockName + */ + + WOOD_TYPES = ['oak', 'spruce', 'birch', 'jungle', 'acacia', 'dark_oak']; + MATCHING_WOOD_BLOCKS = [ + 'log', + 'planks', + 'sign', + 'boat', + 'fence_gate', + 'door', + 'fence', + 'slab', + 'stairs', + 'button', + 'pressure_plate', + 'trapdoor' + ] + WOOL_COLORS = [ + 'white', + 'orange', + 'magenta', + 'light_blue', + 'yellow', + 'lime', + 'pink', + 'gray', + 'light_gray', + 'cyan', + 'purple', + 'blue', + 'brown', + 'green', + 'red', + 'black' + ] +} + +export const mc = new MinecraftData(); \ No newline at end of file diff --git a/src/utils/mcserver.js b/src/utils/mcserver.js index 3e9d29e..e5a5f05 100644 --- a/src/utils/mcserver.js +++ b/src/utils/mcserver.js @@ -1,3 +1,4 @@ +import settings from '../../settings.js'; import net from 'net'; import mc from 'minecraft-protocol'; @@ -81,4 +82,47 @@ export async function findServers(ip, earlyExit = false, timeout = 100) { console.log = originalConsoleLog; return servers; +} + +export async function getServer() { + let server = null; + let serverString = ""; + let serverVersion = ""; + + // Search for server + if (settings.port == -1) + { + console.log(`No port provided. Searching for LAN server on host ${settings.host}...`); + + await findServers(settings.host, true).then((servers) => { + if (servers.length > 0) + server = servers[0]; + }); + + if (server == null) + throw new Error(`No server found on LAN.`); + } + else + server = await serverInfo(settings.host, settings.port); + + // Server not found + if (server == null) + throw new Error(`Server not found. (Host: ${settings.host}, Port: ${settings.port}) Check the host and port in settings.js.`); + + serverString = `(Host: ${server.host}, Port: ${server.port}, Version: ${server.version})`; + + if (settings.minecraft_version === "auto") + serverVersion = server.version; + else + serverVersion = settings.minecraft_version; + + // Server version unsupported / mismatch + if (mc.supportedVersions.indexOf(serverVersion) === -1) + throw new Error(`A server was found ${serverString}, but version is unsupported. Supported versions are: ${mc.supportedVersions.join(", ")}.`); + else if (settings.minecraft_version !== "auto" && server.version !== settings.minecraft_version) + throw new Error(`A server was found ${serverString}, but version is incorrect. Expected ${settings.minecraft_version}, but found ${server.version}.`); + else + console.log(`Server found. ${serverString}`); + + return server; } \ No newline at end of file