import type {Bot, RaycastBlock} from '@wp2508/mineflayer'; import type {FunctionArgument} from '../../../types/bot/function/function.ts'; import {on_function_cancel} from '../cancellation.ts'; type Block = NonNullable>; type Vector = Bot['entity']['position']; export type BlockOperation = 'destroy' | 'place' | 'use'; export type BlockTarget = 'cursor' | 'nearest' | 'position' | 'group'; export type CursorArgs = [reach: number, need_look?: boolean]; export type NearestArgs = [name: string, reach: number, face: number, need_look?: boolean]; export type PositionArgs = [x: number, y: number, z: number, face: number, need_look?: boolean]; export type GroupArgs = [positions: number[], need_look?: boolean]; const digging = new WeakSet(); const faces = [[0, -1, 0], [0, 1, 0], [0, 0, -1], [0, 0, 1], [-1, 0, 0], [1, 0, 0]] as const; export function block_arguments(target: BlockTarget): FunctionArgument[] { const arg = (name: string, type: FunctionArgument['type'], label: string, description: string): FunctionArgument => ({name, type, i18n: {lang: 'zh-CN', name: label, description}}); const reach = arg('reach', 'number', '最大距离', '眼睛到方块碰撞箱的最大距离,必须大于 0 且不超过 6'); const face = arg('face', 'number', '操作面', '整数 0 下、1 上、2 北、3 南、4 西、5 东'); const look = {...arg('need_look', 'boolean', '转向操作面', '操作前转向方块操作面的中心,默认不转向'), default: false}; switch (target) { case 'cursor': return [reach, look]; case 'nearest': return [arg('name', 'string', '方块 ID', '不区分大小写的方块 ID 子串,可带 minecraft: 前缀'), reach, face, look]; case 'position': return [ ...['x', 'y', 'z'].map(axis => arg(axis, 'number', `${axis.toUpperCase()} 坐标`, '方块坐标,必须是安全整数;最大操作距离为 4.5')), face, look, ]; case 'group': return [arg('positions', 'number-array', '坐标与操作面列表', '非空平铺数组,每四项为 x、y、z、face;坐标为安全整数,面为 0 到 5,最大距离 4.5;按顺序操作,失败停止且不回滚'), look]; } } function valid_face(value: unknown): value is number { return typeof value === 'number' && Number.isInteger(value) && value >= 0 && value <= 5; } function valid_position(values: unknown[]): boolean { return values.length === 4 && [values[0], values[1], values[2]].every(value => typeof value === 'number' && Number.isSafeInteger(value)) && valid_face(values[3]); } function valid_block(block: Block | null | undefined): block is Block { return !!block && !!block.position && typeof block.name === 'string' && !['air', 'cave_air', 'void_air'].includes(block.name.toLowerCase().replace(/^minecraft:/, '')) && [block.position.x, block.position.y, block.position.z].every(Number.isSafeInteger); } function eye(bot: Bot): Vector | undefined { const self = bot.entity; const height = (self as Bot['entity'] & {eyeHeight?: number})?.eyeHeight ?? self?.height; if (!self?.position || ![self.position.x, self.position.y, self.position.z, height].every(Number.isFinite)) return; return self.position.offset(0, height, 0); } function distance(point: Vector, block: Block): number { const pos = block.position; return Math.hypot(Math.max(pos.x - point.x, point.x - pos.x - 1, 0), Math.max(pos.y - point.y, point.y - pos.y - 1, 0), Math.max(pos.z - point.z, point.z - pos.z - 1, 0)); } export async function run_block_operation(bot: Bot, args: unknown[], target: BlockTarget, operation: BlockOperation): Promise { if (!Array.isArray(args)) return false; const length = target === 'position' ? 4 : target === 'nearest' ? 3 : 1; if (args.length !== length && args.length !== length + 1) return false; if (args.length > length && typeof args[length] !== 'boolean') return false; const need_look = args[length] === true; args = args.slice(0, length); let positions: number[][] = []; let reach = 4.5; let query = ''; if (target === 'cursor' || target === 'nearest') { if (args.length !== (target === 'cursor' ? 1 : 3)) return false; const value = args[target === 'cursor' ? 0 : 1]; if (typeof value !== 'number' || !Number.isFinite(value) || value <= 0 || value > 6) return false; reach = value; if (target === 'nearest') { if (typeof args[0] !== 'string' || !valid_face(args[2])) return false; query = args[0].trim().toLowerCase().replace(/^minecraft:/, ''); if (!query) return false; } } else if (target === 'position') { if (!valid_position(args)) return false; positions = [args.slice() as number[]]; } else { if (args.length !== 1 || !Array.isArray(args[0]) || !args[0].length || args[0].length % 4 !== 0) return false; for (let i = 0; i < args[0].length; i += 4) { const position = args[0].slice(i, i + 4); if (!valid_position(position)) return false; positions.push(position); } } const self = bot.entity; const client = bot._client; let ended = false; let owned_dig: Block | undefined; let cancel!: () => void; const cancelled = new Promise(resolve => { cancel = () => resolve(false); }); const active = () => !ended && !!self && !!client && bot.entity === self && bot._client === client && self.isValid !== false && !(client as typeof client & {ended?: boolean}).ended; const stop_owned = () => { if (owned_dig && bot.targetDigBlock === owned_dig) { try { bot.stopDigging(); } catch { /* The connection may already be closed. */ } } }; const on_end = () => { ended = true; stop_owned(); cancel(); }; const unwatch = on_function_cancel(bot, on_end); const wait = async (promise: Promise): Promise => { const result = await Promise.race([promise.then(() => true, () => false), cancelled]); return result && active(); }; try { bot.on('end', on_end); if (!active() || !eye(bot) || operation === 'place' && !bot.heldItem) return false; let selected: Block | undefined; let cursor: RaycastBlock | undefined; if (target === 'cursor') { cursor = bot.blockAtCursor(reach) ?? undefined; if (!valid_block(cursor) || !valid_face(cursor.face) || !cursor.intersect || ![cursor.intersect.x, cursor.intersect.y, cursor.intersect.z].every(Number.isFinite)) return false; selected = cursor; } else if (target === 'nearest') { // findBlocks sorts block origins around a floored point. Include every // candidate before applying our eye-to-AABB distance ordering. const radius = Math.ceil(reach + 2 * Math.sqrt(3)); const matching = (block: Block | null) => !!block && typeof block.name === 'string' && block.name.toLowerCase().replace(/^minecraft:/, '').includes(query); selected = bot.findBlocks({point: eye(bot)!, matching, maxDistance: radius, count: (2 * radius + 1) ** 3}) .map(position => bot.blockAt(position)).filter(valid_block).filter(matching) .map(block => ({block: block!, distance: distance(eye(bot)!, block!)})) .filter(entry => entry.distance <= reach).sort((a, b) => a.distance - b.distance)[0]?.block; if (!selected) return false; } const total = target === 'group' ? positions.length : 1; for (let i = 0; i < total; i++) { if (!active()) return false; const position = positions[i]; const block = selected ?? bot.blockAt(self.position.scaled(0).offset(position![0]!, position![1]!, position![2]!)); const point = eye(bot); if (!valid_block(block) || !point || distance(point, block) > reach) return false; const loaded = bot.blockAt(block.position); if (!valid_block(loaded) || loaded.type !== block.type) return false; const face = cursor?.face ?? (target === 'nearest' ? args[2] as number : position![3]!); const components = faces[face]!; const vector = self.position.scaled(0).offset(components[0], components[1], components[2]); const delta = cursor?.intersect.minus(block.position); const {type, stateId} = block; const valid = () => { const point = eye(bot); const loaded = bot.blockAt(block.position); return active() && !!point && distance(point, block) <= reach && valid_block(loaded) && loaded.type === type && loaded.stateId === stateId && (operation !== 'place' || !!bot.heldItem) && (operation !== 'destroy' || bot.canDigBlock(block)); }; if (operation === 'destroy') { if (digging.has(bot) || bot.targetDigBlock || !bot.canDigBlock(block)) return false; digging.add(bot); owned_dig = block; } try { if (need_look && !await wait(bot.lookAt(block.position.offset(0.5, 0.5, 0.5).plus(vector.scaled(0.5))))) return false; if (!valid()) return false; if (operation === 'destroy') { if (bot.targetDigBlock) return false; try { const promise = bot.dig(block, 'ignore', vector); if (!await wait(promise)) { stop_owned(); return false; } } catch { stop_owned(); return false; } } else if (operation === 'place') { if (!await wait(bot._placeBlockWithOptions(block, vector, {swingArm: 'right', forceLook: 'ignore', delta}))) return false; } else if (!await wait(bot.activateBlock(block, vector, delta, 'ignore'))) return false; } finally { if (operation === 'destroy') { owned_dig = undefined; digging.delete(bot); } } if (!active()) return false; if (operation === 'place' && target === 'group' && i + 1 < total) { let timer: ReturnType | undefined; try { if (!await wait(new Promise(resolve => { timer = setTimeout(resolve, 100); }))) return false; } finally { clearTimeout(timer); } } } return active(); } catch { stop_owned(); return false; } finally { unwatch(); bot.removeListener('end', on_end); } }