chore: initial commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import {base_define} from "../../../types/bot/plugins/plugin.ts";
|
||||
|
||||
export const define: base_define = {
|
||||
path: "block",
|
||||
i18n: [
|
||||
{
|
||||
lang: "zh-CN",
|
||||
name: "方块操作",
|
||||
description: "Bot 方块操作, 提供方块相关功能"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type CursorArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorArgs> {
|
||||
readonly name = 'destroy_cursor_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '破坏光标方块', description: '破坏光标指向的方块,默认保留视角,不自动移动或切换工具'};
|
||||
readonly args = block_arguments('cursor');
|
||||
async run(bot: Bot, args: CursorArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'cursor', 'destroy');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type NearestArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestArgs> {
|
||||
readonly name = 'destroy_nearest_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '破坏最近方块', description: '破坏范围内名称匹配且距离眼睛最近的已加载方块,不自动移动或切换工具'};
|
||||
readonly args = block_arguments('nearest');
|
||||
async run(bot: Bot, args: NearestArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'nearest', 'destroy');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type PositionArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<PositionArgs> {
|
||||
readonly name = 'destroy_position_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '破坏指定方块', description: '破坏指定坐标及面的方块,操作距离为 4.5,不自动移动或切换工具'};
|
||||
readonly args = block_arguments('position');
|
||||
async run(bot: Bot, args: PositionArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'position', 'destroy');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type GroupArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<GroupArgs> {
|
||||
readonly name = 'destroy_position_group_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '依次破坏方块', description: '依次破坏坐标列表中的方块,失败停止且不回滚,不自动移动或切换工具'};
|
||||
readonly args = block_arguments('group');
|
||||
async run(bot: Bot, args: GroupArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'group', 'destroy');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type CursorArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorArgs> {
|
||||
readonly name = 'place_cursor_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '向光标方块放置', description: '使用主手物品向光标命中面及交点放置,默认保留视角,不自动移动或装备'};
|
||||
readonly args = block_arguments('cursor');
|
||||
async run(bot: Bot, args: CursorArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'cursor', 'place');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type NearestArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestArgs> {
|
||||
readonly name = 'place_nearest_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '向最近方块放置', description: '使用主手物品向范围内最近的匹配方块指定面放置,不自动移动或装备'};
|
||||
readonly args = block_arguments('nearest');
|
||||
async run(bot: Bot, args: NearestArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'nearest', 'place');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type PositionArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<PositionArgs> {
|
||||
readonly name = 'place_position_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '向指定方块放置', description: '使用主手物品向指定坐标方块的指定面放置,距离为 4.5,不自动移动或装备'};
|
||||
readonly args = block_arguments('position');
|
||||
async run(bot: Bot, args: PositionArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'position', 'place');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type GroupArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<GroupArgs> {
|
||||
readonly name = 'place_position_group_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '依次放置方块', description: '使用主手物品按列表放置,每次间隔 100 毫秒,失败停止且不回滚,不自动移动或装备'};
|
||||
readonly args = block_arguments('group');
|
||||
async run(bot: Bot, args: GroupArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'group', 'place');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type CursorArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorArgs> {
|
||||
readonly name = 'use_cursor_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '使用光标方块', description: '右键使用光标命中方块的面与交点,默认保留视角,不自动移动或装备'};
|
||||
readonly args = block_arguments('cursor');
|
||||
async run(bot: Bot, args: CursorArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'cursor', 'use');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type NearestArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestArgs> {
|
||||
readonly name = 'use_nearest_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '使用最近方块', description: '右键使用范围内最近的匹配方块指定面,不自动移动或装备'};
|
||||
readonly args = block_arguments('nearest');
|
||||
async run(bot: Bot, args: NearestArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'nearest', 'use');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type PositionArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<PositionArgs> {
|
||||
readonly name = 'use_position_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '使用指定方块', description: '右键使用指定坐标方块的指定面,距离为 4.5,不自动移动或装备'};
|
||||
readonly args = block_arguments('position');
|
||||
async run(bot: Bot, args: PositionArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'position', 'use');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {block_arguments, run_block_operation, type GroupArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<GroupArgs> {
|
||||
readonly name = 'use_position_group_block';
|
||||
readonly i18n = {lang: 'zh-CN', name: '依次使用方块', description: '按坐标列表依次右键使用方块,失败停止且不回滚,不自动移动或装备'};
|
||||
readonly args = block_arguments('group');
|
||||
async run(bot: Bot, args: GroupArgs): Promise<boolean> {
|
||||
return run_block_operation(bot, args, 'group', 'use');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {argument, guarded} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<[message: string]> {
|
||||
readonly name = 'chat';
|
||||
readonly i18n = {lang: 'zh-CN', name: '发送聊天', description: '原样发送聊天消息,成功仅表示本地调用完成,不等待服务器确认'};
|
||||
readonly args = [argument('message', 'string', '消息', '非空白消息,最多 256 个字符,不允许换行或空字符')];
|
||||
async run(bot: Bot, args: [string]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 1 || typeof args[0] !== 'string'
|
||||
|| !args[0].trim() || args[0].length > 256 || /[\r\n\0]/.test(args[0])) return false;
|
||||
return guarded(bot, async session => {
|
||||
bot.chat(args[0]);
|
||||
return session.active();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {base_define} from "../../../types/bot/plugins/plugin.ts";
|
||||
|
||||
export const define: base_define = {
|
||||
path: "bot",
|
||||
i18n: [
|
||||
{
|
||||
lang: "zh-CN",
|
||||
name: "BOT操作",
|
||||
description: "Bot 自身操作, 提供自身相关功能"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {pulse} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<[]> {
|
||||
readonly name = 'jump';
|
||||
readonly i18n = {lang: 'zh-CN', name: '跳跃一次', description: '在地面且未按住跳跃时,按住跳跃直到下一物理刻,最多等待 1 秒'};
|
||||
readonly args: [] = [];
|
||||
async run(bot: Bot, args: []): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 0 || !bot.entity?.onGround) return false;
|
||||
return pulse(bot, 'jump');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {argument, guarded} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<[x: number, y: number, z: number]> {
|
||||
readonly name = 'look_at';
|
||||
readonly i18n = {lang: 'zh-CN', name: '看向坐标', description: '看向指定有限坐标,允许小数,最多等待 5 秒,目标不能与眼睛重合'};
|
||||
readonly args = ['x', 'y', 'z'].map(axis => argument(axis, 'number', `${axis} 坐标`, '有限数值,允许小数'));
|
||||
async run(bot: Bot, args: [number, number, number]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 3 || !args.every(value => typeof value === 'number' && Number.isFinite(value))) return false;
|
||||
return guarded(bot, async session => {
|
||||
const self = bot.entity;
|
||||
const eye_height = (self as typeof self & {eyeHeight?: number}).eyeHeight;
|
||||
if (!self.position || ![self.position.x, self.position.y, self.position.z, eye_height].every(Number.isFinite)) return false;
|
||||
const target = self.position.offset(0, 0, 0);
|
||||
[target.x, target.y, target.z] = args;
|
||||
if (target.x === self.position.x && target.y === self.position.y + eye_height! && target.z === self.position.z) return false;
|
||||
return await session.wait(() => bot.lookAt(target), 5000, 'promise') && session.active();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type {Bot, ControlState} from '@wp2508/mineflayer';
|
||||
import type {FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
import {on_function_cancel} from '../cancellation.ts';
|
||||
|
||||
export const argument = (name: string, type: FunctionArgument['type'], label: string, description: string): FunctionArgument =>
|
||||
({name, type, i18n: {lang: 'zh-CN', name: label, description}});
|
||||
|
||||
const owners = new WeakMap<Bot, Map<ControlState, symbol>>();
|
||||
function controls(bot: Bot): Map<ControlState, symbol> {
|
||||
let keys = owners.get(bot);
|
||||
if (!keys) owners.set(bot, keys = new Map());
|
||||
return keys;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
active(): boolean;
|
||||
wait(start: () => unknown, duration: number, mode: 'physicsTick' | 'delay' | 'promise'): Promise<boolean>;
|
||||
}
|
||||
|
||||
export async function guarded(bot: Bot, operation: (session: Session) => Promise<boolean>): Promise<boolean> {
|
||||
const self = bot.entity;
|
||||
const client = bot._client;
|
||||
let ended = false;
|
||||
let cancel: (() => void) | undefined;
|
||||
const on_end = () => { ended = true; cancel?.(); };
|
||||
const active = () => !ended && !!self && !!client && self.isValid !== false
|
||||
&& bot.entity === self && bot._client === client && client.state === 'play';
|
||||
const unwatch = on_function_cancel(bot, on_end);
|
||||
try {
|
||||
if (!active()) return false;
|
||||
bot.on('end', on_end);
|
||||
bot.on('error', on_end);
|
||||
client.on('end', on_end);
|
||||
client.on('error', on_end);
|
||||
return await operation({active, wait(start, duration, mode) {
|
||||
return new Promise<boolean>(resolve => {
|
||||
let settled = false;
|
||||
let started = false;
|
||||
let tick_seen = false;
|
||||
const finish = (success: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
bot.removeListener('physicsTick', on_tick);
|
||||
cancel = undefined;
|
||||
resolve(success && active());
|
||||
};
|
||||
const on_tick = () => {
|
||||
tick_seen = true;
|
||||
if (started) finish(true);
|
||||
};
|
||||
const timer = setTimeout(() => finish(mode === 'delay'), duration);
|
||||
cancel = () => finish(false);
|
||||
if (mode === 'physicsTick') bot.on('physicsTick', on_tick);
|
||||
try {
|
||||
if (!active()) return finish(false);
|
||||
const result = start();
|
||||
started = true;
|
||||
Promise.resolve(result).then(() => {
|
||||
if (mode === 'promise') finish(true);
|
||||
}, () => finish(false));
|
||||
if (mode === 'physicsTick' && tick_seen) finish(true);
|
||||
} catch {
|
||||
finish(false);
|
||||
}
|
||||
});
|
||||
}});
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
unwatch();
|
||||
cancel?.();
|
||||
bot.removeListener('end', on_end);
|
||||
bot.removeListener('error', on_end);
|
||||
client?.removeListener('end', on_end);
|
||||
client?.removeListener('error', on_end);
|
||||
}
|
||||
}
|
||||
|
||||
export async function pulse(bot: Bot, key: ControlState, duration?: number): Promise<boolean> {
|
||||
return guarded(bot, async session => {
|
||||
if (bot.getControlState(key)) return false;
|
||||
const keys = controls(bot);
|
||||
const owner = Symbol(key);
|
||||
keys.set(key, owner);
|
||||
try {
|
||||
const success = await session.wait(() => bot.setControlState(key, true), duration ?? 1000,
|
||||
duration === undefined ? 'physicsTick' : 'delay');
|
||||
return success && session.active();
|
||||
} finally {
|
||||
// A persistent command may have taken ownership while this pulse was waiting.
|
||||
if (keys.get(key) === owner) {
|
||||
keys.delete(key);
|
||||
bot.setControlState(key, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function set_sneak(bot: Bot, enabled: boolean): void {
|
||||
controls(bot).delete('sneak');
|
||||
bot.setControlState('sneak', enabled);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {argument, guarded, set_sneak} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<[enabled: boolean]> {
|
||||
readonly name = 'squat_continue_switch';
|
||||
readonly i18n = {lang: 'zh-CN', name: '设置持续下蹲', description: '显式设置持续下蹲状态,重复设置相同状态不会切换'};
|
||||
readonly args = [argument('enabled', 'boolean', '启用下蹲', 'true 持续下蹲,false 取消下蹲')];
|
||||
async run(bot: Bot, args: [boolean]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 1 || typeof args[0] !== 'boolean') return false;
|
||||
return guarded(bot, async session => {
|
||||
set_sneak(bot, args[0]);
|
||||
return session.active();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {pulse} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<[]> {
|
||||
readonly name = 'squat_once';
|
||||
readonly i18n = {lang: 'zh-CN', name: '下蹲一次', description: '未按住下蹲时,按住下蹲直到下一物理刻,最多等待 1 秒'};
|
||||
readonly args: [] = [];
|
||||
async run(bot: Bot, args: []): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 0) return false;
|
||||
return pulse(bot, 'sneak');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {cancel_functions} from '../cancellation.ts';
|
||||
|
||||
export class init implements BaseFunction<[]> {
|
||||
readonly name = 'stop_current_function';
|
||||
readonly i18n = {lang: 'zh-CN', name: '停止当前函数', description: '立即取消当前和排队函数,停止挖掘、使用物品并释放所有控制键'};
|
||||
readonly args: [] = [];
|
||||
|
||||
async run(bot: Bot, args: []): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 0) return false;
|
||||
let success = true;
|
||||
for (const stop of [() => cancel_functions(bot), () => bot.stopDigging(),
|
||||
() => bot.deactivateItem(), () => bot.clearControlStates()]) {
|
||||
try { stop(); } catch { success = false; }
|
||||
}
|
||||
return success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type {Bot, ControlState} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {argument, pulse} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<[direction: string, duration_ms: number]> {
|
||||
readonly name = 'use_direction_key';
|
||||
readonly i18n = {lang: 'zh-CN', name: '按住方向键', description: '按住指定方向键一段时间后释放,拒绝操作已按住的方向键'};
|
||||
readonly args = [argument('direction', 'string', '方向', '仅允许 forward、back、left、right'),
|
||||
argument('duration_ms', 'number', '持续毫秒数', '1 到 5000 的安全整数')];
|
||||
async run(bot: Bot, args: [string, number]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 2 || typeof args[0] !== 'string'
|
||||
|| !['forward', 'back', 'left', 'right'].includes(args[0])
|
||||
|| !Number.isSafeInteger(args[1]) || args[1] < 1 || args[1] > 5000) return false;
|
||||
return pulse(bot, args[0] as ControlState, args[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import {EventEmitter} from 'node:events';
|
||||
import {AsyncLocalStorage} from 'node:async_hooks';
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
|
||||
type Cancellation = {generation: number; events: EventEmitter};
|
||||
const scopes = new AsyncLocalStorage<{bot: Bot; cancellation: Cancellation}>();
|
||||
const states = new WeakMap<Bot, Cancellation & {scopes: Set<Cancellation>}>();
|
||||
function state(bot: Bot) {
|
||||
let value = states.get(bot);
|
||||
if (!value) states.set(bot, value = {generation: 0, events: new EventEmitter(), scopes: new Set()});
|
||||
return value;
|
||||
}
|
||||
|
||||
function current(bot: Bot): Cancellation {
|
||||
const scope = scopes.getStore();
|
||||
return scope?.bot === bot ? scope.cancellation : state(bot);
|
||||
}
|
||||
|
||||
export function cancellation_generation(bot: Bot): number {
|
||||
return current(bot).generation;
|
||||
}
|
||||
|
||||
export function on_function_cancel(bot: Bot, listener: () => void): () => void {
|
||||
const {events} = current(bot);
|
||||
events.on('cancel', listener);
|
||||
return () => { events.removeListener('cancel', listener); };
|
||||
}
|
||||
|
||||
/** Existing helpers inherit the call's cancellation scope across awaits. */
|
||||
export function create_function_cancellation(bot: Bot) {
|
||||
const value = state(bot);
|
||||
const cancellation: Cancellation = {generation: 0, events: new EventEmitter()};
|
||||
value.scopes.add(cancellation);
|
||||
return {
|
||||
run<T>(operation: () => T): T { return scopes.run({bot, cancellation}, operation); },
|
||||
cancel() {
|
||||
cancellation.generation++;
|
||||
cancellation.events.emit('cancel');
|
||||
},
|
||||
dispose() { value.scopes.delete(cancellation); },
|
||||
};
|
||||
}
|
||||
|
||||
export function cancel_functions(bot: Bot): void {
|
||||
const value = state(bot);
|
||||
value.generation++;
|
||||
value.events.emit('cancel');
|
||||
for (const cancellation of value.scopes) {
|
||||
cancellation.generation++;
|
||||
cancellation.events.emit('cancel');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type CursorArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorArgs> {
|
||||
readonly name = 'attack_cursor_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '攻击光标实体', description: '攻击光标指向且在指定距离内的实体,不自动移动'};
|
||||
readonly args = entity_arguments(false, false, true);
|
||||
async run(bot: Bot, args: CursorArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, false, 'attack');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type NearestArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestArgs> {
|
||||
readonly name = 'attack_nearest_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '攻击最近实体', description: '攻击指定距离内名称匹配的最近实体,可选瞄准,不自动移动'};
|
||||
readonly args = entity_arguments(true, false, true);
|
||||
async run(bot: Bot, args: NearestArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, true, 'attack', this.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {base_define} from "../../../types/bot/plugins/plugin.ts";
|
||||
|
||||
export const define: base_define = {
|
||||
path: "entity",
|
||||
i18n: [
|
||||
{
|
||||
lang: "zh-CN",
|
||||
name: "实体操作",
|
||||
description: "Bot 实体操作, 提供实体相关功能"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type CursorItemArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorItemArgs> {
|
||||
readonly name = 'feed_cursor_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '喂食光标实体', description: '对光标实体使用指定手的物品,可选瞄准,由服务器判断是否可以喂食'};
|
||||
readonly args = entity_arguments(false, true);
|
||||
async run(bot: Bot, args: CursorItemArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, false, 'item');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type NearestItemArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestItemArgs> {
|
||||
readonly name = 'feed_nearest_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '喂食最近实体', description: '对指定距离内名称匹配的最近实体使用指定手的物品,由服务器判断是否可以喂食'};
|
||||
readonly args = entity_arguments(true, true);
|
||||
async run(bot: Bot, args: NearestItemArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, true, 'item', this.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type CursorArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorArgs> {
|
||||
readonly name = 'null_main_hand_right_cursor_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '空主手右键光标实体', description: '主手为空时右键交互光标实体,可选瞄准,主手有物品则失败'};
|
||||
readonly args = entity_arguments(false, false);
|
||||
|
||||
async run(bot: Bot, args: CursorArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, false, 'empty_hand');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type NearestArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestArgs> {
|
||||
readonly name = 'null_main_hand_right_nearest_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '空主手右键最近实体', description: '主手为空时右键交互指定距离内名称匹配的最近实体,可选瞄准,主手有物品则失败'};
|
||||
readonly args = entity_arguments(true, false);
|
||||
|
||||
async run(bot: Bot, args: NearestArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, true, 'empty_hand', this.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type CursorArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorArgs> {
|
||||
readonly name = 'open_cursor_entity_container';
|
||||
readonly i18n = {lang: 'zh-CN', name: '打开光标实体容器', description: '激活光标实体并等待新容器窗口,已有窗口时失败,最多等待五秒'};
|
||||
readonly args = entity_arguments(false, false);
|
||||
async run(bot: Bot, args: CursorArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, false, 'open');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type NearestArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestArgs> {
|
||||
readonly name = 'open_nearest_entity_container';
|
||||
readonly i18n = {lang: 'zh-CN', name: '打开最近实体容器', description: '激活指定距离内名称匹配的最近实体并等待新窗口,已有窗口时失败,最多等待五秒'};
|
||||
readonly args = entity_arguments(true, false);
|
||||
async run(bot: Bot, args: NearestArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, true, 'open', this.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
import {on_function_cancel} from '../cancellation.ts';
|
||||
|
||||
type Entity = NonNullable<ReturnType<Bot['entityAtCursor']>>;
|
||||
export type EntityOperation = 'attack' | 'item' | 'open' | 'empty_hand';
|
||||
export type CursorArgs = [reach: number, ignore_blocks: boolean, need_look?: boolean];
|
||||
export type NearestArgs = [name: string, reach: number, lock_target: boolean, need_look?: boolean];
|
||||
export type CursorItemArgs = [reach: number, ignore_blocks: boolean, off_hand: boolean, need_look?: boolean];
|
||||
export type NearestItemArgs = [name: string, reach: number, off_hand: boolean, lock_target: boolean, need_look?: boolean];
|
||||
|
||||
const target_locks = new WeakMap<Bot, {
|
||||
self: Bot['entity']; client: Bot['_client']; targets: Map<string, Entity>;
|
||||
}>();
|
||||
|
||||
export function entity_arguments(nearest: boolean, item: boolean, attack = false): 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(nearest && !attack ? 'max_distance' : 'reach', 'number', '最大距离', '眼睛到实体碰撞箱的最大距离,必须为有限正数');
|
||||
const args = nearest
|
||||
? [arg('name', 'string', '实体类型 ID 或用户名', '不区分大小写的子串匹配,如 zom 或 minecraft:zom;也支持玩家用户名'), reach]
|
||||
: [reach, arg('ignore_blocks', 'boolean', '忽略方块遮挡', '是否忽略光标射线上的方块遮挡')];
|
||||
if (item) args.push(arg('off_hand', 'boolean', '使用副手', 'false 使用主手,true 使用副手,所选手必须持有物品'));
|
||||
if (nearest) args.push(arg('lock_target', 'boolean', '持续锁定实体', '相同函数和参数持续操作首次选中的实体,直到实体消失;超距时保留锁定但不操作'));
|
||||
args.push({...arg('need_look', 'boolean', '瞄准实体', '是否先瞄准实体中心,默认 false'), default: false});
|
||||
return args;
|
||||
}
|
||||
|
||||
function valid_entity(bot: Bot, target: Entity): boolean {
|
||||
return !!target && target !== bot.entity && target.id !== bot.entity?.id
|
||||
&& !(bot.username && target.username === bot.username)
|
||||
&& target.isValid !== false && bot.entities?.[target.id] === target
|
||||
&& Number.isFinite(target.id) && !!target.position
|
||||
&& [target.position.x, target.position.y, target.position.z, target.width, target.height].every(Number.isFinite)
|
||||
&& target.width > 0 && target.height > 0;
|
||||
}
|
||||
|
||||
function distance(bot: Bot, target: Entity): number {
|
||||
const self = bot.entity;
|
||||
const eye_height = (self as Entity & {eyeHeight?: number})?.eyeHeight ?? self?.height;
|
||||
if (!self?.position || ![self.position.x, self.position.y, self.position.z, eye_height].every(Number.isFinite)) return Infinity;
|
||||
const half = target.width / 2;
|
||||
const dx = Math.max(Math.abs(self.position.x - target.position.x) - half, 0);
|
||||
const dy = Math.max(target.position.y - (self.position.y + eye_height), self.position.y + eye_height - (target.position.y + target.height), 0);
|
||||
const dz = Math.max(Math.abs(self.position.z - target.position.z) - half, 0);
|
||||
return Math.hypot(dx, dy, dz);
|
||||
}
|
||||
|
||||
function has_item(bot: Bot, off_hand: boolean): boolean {
|
||||
return off_hand ? !bot.supportFeature('doesntHaveOffHandSlot') && !!bot.inventory?.slots[45] : !!bot.heldItem;
|
||||
}
|
||||
|
||||
function open_container(bot: Bot, target: Entity, active: () => boolean): Promise<boolean> {
|
||||
if (bot.currentWindow || !active()) return Promise.resolve(false);
|
||||
return new Promise(resolve => {
|
||||
let settled = false;
|
||||
const finish = (success: boolean) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
bot.removeListener('windowOpen', on_open);
|
||||
bot.removeListener('end', on_end);
|
||||
unwatch();
|
||||
resolve(success);
|
||||
};
|
||||
const on_open = (window: Bot['currentWindow']) => {
|
||||
finish(!!window && active() && bot.currentWindow === window);
|
||||
};
|
||||
const on_end = () => finish(false);
|
||||
const timer = setTimeout(() => finish(false), 5000);
|
||||
const unwatch = on_function_cancel(bot, on_end);
|
||||
bot.on('windowOpen', on_open);
|
||||
bot.on('end', on_end);
|
||||
try {
|
||||
Promise.resolve(bot.interactEntity(target, {hand: 0})).catch(() => finish(false));
|
||||
} catch {
|
||||
finish(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function run_entity_operation(
|
||||
bot: Bot, args: unknown[], nearest: boolean, operation: EntityOperation, function_name: string = operation,
|
||||
): Promise<boolean> {
|
||||
const item = operation === 'item';
|
||||
const required_count = (item ? 3 : 2) + (nearest ? 1 : 0);
|
||||
if (!Array.isArray(args) || args.length !== required_count && args.length !== required_count + 1) return false;
|
||||
if (args.length > required_count && typeof args[required_count] !== 'boolean') return false;
|
||||
const need_look = args[required_count] === true;
|
||||
args = args.slice(0, required_count);
|
||||
const reach = args[nearest ? 1 : 0];
|
||||
if (typeof reach !== 'number' || !Number.isFinite(reach) || reach <= 0) return false;
|
||||
if (nearest ? typeof args[0] !== 'string' || !args[0].trim() : typeof args[1] !== 'boolean') return false;
|
||||
if (item && typeof args[2] !== 'boolean') return false;
|
||||
if (nearest && typeof args.at(-1) !== 'boolean') return false;
|
||||
const self = bot.entity;
|
||||
const client = bot._client;
|
||||
let ended = false;
|
||||
let cancel!: () => void;
|
||||
const cancelled = new Promise<false>(resolve => { cancel = () => resolve(false); });
|
||||
const on_end = () => { ended = true; cancel(); };
|
||||
const unwatch = on_function_cancel(bot, on_end);
|
||||
const active = () => !ended && !!self && bot.entity === self && bot._client === client && self.isValid !== false;
|
||||
try {
|
||||
bot.on('end', on_end);
|
||||
if (operation === 'empty_hand' && bot.heldItem) return false;
|
||||
if (!active() || operation === 'open' && bot.currentWindow || item && !has_item(bot, args[2] as boolean)) return false;
|
||||
let target: Entity | undefined;
|
||||
if (nearest) {
|
||||
let locks = target_locks.get(bot);
|
||||
if (!locks || locks.self !== self || locks.client !== client) {
|
||||
locks = {self, client, targets: new Map()};
|
||||
target_locks.set(bot, locks);
|
||||
}
|
||||
for (const [key, entity] of locks.targets) {
|
||||
if (!valid_entity(bot, entity)) locks.targets.delete(key);
|
||||
}
|
||||
const lock_target = args.at(-1) === true;
|
||||
const key = JSON.stringify([function_name, [...args, need_look]]);
|
||||
target = lock_target ? locks.targets.get(key) : undefined;
|
||||
if (!target) {
|
||||
const name = (args[0] as string).trim().toLowerCase();
|
||||
const entity_name = name.replace(/^minecraft:/, '');
|
||||
target = Object.values(bot.entities).filter(entity => valid_entity(bot, entity)
|
||||
&& (entity_name.length > 0 && typeof entity.name === 'string'
|
||||
&& entity.name.toLowerCase().replace(/^minecraft:/, '').includes(entity_name)
|
||||
|| typeof entity.username === 'string' && entity.username.toLowerCase().includes(name)))
|
||||
.map(entity => ({entity, distance: distance(bot, entity)}))
|
||||
.filter(entry => Number.isFinite(entry.distance) && entry.distance <= reach)
|
||||
.sort((a, b) => a.distance - b.distance)[0]?.entity;
|
||||
if (lock_target && target) locks.targets.set(key, target);
|
||||
}
|
||||
} else {
|
||||
target = bot.entityAtCursor(reach, true, args[1] as boolean) ?? undefined;
|
||||
}
|
||||
const in_reach = () => active() && valid_entity(bot, target!) && distance(bot, target!) <= reach;
|
||||
if (!target || !in_reach()) return false;
|
||||
if (need_look) {
|
||||
if (!await Promise.race([
|
||||
bot.lookAt(target.position.offset(0, target.height / 2, 0)).then(() => true, () => false), cancelled,
|
||||
])) return false;
|
||||
if (!in_reach()) return false;
|
||||
}
|
||||
if (operation === 'attack') {
|
||||
bot.attack(target);
|
||||
return true;
|
||||
}
|
||||
if (operation === 'empty_hand') {
|
||||
if (bot.heldItem) return false;
|
||||
bot.interactEntity(target, {hand: 0});
|
||||
return true;
|
||||
}
|
||||
if (item) {
|
||||
if (!has_item(bot, args[2] as boolean)) return false;
|
||||
bot.interactEntity(target, {hand: args[2] ? 1 : 0, position: target.position.offset(0, target.height / 2, 0)});
|
||||
return true;
|
||||
}
|
||||
return await open_container(bot, target, active);
|
||||
} catch {
|
||||
return false;
|
||||
} finally {
|
||||
unwatch();
|
||||
bot.removeListener('end', on_end);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type CursorItemArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<CursorItemArgs> {
|
||||
readonly name = 'use_item_on_cursor_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '对光标实体使用物品', description: '对光标实体使用指定手的物品,可选瞄准,不自动移动'};
|
||||
readonly args = entity_arguments(false, true);
|
||||
async run(bot: Bot, args: CursorItemArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, false, 'item');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction} from '../../../types/bot/function/function.ts';
|
||||
import {entity_arguments, run_entity_operation, type NearestItemArgs} from './shared.ts';
|
||||
|
||||
export class init implements BaseFunction<NearestItemArgs> {
|
||||
readonly name = 'use_item_on_nearest_entity';
|
||||
readonly i18n = {lang: 'zh-CN', name: '对最近实体使用物品', description: '对指定距离内名称匹配的最近实体使用指定手的物品,可选瞄准'};
|
||||
readonly args = entity_arguments(true, true);
|
||||
async run(bot: Bot, args: NearestItemArgs): Promise<boolean> {
|
||||
return run_entity_operation(bot, args, true, 'item', this.name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import {readdir} from 'node:fs/promises';
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument, FunctionI18n} from '../../types/bot/function/function.ts';
|
||||
import type {data_type} from '../../types/bot/instance/bot_runtime.ts';
|
||||
import type {bot_instance_context} from '../instance/bot_instance_init.ts';
|
||||
import {create_function_cancellation, on_function_cancel} from './cancellation.ts';
|
||||
import {measure_sync} from '../info/resource_metrics.ts';
|
||||
|
||||
export interface FunctionTemplate {
|
||||
name: string;
|
||||
i18n: FunctionI18n;
|
||||
args: FunctionArgument[];
|
||||
}
|
||||
|
||||
export interface FunctionCategory {
|
||||
path: string;
|
||||
name: string;
|
||||
i18n: FunctionI18n[];
|
||||
function_list: FunctionTemplate[];
|
||||
}
|
||||
|
||||
export type FunctionList = FunctionCategory[];
|
||||
|
||||
export interface FunctionManager {
|
||||
get_function_list(): FunctionList;
|
||||
run(path: string, name: string, args: unknown[], before_run?: () => void,
|
||||
options?: {signal?: AbortSignal}): Promise<boolean>;
|
||||
/** Cancels queued calls and notifies the running call, which drains before rejecting. */
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
const data_types = new Set<data_type>([
|
||||
'string', 'boolean', 'number', 'datetime', 'string-select', 'number-select',
|
||||
'string-multi-select', 'number-multi-select', 'string-array', 'number-array',
|
||||
]);
|
||||
const valid_name = (value: unknown): value is string => typeof value === 'string' && /^[a-zA-Z0-9_-]+$/.test(value);
|
||||
const valid_i18n = (value: unknown): value is FunctionI18n => {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const item = value as FunctionI18n;
|
||||
return typeof item.lang === 'string' && item.lang.length > 0
|
||||
&& typeof item.name === 'string' && typeof item.description === 'string';
|
||||
};
|
||||
|
||||
async function load_functions(check_active: () => void = () => {}): Promise<{
|
||||
list: FunctionList;
|
||||
functions: Map<string, BaseFunction>;
|
||||
}> {
|
||||
const list: FunctionList = [];
|
||||
const functions = new Map<string, BaseFunction>();
|
||||
const directories = [''];
|
||||
const stamp = `${Date.now()}-${process.hrtime.bigint()}`;
|
||||
const import_module = async (url: URL): Promise<Record<string, unknown>> => {
|
||||
check_active();
|
||||
url.searchParams.set('t', stamp);
|
||||
const module = await import(url.href);
|
||||
check_active();
|
||||
return module;
|
||||
};
|
||||
for (const path of directories) {
|
||||
check_active();
|
||||
const directory = new URL(path ? `./${path}/` : './', import.meta.url);
|
||||
const entries = await readdir(directory, {withFileTypes: true});
|
||||
check_active();
|
||||
entries.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory() && valid_name(entry.name)) directories.push(path ? `${path}/${entry.name}` : entry.name);
|
||||
}
|
||||
if (!path) continue;
|
||||
const files = entries.filter(entry => entry.isFile() && entry.name !== 'define.ts'
|
||||
&& /^[a-zA-Z0-9_-]+\.ts$/.test(entry.name));
|
||||
const has_define = entries.some(entry => entry.isFile() && entry.name === 'define.ts');
|
||||
if (!files.length && !has_define) continue;
|
||||
if (!has_define) throw new Error(`函数分类 ${path} 缺少 define.ts`);
|
||||
try {
|
||||
const {define} = await import_module(new URL('define.ts', directory));
|
||||
const config = define as {path?: unknown; name?: unknown; i18n?: unknown} | undefined;
|
||||
if (config?.path !== path || !Array.isArray(config.i18n) || !config.i18n.length
|
||||
|| !config.i18n.every(valid_i18n) || config.name !== undefined && !valid_name(config.name)) {
|
||||
throw new Error('分类 define 无效,path 必须与目录路径一致');
|
||||
}
|
||||
const category: FunctionCategory = {
|
||||
path, name: typeof config.name === 'string' ? config.name : path.split('/').at(-1)!,
|
||||
i18n: config.i18n.map(item => ({...item})), function_list: [],
|
||||
};
|
||||
for (const file of files) {
|
||||
const name = file.name.slice(0, -3);
|
||||
try {
|
||||
const module = await import_module(new URL(file.name, directory));
|
||||
if (!Object.hasOwn(module, 'init')) continue;
|
||||
if (typeof module.init !== 'function') throw new Error('必须导出 init 类');
|
||||
const instance = new (module.init as new () => BaseFunction)();
|
||||
if (instance.name !== name || !valid_i18n(instance.i18n)
|
||||
|| !Array.isArray(instance.args) || typeof instance.run !== 'function') {
|
||||
throw new Error('函数必须满足 BaseFunction 且 name 与文件名一致');
|
||||
}
|
||||
const names = new Set<string>();
|
||||
let optional = false;
|
||||
for (const arg of instance.args) {
|
||||
if (!arg || !valid_name(arg.name) || names.has(arg.name)
|
||||
|| !data_types.has(arg.type) || !valid_i18n(arg.i18n)) {
|
||||
throw new Error('函数参数定义无效或名称重复');
|
||||
}
|
||||
if (Object.hasOwn(arg, 'default')) {
|
||||
if (arg.type !== 'boolean' || typeof arg.default !== 'boolean') throw new Error('默认值必须匹配布尔参数');
|
||||
optional = true;
|
||||
} else if (optional) throw new Error('带默认值的参数必须位于末尾');
|
||||
names.add(arg.name);
|
||||
}
|
||||
const key = `${path}/${name}`;
|
||||
if (functions.has(key)) throw new Error('函数重复注册');
|
||||
functions.set(key, instance);
|
||||
category.function_list.push({name, i18n: {...instance.i18n},
|
||||
args: instance.args.map(arg => ({name: arg.name, type: arg.type, i18n: {...arg.i18n},
|
||||
...(Object.hasOwn(arg, 'default') ? {default: arg.default} : {})}))});
|
||||
} catch (error) {
|
||||
throw new Error(`函数 ${path}/${name} 加载失败`, {cause: error});
|
||||
}
|
||||
}
|
||||
list.push(category);
|
||||
} catch (error) {
|
||||
throw new Error(`函数分类 ${path} 加载失败`, {cause: error});
|
||||
}
|
||||
}
|
||||
list.sort((a, b) => a.path.localeCompare(b.path));
|
||||
return {list, functions};
|
||||
}
|
||||
|
||||
/** Builds grouped metadata without a Bot; constructors must have no side effects. */
|
||||
export async function get_function_list(): Promise<FunctionList> {
|
||||
return (await load_functions()).list;
|
||||
}
|
||||
|
||||
function valid_argument(value: unknown, type: data_type): boolean {
|
||||
switch (type) {
|
||||
case 'number': case 'number-select': return typeof value === 'number' && Number.isFinite(value);
|
||||
case 'string': case 'string-select': return typeof value === 'string';
|
||||
case 'boolean': return typeof value === 'boolean';
|
||||
case 'datetime': return typeof value === 'string'
|
||||
&& /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(value) && Number.isFinite(Date.parse(value));
|
||||
case 'string-array': case 'string-multi-select':
|
||||
return Array.isArray(value) && Array.from(value).every(item => typeof item === 'string');
|
||||
case 'number-array': case 'number-multi-select':
|
||||
return Array.isArray(value) && Array.from(value).every(item => typeof item === 'number' && Number.isFinite(item));
|
||||
}
|
||||
}
|
||||
|
||||
/** The caller owns disposal; this manager never registers end listeners or follows a replacement Bot. */
|
||||
export async function create_function_manager(
|
||||
context: Pick<bot_instance_context<unknown>, 'bot_instance' | 'bot_status'>
|
||||
& Partial<Pick<bot_instance_context<unknown>, 'workflow_manager'>>,
|
||||
): Promise<FunctionManager> {
|
||||
const bot: Bot | null = context.bot_instance;
|
||||
if (!bot) throw new Error('Bot 实例尚未创建');
|
||||
const check_instance = () => {
|
||||
if (context.bot_instance !== bot) throw new Error('Bot 实例已变化');
|
||||
};
|
||||
const {list, functions} = await load_functions(check_instance);
|
||||
let disposed = false;
|
||||
let tail = Promise.resolve();
|
||||
const pending = new Set<{reject: (error: unknown) => void; resolve: (value: boolean) => void}>();
|
||||
let cancel_running: ((reason: unknown) => void) | undefined;
|
||||
const check_active = () => {
|
||||
if (disposed) throw new Error('Function manager 已销毁');
|
||||
check_instance();
|
||||
};
|
||||
return {
|
||||
get_function_list() {
|
||||
check_active();
|
||||
return structuredClone(list);
|
||||
},
|
||||
async run(path, name, args, before_run, options) {
|
||||
const signal = options?.signal;
|
||||
signal?.throwIfAborted();
|
||||
check_active();
|
||||
const template = list.find(category => category.path === path)?.function_list.find(item => item.name === name);
|
||||
const instance = template && functions.get(`${path}/${name}`);
|
||||
if (!instance || !template) throw new Error('函数不存在');
|
||||
if (!Array.isArray(args) || args.length > template.args.length
|
||||
|| !template.args.every((arg, index) => index < args.length
|
||||
? valid_argument(args[index], arg.type) : Object.hasOwn(arg, 'default'))) {
|
||||
throw new Error('函数参数数量或类型不正确');
|
||||
}
|
||||
const values = structuredClone(template.args.map((arg, index) => index < args.length ? args[index] : arg.default));
|
||||
if (path === 'workflow' && instance.execution === 'workflow_control') {
|
||||
const workflow = context.workflow_manager;
|
||||
const check_control = () => {
|
||||
signal?.throwIfAborted();
|
||||
check_active();
|
||||
if (context.workflow_manager !== workflow) throw new Error('Workflow manager 已变化');
|
||||
};
|
||||
before_run?.();
|
||||
check_control();
|
||||
if (!workflow) return false;
|
||||
// Bind only the control capability to this dispatch, never the raw Bot context.
|
||||
let result = false;
|
||||
try {
|
||||
result = await measure_sync(bot, () => instance.run(bot, values, {workflow: {
|
||||
control(request) {
|
||||
check_control();
|
||||
const command = workflow.control(request);
|
||||
check_control();
|
||||
return command;
|
||||
},
|
||||
}})) === true;
|
||||
} catch { /* Function failures retain the boolean API. */ }
|
||||
check_control();
|
||||
return result;
|
||||
}
|
||||
if (context.bot_status !== 'online') throw new Error('Bot 当前不在线');
|
||||
if (path === 'bot' && name === 'stop_current_function') {
|
||||
before_run?.();
|
||||
signal?.throwIfAborted();
|
||||
check_active();
|
||||
for (const task of pending) task.resolve(false);
|
||||
pending.clear();
|
||||
const result = await measure_sync(bot, () => instance.run(bot, values));
|
||||
signal?.throwIfAborted();
|
||||
return result;
|
||||
}
|
||||
if (pending.size >= 64) throw new Error('函数等待队列已满');
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
let cancel_execution: ((reason: unknown) => void) | undefined;
|
||||
const cleanup = () => signal?.removeEventListener('abort', abort);
|
||||
const task = {
|
||||
reject(error: unknown) { cleanup(); reject(error); },
|
||||
resolve(value: boolean) { cleanup(); resolve(value); },
|
||||
};
|
||||
const abort = () => {
|
||||
if (pending.delete(task)) task.reject(signal!.reason);
|
||||
else cancel_execution?.(signal!.reason);
|
||||
};
|
||||
pending.add(task);
|
||||
signal?.addEventListener('abort', abort, {once: true});
|
||||
const execute = async () => {
|
||||
if (!pending.delete(task)) return;
|
||||
try {
|
||||
check_active();
|
||||
if (context.bot_status !== 'online') throw new Error('Bot 当前不在线');
|
||||
signal?.throwIfAborted();
|
||||
before_run?.();
|
||||
signal?.throwIfAborted();
|
||||
check_active();
|
||||
const cancellation = create_function_cancellation(bot);
|
||||
let interrupted = false;
|
||||
let reason: unknown;
|
||||
cancel_running = cancel_execution = error => {
|
||||
if (interrupted) return;
|
||||
interrupted = true;
|
||||
reason = error;
|
||||
cancellation.cancel();
|
||||
};
|
||||
try {
|
||||
await cancellation.run(async () => {
|
||||
// Global stop may answer early, but the queue still drains instance.run.
|
||||
const unwatch = on_function_cancel(bot, () => {
|
||||
if (!interrupted) task.resolve(false);
|
||||
});
|
||||
let result = false;
|
||||
try { result = await measure_sync(bot, () => instance.run(bot, values)) === true; }
|
||||
catch { /* Function failures retain the boolean API. */ }
|
||||
finally { unwatch(); }
|
||||
if (interrupted) task.reject(reason);
|
||||
else task.resolve(result);
|
||||
});
|
||||
} finally {
|
||||
cleanup();
|
||||
cancel_running = cancel_execution = undefined;
|
||||
cancellation.dispose();
|
||||
}
|
||||
} catch (error) { task.reject(error); }
|
||||
};
|
||||
tail = tail.then(execute, execute);
|
||||
});
|
||||
},
|
||||
dispose() {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
for (const task of pending) task.reject(new Error('Function manager 已销毁,排队调用已取消'));
|
||||
pending.clear();
|
||||
functions.clear();
|
||||
cancel_running?.(new Error('Function manager 已销毁,当前调用已取消'));
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
|
||||
export class init implements BaseFunction<number[]> {
|
||||
readonly name = 'change_main_hand_slot';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '切换主手槽位', description: '切换主手选中的快捷栏槽位,编号为 0 到 8',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [{
|
||||
name: 'slot', type: 'number',
|
||||
i18n: {lang: 'zh-CN', name: '快捷栏槽位', description: '0 到 8 的整数,分别对应快捷栏第 1 到第 9 格'},
|
||||
}];
|
||||
|
||||
async run(bot: Bot, args: number[]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 1 || !Number.isSafeInteger(args[0])
|
||||
|| args[0] < 0 || args[0] > 8) return false;
|
||||
try {
|
||||
bot.setQuickBarSlot(args[0]);
|
||||
return bot.quickBarSlot === args[0];
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
|
||||
export class init implements BaseFunction<[]> {
|
||||
readonly name = 'close_current_window';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '关闭当前窗口', description: '关闭当前打开的容器窗口;没有打开窗口时直接成功',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [];
|
||||
|
||||
async run(bot: Bot, args: []): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 0) return false;
|
||||
const window = bot.currentWindow;
|
||||
if (!window) return true;
|
||||
try {
|
||||
await bot.closeWindow(window);
|
||||
return bot.currentWindow == null;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {base_define} from "../../../types/bot/plugins/plugin.ts";
|
||||
|
||||
export const define: base_define = {
|
||||
path: "inventory",
|
||||
i18n: [
|
||||
{
|
||||
lang: "zh-CN",
|
||||
name: "容器操作",
|
||||
description: "Bot 容器操作, 提供容器相关功能"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
|
||||
export class init implements BaseFunction<number[]> {
|
||||
readonly name = 'left_click_slot';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '左键点击槽位', description: '在当前窗口左键点击一个物品槽位,使用原生拾取、放置或合并行为',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [{
|
||||
name: 'slot', type: 'number',
|
||||
i18n: {lang: 'zh-CN', name: '物品槽位', description: '当前窗口中的槽位编号'},
|
||||
}];
|
||||
|
||||
async run(bot: Bot, args: number[]): Promise<boolean> {
|
||||
const window = bot.currentWindow ?? bot.inventory;
|
||||
if (!window || !Array.isArray(args) || args.length !== 1 || !Number.isSafeInteger(args[0])
|
||||
|| args[0] < 0 || args[0] >= window.slots.length) return false;
|
||||
try {
|
||||
await bot.clickWindow(args[0], 0, 0);
|
||||
return (bot.currentWindow ?? bot.inventory) === window;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
|
||||
export class init implements BaseFunction<number[]> {
|
||||
readonly name = 'right_click_slot';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '右键点击槽位', description: '在当前窗口右键点击一个物品槽位,使用原生半组拾取或单个放置行为',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [{
|
||||
name: 'slot', type: 'number',
|
||||
i18n: {lang: 'zh-CN', name: '物品槽位', description: '当前窗口中的槽位编号'},
|
||||
}];
|
||||
|
||||
async run(bot: Bot, args: number[]): Promise<boolean> {
|
||||
const window = bot.currentWindow ?? bot.inventory;
|
||||
if (!window || !Array.isArray(args) || args.length !== 1 || !Number.isSafeInteger(args[0])
|
||||
|| args[0] < 0 || args[0] >= window.slots.length) return false;
|
||||
try {
|
||||
await bot.clickWindow(args[0], 1, 0);
|
||||
return (bot.currentWindow ?? bot.inventory) === window;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
import {Mutex} from 'async-mutex';
|
||||
import {cancellation_generation} from '../cancellation.ts';
|
||||
|
||||
const locks = new WeakMap<Bot, Mutex>();
|
||||
|
||||
export class init implements BaseFunction<number[]> {
|
||||
readonly name = 'slot_switch_slot';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '物品交换', description: '依次左键点击源槽位、目标槽位,并将鼠标剩余物品放回源槽位;同类物品按原生规则合并',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [
|
||||
{name: 'slot', type: 'number', i18n: {lang: 'zh-CN', name: '物品槽位', description: '第一个槽位编号'}},
|
||||
{name: 'new_slot', type: 'number', i18n: {lang: 'zh-CN', name: '目标槽位', description: '第二个槽位编号'}},
|
||||
];
|
||||
|
||||
async run(bot: Bot, args: number[]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 2 || !args.every(Number.isSafeInteger)) return false;
|
||||
const window = bot.currentWindow ?? bot.inventory;
|
||||
if (!window) return false;
|
||||
const generation = cancellation_generation(bot);
|
||||
const active = () => cancellation_generation(bot) === generation;
|
||||
let lock = locks.get(bot);
|
||||
if (!lock) locks.set(bot, lock = new Mutex());
|
||||
return lock.runExclusive(async () => {
|
||||
const [slot, new_slot] = args;
|
||||
if (!active() || (bot.currentWindow ?? bot.inventory) !== window || window.selectedItem
|
||||
|| slot === new_slot || args.some(value => value < 0 || value >= window.slots.length)) return false;
|
||||
try {
|
||||
await bot.clickWindow(slot, 0, 0);
|
||||
if (!active() || (bot.currentWindow ?? bot.inventory) !== window) return false;
|
||||
await bot.clickWindow(new_slot, 0, 0);
|
||||
if (!active() || (bot.currentWindow ?? bot.inventory) !== window) return false;
|
||||
// An empty cursor means a final click would pick the source item up again.
|
||||
if (window.selectedItem) {
|
||||
await bot.clickWindow(slot, 0, 0);
|
||||
}
|
||||
return active() && (bot.currentWindow ?? bot.inventory) === window && !window.selectedItem;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type {Bot} from "@wp2508/mineflayer";
|
||||
import type {BaseFunction, FunctionArgument} from "../../../types/bot/function/function.ts";
|
||||
import {Mutex} from "async-mutex";
|
||||
import {cancellation_generation} from '../cancellation.ts';
|
||||
|
||||
const locks = new WeakMap<Bot, Mutex>();
|
||||
|
||||
export class init implements BaseFunction<number[]> {
|
||||
public name = "slot_to_slot";
|
||||
public i18n = {
|
||||
lang: "zh-CN",
|
||||
name: "物品移动",
|
||||
description: "将物品从一个槽位移动到另一个槽位, 目标槽位必须为空"
|
||||
}
|
||||
public args: FunctionArgument[] = [
|
||||
{
|
||||
name: "slot",
|
||||
type: "number",
|
||||
i18n: {
|
||||
lang: "zh-CN",
|
||||
name: "物品槽位",
|
||||
description: "物品槽位"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "new_slot",
|
||||
type: "number",
|
||||
i18n: {
|
||||
lang: "zh-CN",
|
||||
name: "新物品槽位",
|
||||
description: "新物品槽位"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
async run(bot: Bot, args: number[]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 2 || !args.every(Number.isSafeInteger)) {
|
||||
return false;
|
||||
}
|
||||
const [slot, new_slot] = args;
|
||||
const window = bot.currentWindow ?? bot.inventory;
|
||||
if (!window) return false;
|
||||
const generation = cancellation_generation(bot);
|
||||
const active = () => cancellation_generation(bot) === generation;
|
||||
let lock = locks.get(bot);
|
||||
if (!lock) locks.set(bot, lock = new Mutex());
|
||||
return lock.runExclusive(async () => {
|
||||
if (!active() || (bot.currentWindow ?? bot.inventory) !== window) return false;
|
||||
if ([slot, new_slot].some(value => value < 0 || value >= window.slots.length)) {
|
||||
return false;
|
||||
}
|
||||
if (slot === new_slot || !window.slots[slot] || window.slots[new_slot] || window.selectedItem) return false;
|
||||
|
||||
try {
|
||||
await bot.clickWindow(slot, 0, 0);
|
||||
// Never apply the destination click to a replacement window.
|
||||
if (!active() || (bot.currentWindow ?? bot.inventory) !== window || window.slots[new_slot]) return false;
|
||||
await bot.clickWindow(new_slot, 0, 0);
|
||||
return active();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import {base_define} from "../../../types/bot/plugins/plugin.ts";
|
||||
|
||||
export const define: base_define = {
|
||||
path: "item",
|
||||
i18n: [
|
||||
{
|
||||
lang: "zh-CN",
|
||||
name: "物品操作",
|
||||
description: "Bot 物品操作, 提供物品相关功能"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
|
||||
export class init implements BaseFunction<number[]> {
|
||||
readonly name = 'drop_slot_all_item';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '丢弃整组物品', description: '丢弃当前窗口指定槽位的整组物品,不主动关闭窗口',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [{
|
||||
name: 'slot', type: 'number',
|
||||
i18n: {lang: 'zh-CN', name: '物品槽位', description: '当前窗口中的槽位编号'},
|
||||
}];
|
||||
|
||||
async run(bot: Bot, args: number[]): Promise<boolean> {
|
||||
const window = bot.currentWindow ?? bot.inventory;
|
||||
if (!window || !Array.isArray(args) || args.length !== 1 || !Number.isSafeInteger(args[0])
|
||||
|| args[0] < 0 || args[0] >= window.slots.length || !window.slots[args[0]]
|
||||
|| window.selectedItem) return false;
|
||||
try {
|
||||
await bot.clickWindow(args[0], 1, 4);
|
||||
return (bot.currentWindow ?? bot.inventory) === window && !window.slots[args[0]];
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
|
||||
export class init implements BaseFunction<number[]> {
|
||||
readonly name = 'drop_slot_item_number';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '丢弃指定数量物品', description: '逐个丢弃指定槽位的物品,数量不超过槽位现有数量',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [
|
||||
{name: 'slot', type: 'number', i18n: {lang: 'zh-CN', name: '物品槽位', description: '当前窗口中的槽位编号'}},
|
||||
{name: 'count', type: 'number', i18n: {lang: 'zh-CN', name: '丢弃数量', description: '大于零的整数,超过现有数量时丢弃全部'}},
|
||||
];
|
||||
|
||||
async run(bot: Bot, args: number[]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 2 || !args.every(Number.isSafeInteger)) return false;
|
||||
const [slot, count] = args;
|
||||
const window = bot.currentWindow ?? bot.inventory;
|
||||
if (!window || slot < 0 || slot >= window.slots.length || count <= 0 || window.selectedItem) return false;
|
||||
const item = window.slots[slot];
|
||||
if (!item) return false;
|
||||
const drop_count = Math.min(count, item.count);
|
||||
const initial_count = item.count;
|
||||
const type = item.type;
|
||||
const metadata = item.metadata;
|
||||
try {
|
||||
for (let index = 0; index < drop_count; index++) {
|
||||
const current = window.slots[slot];
|
||||
if ((bot.currentWindow ?? bot.inventory) !== window || window.selectedItem
|
||||
|| !current || current.type !== type || current.metadata !== metadata
|
||||
|| current.count !== initial_count - index) return false;
|
||||
await bot.clickWindow(slot, 0, 4);
|
||||
if ((bot.currentWindow ?? bot.inventory) !== window || window.selectedItem) return false;
|
||||
const remaining = window.slots[slot];
|
||||
const expected = initial_count - index - 1;
|
||||
if (expected === 0 ? remaining !== null && remaining !== undefined
|
||||
: !remaining || remaining.type !== type || remaining.metadata !== metadata
|
||||
|| remaining.count !== expected) return false;
|
||||
}
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionArgument} from '../../../types/bot/function/function.ts';
|
||||
|
||||
export class init implements BaseFunction<boolean[]> {
|
||||
readonly name = 'use_item';
|
||||
readonly i18n = {
|
||||
lang: 'zh-CN', name: '使用手持物品', description: '发起主手或副手物品的使用,不等待使用完成,也不自动停止持续使用',
|
||||
};
|
||||
readonly args: FunctionArgument[] = [{
|
||||
name: 'off_hand', type: 'boolean',
|
||||
i18n: {lang: 'zh-CN', name: '使用副手', description: 'false 使用主手,true 使用副手'},
|
||||
}];
|
||||
|
||||
async run(bot: Bot, args: boolean[]): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 1 || typeof args[0] !== 'boolean') return false;
|
||||
try {
|
||||
const off_hand = args[0];
|
||||
if (off_hand) {
|
||||
if (bot.supportFeature('doesntHaveOffHandSlot') || !bot.inventory?.slots[45]) return false;
|
||||
} else if (!bot.heldItem) return false;
|
||||
bot.activateItem(off_hand);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type {base_define} from '../../../types/bot/plugins/plugin.ts';
|
||||
|
||||
export const define: base_define = {
|
||||
path: 'workflow',
|
||||
i18n: [{lang: 'zh-CN', name: '工作流控制', description: '运行、停止、管理工作流触发器和重新加载工作流'}],
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import {WorkflowControlFunction} from './shared.ts';
|
||||
|
||||
export class init extends WorkflowControlFunction {
|
||||
readonly name = 'reload';
|
||||
readonly i18n = {lang: 'zh-CN', name: '重新加载工作流', description: '提交重新加载指定工作流的请求,接受后立即返回,不等待加载完成'};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {WorkflowControlFunction} from './shared.ts';
|
||||
|
||||
export class init extends WorkflowControlFunction {
|
||||
readonly name = 'run_once';
|
||||
readonly i18n = {lang: 'zh-CN', name: '运行工作流一次', description: '提交工作流单次运行请求,接受后立即返回,不等待运行完成'};
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import {randomUUID} from 'node:crypto';
|
||||
import type {Bot} from '@wp2508/mineflayer';
|
||||
import type {BaseFunction, FunctionExecutionContext, FunctionI18n} from '../../../types/bot/function/function.ts';
|
||||
import type {WorkflowAction} from '../../../types/bot/workflow/runtime.ts';
|
||||
|
||||
export abstract class WorkflowControlFunction implements BaseFunction<[string]> {
|
||||
abstract readonly name: WorkflowAction;
|
||||
abstract readonly i18n: FunctionI18n;
|
||||
readonly execution = 'workflow_control';
|
||||
readonly args = [{name: 'workflow_id', type: 'string' as const,
|
||||
i18n: {lang: 'zh-CN', name: '工作流 ID', description: '目标工作流的唯一标识'}}];
|
||||
|
||||
async run(_bot: Bot, args: [string], executionContext?: FunctionExecutionContext): Promise<boolean> {
|
||||
if (!Array.isArray(args) || args.length !== 1 || typeof args[0] !== 'string'
|
||||
|| args[0].length < 1 || args[0].length > 100 || /[^a-zA-Z0-9_-]/.test(args[0])
|
||||
|| ['__proto__', 'prototype', 'constructor'].includes(args[0])) return false;
|
||||
const workflow = executionContext?.workflow;
|
||||
if (!workflow) return false;
|
||||
try {
|
||||
const command = workflow.control({command_id: randomUUID(), action: this.name,
|
||||
...(this.name === 'stop_run' ? {run_id: args[0]} : {workflow_id: args[0]})});
|
||||
// Acknowledgement is enough; waiting for completion would deadlock a self-stop.
|
||||
return command.state === 'accepted' || command.state === 'completed';
|
||||
} catch { return false; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {WorkflowControlFunction} from './shared.ts';
|
||||
|
||||
export class init extends WorkflowControlFunction {
|
||||
readonly name = 'stop';
|
||||
readonly i18n = {lang: 'zh-CN', name: '停止工作流', description: '提交停止指定工作流的请求,接受后立即返回,不等待运行结束'};
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import {WorkflowControlFunction} from './shared.ts';
|
||||
|
||||
export class init extends WorkflowControlFunction {
|
||||
readonly name = 'stop_run';
|
||||
readonly i18n = {lang: 'zh-CN', name: '停止工作流运行实例', description: '提交停止指定运行实例的请求,接受后立即返回,不等待实例结束'};
|
||||
readonly args = [{name: 'run_id', type: 'string' as const,
|
||||
i18n: {lang: 'zh-CN', name: '运行实例 ID', description: '目标工作流运行实例的唯一标识'}}];
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {WorkflowControlFunction} from './shared.ts';
|
||||
|
||||
export class init extends WorkflowControlFunction {
|
||||
readonly name = 'trigger_start';
|
||||
readonly i18n = {lang: 'zh-CN', name: '启动工作流触发器', description: '提交启动指定工作流触发器的请求,接受后立即返回'};
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import {WorkflowControlFunction} from './shared.ts';
|
||||
|
||||
export class init extends WorkflowControlFunction {
|
||||
readonly name = 'trigger_stop';
|
||||
readonly i18n = {lang: 'zh-CN', name: '停止工作流触发器', description: '提交停止指定工作流触发器的请求,接受后立即返回'};
|
||||
}
|
||||
Reference in New Issue
Block a user