fix(minecraft): fix craftables showing wrong results

This commit is contained in:
Rin
2026-02-18 11:12:46 +08:00
committed by Neko Ayaka
parent fc24f6e636
commit e9dd2ce0ac
6 changed files with 166 additions and 180 deletions
@@ -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')
@@ -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<void> {
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 })
+9 -3
View File
@@ -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<boolean> {
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)
}
+10 -8
View File
@@ -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,
)}.`,
)
+12 -8
View File
@@ -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<string, numbe
}
export function getCraftableItems(mineflayer: Mineflayer): string[] {
// Only use a placed crafting table Block, not an Item from inventory
// recipesFor expects a Block instance or null (for 2x2 inventory crafting)
const table = getNearestBlock(mineflayer, 'crafting_table')
|| getInventoryStacks(mineflayer).find(item => 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'
}
+121 -155
View File
@@ -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<string, number>[] | null {
const itemId = this.getItemId(itemName)
if (!itemId || !this.registry.recipes[itemId]) {
return null
}
const recipes: Record<string, number>[] = []
for (const r of this.registry.recipes[itemId]) {
const recipe: Record<string, number> = {}
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<number, Biome> {
return gameData.biomes
}
export function getItemCraftingRecipes(itemName: string): any[] | null {
const itemId = getItemId(itemName)
if (!itemId || !gameData.recipes[itemId]) {
return null
}
const recipes: Record<string, number>[] = []
for (const r of gameData.recipes[itemId]) {
const recipe: Record<string, number> = {}
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<typeof Item> {
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,