Files
node_server/bot/function/block/shared.ts
T
2026-10-22 17:07:08 +08:00

200 lines
11 KiB
TypeScript

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<ReturnType<Bot['blockAt']>>;
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<Bot>();
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<boolean> {
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<false>(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<unknown>): Promise<boolean> => {
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<typeof setTimeout> | undefined;
try {
if (!await wait(new Promise<void>(resolve => { timer = setTimeout(resolve, 100); }))) return false;
} finally { clearTimeout(timer); }
}
}
return active();
} catch {
stop_owned();
return false;
} finally {
unwatch();
bot.removeListener('end', on_end);
}
}