import assert from 'node:assert/strict'; import {EventEmitter} from 'node:events'; import {createRequire} from 'node:module'; import test from 'node:test'; import {tsImport} from 'tsx/esm/api'; const names = ['destroy', 'place', 'use'].flatMap(action => ['cursor', 'nearest', 'position', 'position_group'].map(target => `${action}_${target}_block`)); const operations = Object.fromEntries(await Promise.all(names.map(async name => { const {init} = await tsImport(`../bot/function/block/${name}.ts`, import.meta.url); return [name, () => new init()]; }))); const require = createRequire(import.meta.url); const mf = createRequire(require.resolve('@wp2508/mineflayer')); const {Vec3} = mf('vec3'); function fixture() { const bot = new EventEmitter(); bot.entity = {position: new Vec3(0, 0, 0), eyeHeight: 1.62, height: 1.8, isValid: true}; bot._client = Object.assign(new EventEmitter(), {state: 'play'}); bot.heldItem = {name: 'stone'}; const block = {name: 'stone', type: 1, stateId: 1, position: new Vec3(2, 1, 0), boundingBox: 'block', shapes: [[0, 0, 0, 1, 1, 1]], diggable: true}; const blocks = new Map([[block.position.toString(), block]]); bot.blockAt = position => blocks.get(position.toString()) ?? null; bot.findBlocks = options => [...blocks.values()].filter(options.matching).map(block => block.position); bot.blockAtCursor = reach => { bot.cursorReach = reach; return {...block, face: 4, intersect: new Vec3(2, 1.25, 0.5)}; }; bot.calls = []; bot.looks = []; bot.lookAt = async point => { bot.looks.push(point); }; bot.canDigBlock = () => true; bot.dig = async (...args) => { bot.calls.push(['dig', ...args]); }; bot.placeBlock = async (...args) => { bot.calls.push(['place', ...args]); }; bot._placeBlockWithOptions = async (...args) => { bot.calls.push(['place', ...args]); }; bot.activateBlock = async (...args) => { bot.calls.push(['use', ...args]); }; bot.stopDigging = () => { bot.stops = (bot.stops ?? 0) + 1; bot.targetDigBlock = null; }; return {bot, block, blocks}; } test('position operations pass correct faces and await API completion', async () => { const faces = [[0, -1, 0], [0, 1, 0], [0, 0, -1], [0, 0, 1], [-1, 0, 0], [1, 0, 0]]; for (const action of ['destroy', 'place', 'use']) { for (let face = 0; face < 6; face++) { const {bot, block} = fixture(); assert.equal(await operations[`${action}_position_block`]().run(bot, [2, 1, 0, face]), true); assert.equal(bot.calls[0][1], block); const vector = bot.calls[0][action === 'destroy' ? 3 : 2]; assert.deepEqual([vector.x, vector.y, vector.z], faces[face]); } } }); test('cursor operations preserve the hit face and surface offset', async () => { for (const action of ['destroy', 'place', 'use']) { const {bot} = fixture(); assert.equal(await operations[`${action}_cursor_block`]().run(bot, [4.5]), true); assert.equal(bot.cursorReach, 4.5); const call = bot.calls[0]; const vector = call[action === 'destroy' ? 3 : 2]; assert.deepEqual([vector.x, vector.y, vector.z], [-1, 0, 0]); if (action === 'destroy') assert.equal(call[2], 'ignore'); if (action === 'place') { assert.equal(call[3].forceLook, 'ignore'); assert.deepEqual([call[3].delta.x, call[3].delta.y, call[3].delta.z], [0, 0.25, 0.5]); } if (action === 'use') { assert.equal(call[4], 'ignore'); assert.deepEqual([call[3].x, call[3].y, call[3].z], [0, 0.25, 0.5]); } } }); test('nearest operations match partial type IDs and choose the closest loaded block', async () => { for (const action of ['destroy', 'place', 'use']) { const {bot, block, blocks} = fixture(); blocks.set('3,1,0', {...block, position: new Vec3(3, 1, 0)}); assert.equal(await operations[`${action}_nearest_block`]().run(bot, ['MINECRAFT:STO', 4.5, 1]), true); assert.equal(bot.calls[0][1], block); assert.equal(await operations[`${action}_nearest_block`]().run(bot, ['dirt', 4.5, 1]), false); } }); test('all block functions default to no aim and disable internal aiming', async () => { for (const name of names) { for (const flag of [[], [false], [true]]) { const {bot} = fixture(); const args = name.includes('group') ? [[2, 1, 0, 4]] : name.includes('position') ? [2, 1, 0, 4] : name.includes('nearest') ? ['stone', 4.5, 4] : [4.5]; bot.placeBlock = () => { assert.fail('public placement may aim internally'); }; assert.equal(await operations[name]().run(bot, [...args, ...flag]), true, name); assert.equal(bot.looks.length, flag[0] === true ? 1 : 0, name); if (flag[0]) assert.deepEqual(bot.looks[0], new Vec3(2, 1.5, 0.5)); const call = bot.calls[0]; if (name.startsWith('destroy')) assert.equal(call[2], 'ignore'); if (name.startsWith('place')) { assert.equal(call[3].forceLook, 'ignore'); assert.equal(call[3].swingArm, 'right'); if (!name.includes('cursor')) assert.equal(call[3].delta, undefined); } if (name.startsWith('use')) assert.equal(call[4], 'ignore'); } } }); test('groups await a separate face-center look before every operation on all faces', async () => { const faces = [[0, -1, 0], [0, 1, 0], [0, 0, -1], [0, 0, 1], [-1, 0, 0], [1, 0, 0]]; for (const action of ['destroy', 'place', 'use']) { const {bot, block} = fixture(); const events = []; bot.lookAt = async point => { events.push('look'); await Promise.resolve(); bot.looks.push(point); events.push('looked'); }; const record = async () => { events.push('operate'); }; bot.dig = bot._placeBlockWithOptions = bot.activateBlock = record; assert.equal(await operations[`${action}_position_group_block`]().run(bot, [faces.flatMap((_, face) => [2, 1, 0, face]), true]), true); assert.deepEqual(events, faces.flatMap(() => ['look', 'looked', 'operate'])); assert.deepEqual(bot.looks, faces.map(face => block.position.offset(0.5, 0.5, 0.5) .plus(new Vec3(...face).scaled(0.5)))); } }); test('operations revalidate after aiming and clean up on cancellation or look failure', async () => { for (const name of names) { const changes = [ ({bot}) => bot.emit('end'), ({bot}) => { bot.entity = {...bot.entity}; }, ({bot}) => { bot.entity.position = new Vec3(100, 0, 0); }, ({blocks}) => blocks.clear(), ({block, blocks}) => blocks.set(block.position.toString(), {...block, type: 2}), ({block, blocks}) => blocks.set(block.position.toString(), {...block, stateId: 2}), () => { throw Error('look failed'); }, ]; if (name.startsWith('place')) changes.push(({bot}) => { bot.heldItem = null; }); if (name.startsWith('destroy')) changes.push(({bot}) => { bot.canDigBlock = () => false; }); for (const change of changes) { const context = fixture(); const {bot} = context; bot.lookAt = async () => { change(context); }; const args = name.includes('group') ? [[2, 1, 0, 1]] : name.includes('position') ? [2, 1, 0, 1] : name.includes('nearest') ? ['stone', 4.5, 1] : [4.5]; assert.equal(await operations[name]().run(bot, [...args, true]), false, name); assert.equal(bot.calls.length, 0, name); assert.equal(bot.listenerCount('end'), 0, name); } } }); test('dig ownership covers pending explicit aim and is released on disconnect', async () => { const {bot} = fixture(); let finishLook; bot.lookAt = () => new Promise(resolve => { finishLook = resolve; }); const fn = operations.destroy_position_block(); const first = fn.run(bot, [2, 1, 0, 1, true]); assert.equal(await fn.run(bot, [2, 1, 0, 1]), false); bot.emit('end'); assert.equal(await first, false); finishLook(); await Promise.resolve(); assert.equal(bot.calls.length, 0); assert.equal(bot.stops, undefined); assert.equal(await fn.run(bot, [2, 1, 0, 1]), true); }); test('groups prevalidate every tuple, execute in order and stop on a failure', async () => { for (const action of ['destroy', 'place', 'use']) { const {bot, block, blocks} = fixture(); const second = {...block, position: new Vec3(3, 1, 0)}; blocks.set(second.position.toString(), second); const fn = operations[`${action}_position_group_block`](); assert.equal(await fn.run(bot, [[2, 1, 0, 1, 3, 1, 0, 7]]), false); assert.equal(bot.calls.length, 0); assert.equal(await fn.run(bot, [[2, 1, 0, 1, 3, 1, 0, 1]]), true); assert.deepEqual(bot.calls.map(call => call[1].position.x), [2, 3]); bot.calls.length = 0; blocks.delete(second.position.toString()); assert.equal(await fn.run(bot, [[2, 1, 0, 1, 3, 1, 0, 1]]), false); assert.equal(bot.calls.length, 1); } }); test('block operations reject invalid parameters, missing targets and API errors', async () => { for (const name of names) { const {bot} = fixture(); const args = name.includes('group') ? [[2, 1, 0, 1]] : name.includes('position') ? [2, 1, 0, 1] : name.includes('nearest') ? ['stone', 4.5, 1] : [4.5]; const fn = operations[name](); for (const invalid of [[], null, [...args, 1], [...args, undefined], [...args, false, true]]) assert.equal(await fn.run(bot, invalid), false, name); const fail = async () => { throw Error('failed'); }; bot.dig = bot.placeBlock = bot._placeBlockWithOptions = bot.activateBlock = fail; assert.equal(await fn.run(bot, args), false, name); } const {bot, block} = fixture(); for (const args of [[2.5, 1, 0, 1], [2, 1, 0, 6], [20, 1, 0, 1]]) assert.equal(await operations.use_position_block().run(bot, args), false); bot.heldItem = null; assert.equal(await operations.place_position_block().run(bot, [2, 1, 0, 1]), false); bot.canDigBlock = () => false; assert.equal(await operations.destroy_position_block().run(bot, [2, 1, 0, 1]), false); block.name = 'air'; block.type = 0; assert.equal(await operations.use_position_block().run(bot, [2, 1, 0, 1]), false); }); test('owned digging stops on disconnect and releases operation listeners', async () => { const {bot} = fixture(); bot.dig = async block => { bot.targetDigBlock = block; bot.emit('end', 'disconnected'); }; assert.equal(await operations.destroy_position_block().run(bot, [2, 1, 0, 1]), false); assert.equal(bot.stops, 1); assert.equal(bot.listenerCount('end'), 0); }); test('digging does not overlap and releases ownership after rejection', async () => { const {bot} = fixture(); let release; bot.dig = () => new Promise(resolve => { release = resolve; }); const first = operations.destroy_position_block().run(bot, [2, 1, 0, 1]); for (let i = 0; i < 10; i++) await Promise.resolve(); assert.equal(await operations.destroy_position_block().run(bot, [2, 1, 0, 1]), false); release(); assert.equal(await first, true); bot.dig = async () => { throw Error('rejected'); }; assert.equal(await operations.destroy_position_block().run(bot, [2, 1, 0, 1]), false); bot.dig = async () => {}; assert.equal(await operations.destroy_position_block().run(bot, [2, 1, 0, 1]), true); assert.equal(bot.listenerCount('end'), 0); }); test('groups stop after disconnect or bot instance replacement', async () => { for (const change of [bot => bot.emit('end'), bot => { bot.entity = {...bot.entity}; }]) { const {bot, block, blocks} = fixture(); const second = {...block, position: new Vec3(3, 1, 0)}; blocks.set(second.position.toString(), second); bot.activateBlock = async (...args) => { bot.calls.push(['use', ...args]); change(bot); }; assert.equal(await operations.use_position_group_block().run(bot, [[2, 1, 0, 1, 3, 1, 0, 1]]), false); assert.equal(bot.calls.length, 1); assert.equal(bot.listenerCount('end'), 0); } }); test('loader discovers all twelve block functions and valid metadata', async () => { const {get_function_list} = await tsImport('../bot/function/init.ts', import.meta.url); const category = (await get_function_list()).find(item => item.path === 'block'); assert.deepEqual(category.function_list.map(item => item.name).sort(), [...names].sort()); for (const fn of category.function_list) { const types = fn.name.includes('group') ? ['number-array'] : fn.name.includes('position') ? Array(4).fill('number') : fn.name.includes('nearest') ? ['string', 'number', 'number'] : ['number']; assert.deepEqual(fn.args.map(arg => arg.type), [...types, 'boolean']); assert.equal(fn.args.at(-1).name, 'need_look'); assert.equal(fn.args.at(-1).default, false); } });