diff --git a/services/minecraft/src/skills/actions/ensure.ts b/services/minecraft/src/skills/actions/ensure.ts index 88f4c6e66..cae3b663f 100644 --- a/services/minecraft/src/skills/actions/ensure.ts +++ b/services/minecraft/src/skills/actions/ensure.ts @@ -2,7 +2,7 @@ import type { Mineflayer } from '../../libs/mineflayer' import { ActionError } from '../../utils/errors' import { useLogger } from '../../utils/logger' -import { getItemId } from '../../utils/mcdata' +import { McData } from '../../utils/mcdata' import { craftRecipe } from '../crafting' import { moveAway } from '../movement' import { collectBlock } from './collect-block' @@ -158,7 +158,8 @@ export async function ensureSticks(mineflayer: Mineflayer, neededAmount: number) if (planksCount >= planksNeeded) { try { - const sticksId = getItemId('stick') + const mcData = McData.fromBot(mineflayer.bot) + const sticksId = mcData.getItemId('stick') const recipe = mineflayer.bot.recipesFor(sticksId, null, 1, null)[0] if (!recipe) { throw new ActionError('CRAFTING_FAILED', 'No recipe for sticks found') diff --git a/services/minecraft/src/skills/actions/world-interactions.ts b/services/minecraft/src/skills/actions/world-interactions.ts index ad8b93601..e06165941 100644 --- a/services/minecraft/src/skills/actions/world-interactions.ts +++ b/services/minecraft/src/skills/actions/world-interactions.ts @@ -10,7 +10,7 @@ import { Vec3 } from 'vec3' import { ActionError } from '../../utils/errors' import { useLogger } from '../../utils/logger' -import { getNearestBlock, makeItem } from '../../utils/mcdata' +import { McData } from '../../utils/mcdata' import { goToPosition } from '../movement' const logger = useLogger() @@ -44,8 +44,12 @@ export async function placeBlock( .items() .find(item => item.name.includes(blockType)) if (!block && mineflayer.bot.game.gameMode === 'creative') { - // TODO: Rework - await mineflayer.bot.creative.setInventorySlot(36, makeItem(blockType, 1)) // 36 is first hotbar slot + const mcData = McData.fromBot(mineflayer.bot) + const itemId = mcData.getItemId(blockType) + if (itemId) { + const Item = require('prismarine-item')(mineflayer.bot.version) + await mineflayer.bot.creative.setInventorySlot(36, new Item(itemId, 1)) // 36 is first hotbar slot + } block = mineflayer.bot.inventory.items().find(item => item.name.includes(blockType)) } if (!block) { @@ -267,7 +271,10 @@ export async function breakBlockAt( * @throws {ActionError} When the block is not found or cannot be activated. */ export async function activateNearestBlock(mineflayer: Mineflayer, type: string): Promise { - const block = getNearestBlock(mineflayer.bot, type, 16) + const block = mineflayer.bot.findBlock({ + matching: b => b.name === type, + maxDistance: 16, + }) if (!block) { logger.log(`Could not find any ${type} to activate.`) throw new ActionError('TARGET_NOT_FOUND', `Could not find any ${type} to activate`, { blockType: type }) diff --git a/services/minecraft/src/skills/blocks.ts b/services/minecraft/src/skills/blocks.ts index d8af6b7a0..147013d79 100644 --- a/services/minecraft/src/skills/blocks.ts +++ b/services/minecraft/src/skills/blocks.ts @@ -7,7 +7,7 @@ import pathfinderModel from 'mineflayer-pathfinder' import { Vec3 } from 'vec3' -import { getBlockId, makeItem } from '../utils/mcdata' +import { McData } from '../utils/mcdata' import { log } from './base' import { goToPosition } from './movement' import { getNearestBlock, getNearestBlocks, getPosition, shouldPlaceTorch } from './world' @@ -117,7 +117,8 @@ export async function placeBlock( placeOn: BlockFace = 'bottom', dontCheat = false, ): Promise { - if (!getBlockId(blockType)) { + const mcData = McData.fromBot(mineflayer.bot) + if (!mcData.getBlockId(blockType)) { log(mineflayer, `Invalid block type: ${blockType}.`) return false } @@ -218,7 +219,12 @@ async function placeWithoutCheats( let block = mineflayer.bot.inventory.items().find(item => item.name === itemName) if (!block && mineflayer.isCreative) { - await mineflayer.bot.creative.setInventorySlot(36, makeItem(itemName, 1)) + const mcData = McData.fromBot(mineflayer.bot) + const itemId = mcData.getItemId(itemName) + if (itemId) { + const Item = require('prismarine-item')(mineflayer.bot.version) + await mineflayer.bot.creative.setInventorySlot(36, new Item(itemId, 1)) + } block = mineflayer.bot.inventory.items().find(item => item.name === itemName) } diff --git a/services/minecraft/src/skills/crafting.ts b/services/minecraft/src/skills/crafting.ts index 3c152299f..2bc1007f3 100644 --- a/services/minecraft/src/skills/crafting.ts +++ b/services/minecraft/src/skills/crafting.ts @@ -6,7 +6,7 @@ import type { Mineflayer } from '../libs/mineflayer' import { ActionError } from '../utils/errors' import { useLogger } from '../utils/logger' -import { getItemId, getItemName } from '../utils/mcdata' +import { McData } from '../utils/mcdata' import { ensureCraftingTable } from './actions/ensure' import { collectBlock, placeBlock } from './blocks' import { goToNearestBlock, goToPosition, moveAway } from './movement' @@ -24,8 +24,9 @@ export async function craftRecipe( if (itemName.endsWith('plank')) itemName += 's' // Correct common mistakes - const itemId = getItemId(itemName) - if (itemId === null) { + const mcData = McData.fromBot(mineflayer.bot) + const itemId = mcData.getItemId(itemName) + if (!itemId) { throw new ActionError('UNKNOWN', `Invalid item name: ${itemName}`) } @@ -205,14 +206,15 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = const furnace = await mineflayer.bot.openFurnace(furnaceBlock) // Check if the furnace is already smelting something const inputItem = furnace.inputItem() + const mcData = McData.fromBot(mineflayer.bot) if ( inputItem - && inputItem.type !== getItemId(itemName) + && inputItem.type !== mcData.getItemId(itemName) && inputItem.count > 0 ) { if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) - throw new ActionError('CRAFTING_FAILED', `The furnace is currently smelting ${getItemName(inputItem.type)}`) + throw new ActionError('CRAFTING_FAILED', `The furnace is currently smelting ${mcData.getItemName(inputItem.type)}`) } // Check if the bot has enough items to smelt @@ -238,8 +240,8 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = } // Put the items in the furnace - const itemId = getItemId(itemName) - if (itemId === null) { + const itemId = mcData.getItemId(itemName) + if (!itemId) { if (placedFurnace) await collectBlock(mineflayer, 'furnace', 1) throw new ActionError('UNKNOWN', `Invalid item name: ${itemName}`) @@ -293,7 +295,7 @@ export async function smeltItem(mineflayer: Mineflayer, itemName: string, num = } logger.log( - `Successfully smelted ${itemName}, got ${total} ${getItemName( + `Successfully smelted ${itemName}, got ${total} ${mcData.getItemName( smeltedItem?.type || 0, )}.`, ) diff --git a/services/minecraft/src/skills/world.ts b/services/minecraft/src/skills/world.ts index 05f4a18fd..da2580152 100644 --- a/services/minecraft/src/skills/world.ts +++ b/services/minecraft/src/skills/world.ts @@ -7,7 +7,7 @@ import type { Mineflayer } from '../libs/mineflayer' import pf from 'mineflayer-pathfinder' -import * as mc from '../utils/mcdata' +import { McData } from '../utils/mcdata' export function getNearestFreeSpace( mineflayer: Mineflayer, @@ -58,15 +58,16 @@ export function getNearestFreeSpace( } export function getNearestBlocks(mineflayer: Mineflayer, blockTypes: string[] | string | null = null, distance: number = 16, count: number = 10000): Block[] { + const mcData = McData.fromBot(mineflayer.bot) const blockNames = blockTypes === null - ? mc.getAllBlocks(['air']).map(block => block.name) + ? mcData.getAllBlocks(['air']).map(block => block.name) : (Array.isArray(blockTypes) ? blockTypes : [blockTypes]) .map((name) => { - const id = mc.getBlockId(name) + const id = mcData.getBlockId(name) if (id) return name - const closest = mc.getClosestBlockName(name) + const closest = mcData.getClosestBlockName(name) const suggestion = closest ? `; did you mean ${closest}?` : '' throw new Error(`Unknown block type: ${name}${suggestion}`) }) @@ -133,10 +134,13 @@ export function getInventoryCounts(mineflayer: Mineflayer): Record item.name === 'crafting_table') - return mc.getAllItems() - .filter(item => mineflayer.bot.recipesFor(item.id, null, 1, table as Block | null).length > 0) + // Use bot's registry to get items - this ensures IDs match the server version + const registry = mineflayer.bot.registry + return Object.values(registry.items) + .filter(item => mineflayer.bot.recipesFor(item.id, null, 1, table).length > 0) .map(item => item.name) } @@ -207,5 +211,5 @@ export function shouldPlaceTorch(mineflayer: Mineflayer): boolean { export function getBiomeName(mineflayer: Mineflayer): string { const biomeId = mineflayer.bot.world.getBiome(mineflayer.bot.entity.position) - return mc.getAllBiomes()[biomeId].name + return mineflayer.bot.registry.biomes[biomeId]?.name ?? 'unknown' } diff --git a/services/minecraft/src/utils/mcdata.ts b/services/minecraft/src/utils/mcdata.ts index ee50c9e87..46c366144 100644 --- a/services/minecraft/src/utils/mcdata.ts +++ b/services/minecraft/src/utils/mcdata.ts @@ -1,14 +1,129 @@ -import type { Biome, ShapedRecipe, ShapelessRecipe } from 'minecraft-data' +import type { IndexedData, ShapedRecipe, ShapelessRecipe } from 'minecraft-data' import type { Bot } from 'mineflayer' import type { Entity } from 'prismarine-entity' -import minecraftData from 'minecraft-data' -import prismarineItem from 'prismarine-item' +/** + * Registry-aware minecraft data helper. + * Use this class when you have access to a bot to ensure item/block IDs match the server version. + */ +export class McData { + public readonly registry: IndexedData -const GAME_VERSION = '1.20' + constructor(registry: IndexedData) { + this.registry = registry + } -export const gameData = minecraftData(GAME_VERSION) -export const Item = prismarineItem(GAME_VERSION) + static fromBot(bot: Bot): McData { + return new McData(bot.registry) + } + + getItemId(itemName: string): number { + return this.registry.itemsByName[itemName]?.id ?? 0 + } + + getItemName(itemId: number): string { + return this.registry.items[itemId]?.name ?? '' + } + + getBlockId(blockName: string): number { + return this.registry.blocksByName[blockName]?.id ?? 0 + } + + getBlockName(blockId: number): string { + return this.registry.blocks[blockId]?.name ?? '' + } + + getAllItems(ignore: string[] = []): any[] { + return Object.values(this.registry.items).filter(item => !ignore.includes(item.name)) + } + + getAllItemIds(ignore: string[] = []): number[] { + return this.getAllItems(ignore).map(item => item.id) + } + + getAllBlocks(ignore: string[] = []): any[] { + return Object.values(this.registry.blocks).filter(block => !ignore.includes(block.name)) + } + + getAllBlockIds(ignore: string[] = []): number[] { + return this.getAllBlocks(ignore).map(block => block.id) + } + + getClosestBlockName(input: string): string | null { + const names = Object.keys(this.registry.blocksByName) + let best: { name: string | null, distance: number } = { name: null, distance: Number.POSITIVE_INFINITY } + + for (const name of names) { + const distance = levenshteinDistance(input, name) + if (distance < best.distance) { + best = { name, distance } + if (distance === 0) + break + } + } + + return best.name + } + + getBlockTool(blockName: string): string | null { + const block = this.registry.blocksByName[blockName] + if (!block || !block.harvestTools) { + return null + } + const toolIds = Object.keys(block.harvestTools).map(id => Number.parseInt(id)) + const toolName = this.getItemName(toolIds[0]) + return toolName || null + } + + getItemCraftingRecipes(itemName: string): Record[] | null { + const itemId = this.getItemId(itemName) + if (!itemId || !this.registry.recipes[itemId]) { + return null + } + + const recipes: Record[] = [] + for (const r of this.registry.recipes[itemId]) { + const recipe: Record = {} + let ingredients: number[] = [] + + if (isShapelessRecipe(r)) { + ingredients = r.ingredients.map((ing: any) => ing.id) + } + else if (isShapedRecipe(r)) { + ingredients = r.inShape + .flat() + .map((ing: any) => ing?.id) + .filter(Boolean) + } + + for (const ingredientId of ingredients) { + const ingredientName = this.getItemName(ingredientId) + if (ingredientName === null) + continue + if (!recipe[ingredientName]) + recipe[ingredientName] = 0 + recipe[ingredientName]++ + } + + recipes.push(recipe) + } + + return recipes + } + + getItemBlockSources(itemName: string): string[] { + const itemId = this.getItemId(itemName) + const sources: string[] = [] + if (!itemId) + return sources + for (const block of this.getAllBlocks()) { + if (block.drops && block.drops.includes(itemId)) { + sources.push(block.name) + } + } + return sources + } +} export const WOOD_TYPES: string[] = [ 'oak', @@ -78,43 +193,6 @@ export function isHostile(mob: Entity): boolean { ) } -export function getItemId(itemName: string): number { - const item = gameData.itemsByName[itemName] - - return item?.id || 0 -} - -export function getItemName(itemId: number): string { - const item = gameData.items[itemId] - return item.name || '' -} - -export function getBlockId(blockName: string): number { - const block = gameData.blocksByName?.[blockName] - return block?.id || 0 -} - -export function getBlockName(blockId: number): string { - const block = gameData.blocks[blockId] - return block.name || '' -} - -export function getClosestBlockName(input: string): string | null { - const names = Object.keys(gameData.blocksByName ?? {}) - let best: { name: string | null, distance: number } = { name: null, distance: Number.POSITIVE_INFINITY } - - for (const name of names) { - const distance = levenshteinDistance(input, name) - if (distance < best.distance) { - best = { name, distance } - if (distance === 0) - break - } - } - - return best.name -} - function levenshteinDistance(a: string, b: string): number { const matrix: number[][] = Array.from({ length: a.length + 1 }, () => Array.from({ length: b.length + 1 }, () => 0)) @@ -138,88 +216,6 @@ function levenshteinDistance(a: string, b: string): number { return matrix[a.length][b.length] } -export function getAllItems(ignore: string[] = []): any[] { - const items: any[] = [] - for (const itemId in gameData.items) { - const item = gameData.items[itemId] - if (!ignore.includes(item.name)) { - items.push(item) - } - } - return items -} - -export function getAllItemIds(ignore: string[] = []): number[] { - const items = getAllItems(ignore) - const itemIds: number[] = [] - for (const item of items) { - itemIds.push(item.id) - } - return itemIds -} - -export function getAllBlocks(ignore: string[] = []): any[] { - const blocks: any[] = [] - for (const blockId in gameData.blocks) { - const block = gameData.blocks[blockId] - if (!ignore.includes(block.name)) { - blocks.push(block) - } - } - return blocks -} - -export function getAllBlockIds(ignore: string[] = []): number[] { - const blocks = getAllBlocks(ignore) - const blockIds: number[] = [] - for (const block of blocks) { - blockIds.push(block.id) - } - return blockIds -} - -export function getAllBiomes(): Record { - return gameData.biomes -} - -export function getItemCraftingRecipes(itemName: string): any[] | null { - const itemId = getItemId(itemName) - if (!itemId || !gameData.recipes[itemId]) { - return null - } - - const recipes: Record[] = [] - for (const r of gameData.recipes[itemId]) { - const recipe: Record = {} - let ingredients: number[] = [] - - if (isShapelessRecipe(r)) { - // Handle shapeless recipe - ingredients = r.ingredients.map((ing: any) => ing.id) - } - else if (isShapedRecipe(r)) { - // Handle shaped recipe - ingredients = r.inShape - .flat() - .map((ing: any) => ing?.id) - .filter(Boolean) - } - - for (const ingredientId of ingredients) { - const ingredientName = getItemName(ingredientId) - if (ingredientName === null) - continue - if (!recipe[ingredientName]) - recipe[ingredientName] = 0 - recipe[ingredientName]++ - } - - recipes.push(recipe) - } - - return recipes -} - // Type guards function isShapelessRecipe(recipe: any): recipe is ShapelessRecipe { return 'ingredients' in recipe @@ -249,19 +245,6 @@ export function getItemSmeltingIngredient( }[itemName] } -export function getItemBlockSources(itemName: string): string[] { - const itemId = getItemId(itemName) - const sources: string[] = [] - if (!itemId) - return sources - for (const block of getAllBlocks()) { - if (block.drops && block.drops.includes(itemId)) { - sources.push(block.name) - } - } - return sources -} - export function getItemAnimalSource(itemName: string): string | undefined { return { raw_beef: 'cow', @@ -276,23 +259,6 @@ export function getItemAnimalSource(itemName: string): string | undefined { }[itemName] } -export function getBlockTool(blockName: string): string | null { - const block = gameData.blocksByName[blockName] - if (!block || !block.harvestTools) { - return null - } - const toolIds = Object.keys(block.harvestTools).map(id => Number.parseInt(id)) - const toolName = getItemName(toolIds[0]) - return toolName || null // Assuming the first tool is the simplest -} - -export function makeItem(name: string, amount = 1): InstanceType { - const itemId = getItemId(name) - if (itemId === null) - throw new Error(`Item ${name} not found.`) - return new Item(itemId, amount) -} - // Function to get the nearest block of a specific type using Mineflayer export function getNearestBlock( bot: Bot,