CollectBlock.ts•16.5 kB
import { Bot } from 'mineflayer'
import { Block } from 'prismarine-block'
import { Movements, goals } from 'mineflayer-pathfinder'
import { TemporarySubscriber } from './TemporarySubscriber'
import { Entity } from 'prismarine-entity'
import { error } from './Util'
import { Vec3 } from 'vec3'
import { ItemFilter } from './Inventory'
import { findFromVein } from './BlockVeins'
import { Collectable, Targets } from './Targets'
import { Item } from 'prismarine-item'
import mcDataLoader from 'minecraft-data'
import { once } from 'events'
import { callbackify } from 'util'
export type Callback = (err?: Error) => void
async function collectAll (
bot: Bot,
options: CollectOptionsFull,
signal?: AbortSignal
): Promise<string> {
let success_count = 0
const resultJson = {} as {[key: string]: number}
let failureList = ''
//console.log('COLLECTBLOCK STARTED')
let isAborted = false;
if (signal) {
signal.addEventListener('abort', () => {
isAborted = true;
});
}
while (!options.targets.empty && !isAborted) {
// await emptyInventoryIfFull(
// bot,
// options.chestLocations,
// options.itemFilter
// );
if (bot.inventory.emptySlotCount() === 0) {
throw error('InventoryFull', "Your inventory is full so you can't collect anything!")
}
const closest = options.targets.getClosest()
if (closest == null) break
options.targets.removeTarget(closest);
switch (closest.constructor.name) {
case 'Block': {
try {
if (success_count >= options.count) {
break
}
await bot.tool.equipForBlock(
closest as Block,
equipToolOptions
)
const moveGoal = new goals.GoalNear(closest.position.x, closest.position.y, closest.position.z, 5)
//console.log(`[COLLECTBLOCK] ${bot.username} starting get near goal`)
await bot.pathfinder.goto(moveGoal)
//console.log(`[COLLECTBLOCK] ${bot.username} starting mineblock`)
await mineBlock(bot, closest as Block, options)
if (resultJson[closest.name as string]) {
resultJson[closest.name as string] += 1
} else {
resultJson[closest.name as string] = 1
}
success_count++
// TODO: options.ignoreNoPath
} catch (err) {
// console.log(err.stack)
// bot.pathfinder.stop()
// bot.waitForTicks(10)
try {
bot.pathfinder.setGoal(null)
} catch (err) {}
if (options.ignoreNoPath) {
// @ts-expect-error
if (err.name === 'Invalid block') {
failureList += `\n[CONSOLE] Block ${closest.name} at ${closest.position} is not valid!`
} // @ts-expect-error
else if (err.name === 'Unsafe block') {
failureList += `\n[CONSOLE] ${closest.name} at ${closest.position} is not safe to break!`
// @ts-expect-error
} else if (err.name === 'NoItem') {
const blockName = closest.name;
const properties = bot.registry.blocksByName[blockName as keyof typeof bot.registry.blocksByName];
if (properties && properties.harvestTools) {
const leastTool = Object.keys(properties.harvestTools)[0];
if (leastTool) {
const item = bot.registry.items[leastTool as unknown as keyof typeof bot.registry.items];
if (item) {
return `You need at least a ${item.name} to mine ${blockName}`;
}
}
}
return `You don't have the right tool to mine ${blockName}`;
} else if (
// @ts-expect-error
err.name === 'NoPath' ||
// @ts-expect-error
err.name === 'Timeout'
) {
if (
bot.entity.position?.distanceTo(
closest.position
) < 3.5
) {
await mineBlock(bot, closest as Block, options)
break
}
failureList += `\n[CONSOLE] No path to ${closest.name} at ${closest.position}!`
// @ts-expect-error
} else if (err.message === 'Digging aborted') {
// console.log(`[CONSOLE] Digging aborted!`);
failureList += 'Digging aborted!'
} else {
// @ts-expect-error
console.log(`[CONSOLE] CollectBlock: Error [${error.name}]: "${err.message}" occurred while attempting to collect a block. Removing target ${[closest.name, closest.position]} and continuing onto next target.`)
// bot.chat(`Error: ${err.message}`);
}
break
}
throw err
}
break
}
case 'Entity': {
// Don't collect any entities that are marked as 'invalid'
if (!(closest as Entity).isValid) break
try {
const tempEvents = new TemporarySubscriber(bot)
const waitForPickup = new Promise<void>(
(resolve, reject) => {
const timeout = setTimeout(() => {
// After 10 seconds, reject the promise
clearTimeout(timeout)
tempEvents.cleanup()
reject(new Error('Failed to pickup item'))
}, 10000)
tempEvents.subscribeTo(
'entityGone',
(entity: Entity) => {
if (entity === closest) {
clearTimeout(timeout)
tempEvents.cleanup()
resolve()
}
}
)
}
)
bot.pathfinder.setGoal(
new goals.GoalFollow(closest as Entity, 0)
)
// await bot.pathfinder.goto(new goals.GoalBlock(closest.position.x, closest.position.y, closest.position.z))
await waitForPickup
} catch (err) {
// @ts-expect-error
// console.log(`[CONSOLE]`,err.stack);
console.log(`[CONSOLE] Error [${err.name}]: "${err.message}" occurred while attempting to pickup item. Setting current pathfinder goal to null, removing ${closest.name} from collect targets and continuing onto next target.`)
try {
bot.pathfinder.setGoal(null)
} catch (err) {}
if (options.ignoreNoPath) {
// @ts-expect-error
if (err.message === 'Failed to pickup item') {
// bot.chat(`Failed to pickup item!`);
}
break
}
throw err
}
break
}
default: {
throw error(
'UnknownType',
`Target ${closest.constructor.name} is not a Block or Entity!`
)
}
}
}
let result = ''
if (Object.entries(resultJson).length === 0) {
// This happens when the block is not valid or safe to break
return `You weren't able to collect anything because of ${failureList}`
}
for (const entry of Object.entries(resultJson)) {
result += 'You finished collecting ' + entry[1] + ' more ' + entry[0] + '\n'
}
// result += failureList;
console.log('[CONSOLE]', result, failureList)
return result
}
const equipToolOptions = {
requireHarvest: true,
getFromChest: false,
maxTools: 2
}
/*
const lookAtBlockOptions = {
reach: 4,
}
*/
async function mineBlock (
bot: Bot,
block: Block,
options: CollectOptionsFull
): Promise<void> {
if (
bot.blockAt(block.position)?.type !== block.type ||
bot.blockAt(block.position)?.type === 0
) {
options.targets.removeTarget(block)
throw error('Invalid block', 'Block is not valid!')
// @ts-expect-error
} else if (!bot.pathfinder.movements.safeToBreak(block)) {
options.targets.removeTarget(block)
throw error('Unsafe block', 'Block is not safe to break!')
}
await bot.tool.equipForBlock(block, equipToolOptions)
if (!block.canHarvest((bot.heldItem != null) ? bot.heldItem.type : bot.heldItem)) {
options.targets.removeTarget(block)
throw error('NoItem', 'Bot does not have a harvestable tool!')
}
const tempEvents = new TemporarySubscriber(bot)
tempEvents.subscribeTo('itemDrop', (entity: Entity) => {
if (
entity.position?.distanceTo(block.position.offset(0.5, 0.5, 0.5)) <=
0.5
) {
options.targets.appendTarget(entity)
}
})
try {
await bot.dig(block)
// Waiting for items to drop
await new Promise<void>((resolve) => {
let remainingTicks = 10
tempEvents.subscribeTo('physicsTick', () => {
remainingTicks--
if (remainingTicks <= 0) {
tempEvents.cleanup()
resolve()
}
})
})
} catch (err) {
console.error(err)
}
finally {
//console.log('CANCELLING DIG!')
tempEvents.cleanup()
}
}
/**
* A set of options to apply when collecting the given targets.
*/
export interface CollectOptions {
/**
* If true, the target(s) will be appended to the existing target list instead of
* starting a new task. Defaults to false.
*/
append?: boolean
/**
* If true, errors will not be thrown when a path to the target block cannot
* be found. The bot will attempt to choose the best available position it
* can find, instead. Errors are still thrown if the bot cannot interact with
* the block from it's final location. Defaults to false.
*/
ignoreNoPath?: boolean
/**
* Gets the list of chest locations to use when storing items after the bot's
* inventory becomes full. If undefined, it defaults to the chest location
* list on the bot.collectBlock plugin.
*/
chestLocations?: Vec3[]
/**
* When transferring items to a chest, this filter is used to determine what
* items are allowed to be moved, and what items aren't allowed to be moved.
* Defaults to the item filter specified on the bot.collectBlock plugin.
*/
itemFilter?: ItemFilter
/**
* The total number of items to collect
*/
count?: number
/**
* The signal to use to abort the collection task.
*/
signal?: AbortSignal
}
/**
* A version of collect options where all values are assigned.
*/
interface CollectOptionsFull {
append: boolean
ignoreNoPath: boolean
chestLocations: Vec3[]
itemFilter: ItemFilter
targets: Targets
count: number
}
/**
* The collect block plugin.
*/
export class CollectBlock {
/**
* The bot.
*/
private readonly bot: Bot
/**
* The list of active targets being collected.
*/
private readonly targets: Targets
/**
* The movements configuration to be sent to the pathfinder plugin.
*/
movements?: Movements
/**
* A list of chest locations which the bot is allowed to empty their inventory into
* if it becomes full while the bot is collecting resources.
*/
chestLocations: Vec3[] = []
/**
* When collecting items, this filter is used to determine what items should be placed
* into a chest if the bot's inventory becomes full. By default, returns true for all
* items except for tools, weapons, and armor.
*
* @param item - The item stack in the bot's inventory to check.
*
* @returns True if the item should be moved into the chest. False otherwise.
*/
itemFilter: ItemFilter = (item: Item) => {
if (item.name.includes('helmet')) return false
if (item.name.includes('chestplate')) return false
if (item.name.includes('leggings')) return false
if (item.name.includes('boots')) return false
if (item.name.includes('shield')) return false
if (item.name.includes('sword')) return false
if (item.name.includes('pickaxe')) return false
if (item.name.includes('axe')) return false
if (item.name.includes('shovel')) return false
if (item.name.includes('hoe')) return false
return true
}
/**
* Creates a new instance of the create block plugin.
*
* @param bot - The bot this plugin is acting on.
*/
constructor (bot: Bot) {
this.bot = bot
this.targets = new Targets(bot)
// @ts-expect-error
this.movements = new Movements(bot, mcDataLoader(bot.version))
this.movements.allowSprinting = true
this.movements.allowParkour = false
this.movements.digCost = 10
this.movements.placeCost = 10
this.movements.canDig = true
}
/**
* If target is a block:
* Causes the bot to break and collect the target block.
*
* If target is an item drop:
* Causes the bot to collect the item drop.
*
* If target is an array containing items or blocks, preforms the correct action for
* all targets in that array sorting dynamically by distance.
*
* @param target - The block(s) or item(s) to collect.
* @param options - The set of options to use when handling these targets
* @param cb - The callback that is called finished.
*/
async collect (
target: Collectable | Collectable[],
options: CollectOptions | Callback = {},
cb?: Callback
): Promise<string> {
if (typeof options === 'function') {
cb = options
options = {}
}
if (options.signal && options.signal.aborted) {
console.log('collect: Signal Aborted');
await this.cancelTask();
this.targets.clear();
return '';
}
// @ts-expect-error
if (cb != null) return callbackify(this.collect)(target, options, cb)
const optionsFull: CollectOptionsFull = {
append: options.append ?? false,
ignoreNoPath: options.ignoreNoPath ?? false,
chestLocations: options.chestLocations ?? this.chestLocations,
itemFilter: options.itemFilter ?? this.itemFilter,
targets: this.targets,
count: options.count ?? Infinity
}
if (this.bot.pathfinder == null) {
throw error(
'UnresolvedDependency',
'The mineflayer-collectblock plugin relies on the mineflayer-pathfinder plugin to run!'
)
}
if (this.bot.tool == null) {
throw error(
'UnresolvedDependency',
'The mineflayer-collectblock plugin relies on the mineflayer-tool plugin to run!'
)
}
if (this.movements != null) {
this.bot.pathfinder.setMovements(this.movements)
}
if (!optionsFull.append) await this.cancelTask()
if (Array.isArray(target)) {
this.targets.appendTargets(target)
} else {
this.targets.appendTarget(target)
}
let res = ''
try {
res = await collectAll(this.bot, optionsFull, options.signal)
this.targets.clear()
// @ts-expect-error
this.bot.emit('collectBlock_finished')
return res
} catch (err) {
this.targets.clear()
// Ignore path stopped error for cancelTask to work properly (imo we shouldn't throw any pathing errors)
// @ts-expect-error
if (err.name !== 'PathStopped') throw err
// @ts-expect-error
this.bot.emit('collectBlock_finished')
throw err
}
}
/**
* Loads all touching blocks of the same type to the given block and returns them as an array.
* This effectively acts as a flood fill algorithm to retrieve blocks in the same ore vein and similar.
*
* @param block - The starting block.
* @param maxBlocks - The maximum number of blocks to look for before stopping.
* @param maxDistance - The max distance from the starting block to look.
* @param floodRadius - The max distance distance from block A to block B to be considered "touching"
*/
findFromVein (
block: Block,
maxBlocks = 100,
maxDistance = 16,
floodRadius = 1
): Block[] {
return findFromVein(
this.bot,
block,
maxBlocks,
maxDistance,
floodRadius
)
}
/**
* Cancels the current collection task, if still active.
*
* @param cb - The callback to use when the task is stopped.
*/
async cancelTask (cb?: Callback): Promise<void> {
//console.log('[COLLECTBLOCK] cancelling!')
if (this.targets.empty) {
if (cb != null) cb()
return await Promise.resolve()
}
this.bot.pathfinder.stop()
if (cb != null) {
// @ts-expect-error
this.bot.once('collectBlock_finished', cb)
}
await once(this.bot, 'collectBlock_finished')
}
}