|
|
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 { emptyInventoryIfFull, 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 |
|
|
): Promise<void> { |
|
|
let success_count = 0; |
|
|
while (!options.targets.empty) { |
|
|
await emptyInventoryIfFull( |
|
|
bot, |
|
|
options.chestLocations, |
|
|
options.itemFilter |
|
|
); |
|
|
const closest = options.targets.getClosest(); |
|
|
if (closest == null) break; |
|
|
switch (closest.constructor.name) { |
|
|
case "Block": { |
|
|
try { |
|
|
if (success_count >= options.count) { |
|
|
break; |
|
|
} |
|
|
await bot.tool.equipForBlock( |
|
|
closest as Block, |
|
|
equipToolOptions |
|
|
); |
|
|
const goal = new goals.GoalLookAtBlock( |
|
|
closest.position, |
|
|
bot.world |
|
|
); |
|
|
await bot.pathfinder.goto(goal); |
|
|
await mineBlock(bot, closest as Block, options); |
|
|
success_count++; |
|
|
|
|
|
} catch (err) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try { |
|
|
bot.pathfinder.setGoal(null); |
|
|
} catch (err) {} |
|
|
if (options.ignoreNoPath) { |
|
|
|
|
|
if (err.name === "Invalid block") { |
|
|
console.log( |
|
|
`Block ${closest.name} at ${closest.position} is not valid! Skip it!` |
|
|
); |
|
|
} |
|
|
else if (err.name === "Unsafe block") { |
|
|
console.log( |
|
|
`${closest.name} at ${closest.position} is not safe to break! Skip it!` |
|
|
); |
|
|
|
|
|
} else if (err.name === "NoItem") { |
|
|
const properties = |
|
|
bot.registry.blocksByName[closest.name]; |
|
|
const leastTool = Object.keys( |
|
|
properties.harvestTools |
|
|
)[0]; |
|
|
const item = bot.registry.items[leastTool]; |
|
|
bot.chat( |
|
|
`I need at least a ${item.name} to mine ${closest.name}! Skip it!` |
|
|
); |
|
|
return; |
|
|
} else if ( |
|
|
|
|
|
err.name === "NoPath" || |
|
|
|
|
|
err.name === "Timeout" |
|
|
) { |
|
|
if ( |
|
|
bot.entity.position.distanceTo( |
|
|
closest.position |
|
|
) < 0.5 |
|
|
) { |
|
|
await mineBlock(bot, closest as Block, options); |
|
|
break; |
|
|
} |
|
|
console.log( |
|
|
`No path to ${closest.name} at ${closest.position}! Skip it!` |
|
|
); |
|
|
|
|
|
} else if (err.message === "Digging aborted") { |
|
|
console.log(`Digging aborted! Skip it!`); |
|
|
} else { |
|
|
|
|
|
bot.chat(`Error: ${err.message}`); |
|
|
} |
|
|
break; |
|
|
} |
|
|
throw err; |
|
|
} |
|
|
break; |
|
|
} |
|
|
case "Entity": { |
|
|
|
|
|
if (!(closest as Entity).isValid) break; |
|
|
try { |
|
|
const tempEvents = new TemporarySubscriber(bot); |
|
|
const waitForPickup = new Promise<void>( |
|
|
(resolve, reject) => { |
|
|
const timeout = setTimeout(() => { |
|
|
|
|
|
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 waitForPickup; |
|
|
} catch (err) { |
|
|
|
|
|
console.log(err.stack); |
|
|
try { |
|
|
bot.pathfinder.setGoal(null); |
|
|
} catch (err) {} |
|
|
if (options.ignoreNoPath) { |
|
|
|
|
|
if (err.message === "Failed to pickup item") { |
|
|
bot.chat(`Failed to pickup item! Skip it!`); |
|
|
} |
|
|
break; |
|
|
} |
|
|
throw err; |
|
|
} |
|
|
break; |
|
|
} |
|
|
default: { |
|
|
throw error( |
|
|
"UnknownType", |
|
|
`Target ${closest.constructor.name} is not a Block or Entity!` |
|
|
); |
|
|
} |
|
|
} |
|
|
options.targets.removeTarget(closest); |
|
|
} |
|
|
bot.chat(`Collect finish!`); |
|
|
} |
|
|
|
|
|
const equipToolOptions = { |
|
|
requireHarvest: true, |
|
|
getFromChest: false, |
|
|
maxTools: 2, |
|
|
}; |
|
|
|
|
|
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!"); |
|
|
|
|
|
} 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 ? 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); |
|
|
|
|
|
await new Promise<void>((resolve) => { |
|
|
let remainingTicks = 10; |
|
|
tempEvents.subscribeTo("physicTick", () => { |
|
|
remainingTicks--; |
|
|
if (remainingTicks <= 0) { |
|
|
tempEvents.cleanup(); |
|
|
resolve(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
} finally { |
|
|
tempEvents.cleanup(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface CollectOptions { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
append?: boolean; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ignoreNoPath?: boolean; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chestLocations?: Vec3[]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
itemFilter?: ItemFilter; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
count?: number; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
interface CollectOptionsFull { |
|
|
append: boolean; |
|
|
ignoreNoPath: boolean; |
|
|
chestLocations: Vec3[]; |
|
|
itemFilter: ItemFilter; |
|
|
targets: Targets; |
|
|
count: number; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class CollectBlock { |
|
|
|
|
|
|
|
|
|
|
|
private readonly bot: Bot; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private readonly targets: Targets; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
movements?: Movements; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chestLocations: Vec3[] = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
constructor(bot: Bot) { |
|
|
this.bot = bot; |
|
|
this.targets = new Targets(bot); |
|
|
|
|
|
this.movements = new Movements(bot, mcDataLoader(bot.version)); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async collect( |
|
|
target: Collectable | Collectable[], |
|
|
options: CollectOptions | Callback = {}, |
|
|
cb?: Callback |
|
|
): Promise<void> { |
|
|
if (typeof options === "function") { |
|
|
cb = options; |
|
|
options = {}; |
|
|
} |
|
|
|
|
|
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); |
|
|
} |
|
|
|
|
|
try { |
|
|
await collectAll(this.bot, optionsFull); |
|
|
this.targets.clear(); |
|
|
} catch (err) { |
|
|
this.targets.clear(); |
|
|
|
|
|
|
|
|
if (err.name !== "PathStopped") throw err; |
|
|
} finally { |
|
|
|
|
|
this.bot.emit("collectBlock_finished"); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
findFromVein( |
|
|
block: Block, |
|
|
maxBlocks = 100, |
|
|
maxDistance = 16, |
|
|
floodRadius = 1 |
|
|
): Block[] { |
|
|
return findFromVein( |
|
|
this.bot, |
|
|
block, |
|
|
maxBlocks, |
|
|
maxDistance, |
|
|
floodRadius |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async cancelTask(cb?: Callback): Promise<void> { |
|
|
if (this.targets.empty) { |
|
|
if (cb != null) cb(); |
|
|
return await Promise.resolve(); |
|
|
} |
|
|
this.bot.pathfinder.stop(); |
|
|
if (cb != null) { |
|
|
|
|
|
this.bot.once("collectBlock_finished", cb); |
|
|
} |
|
|
await once(this.bot, "collectBlock_finished"); |
|
|
} |
|
|
} |
|
|
|