fix(minecraft): fix craftables showing wrong results
This commit is contained in:
@@ -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 })
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)}.`,
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user