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 = ['attack_cursor_entity', 'attack_nearest_entity', 'use_item_on_cursor_entity', 'use_item_on_nearest_entity', 'feed_cursor_entity', 'feed_nearest_entity', 'open_cursor_entity_container', 'open_nearest_entity_container', 'null_main_hand_right_cursor_entity', 'null_main_hand_right_nearest_entity']; const operations = Object.fromEntries(await Promise.all(names.map(async name => { const {init} = await tsImport(`../bot/function/entity/${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 = {id: 1, position: new Vec3(0, 0, 0), height: 1.8, eyeHeight: 1.62, isValid: true}; bot.entities = {1: bot.entity}; bot._client = Object.assign(new EventEmitter(), {state: 'play'}); bot.currentWindow = null; bot.inventory = {slots: []}; bot.heldItem = {name: 'wheat'}; bot.supportFeature = () => false; bot.looks = []; bot.attacks = []; bot.interactions = []; bot.lookAt = async point => { bot.looks.push(point); }; bot.attack = target => { bot.attacks.push(target); }; bot.interactEntity = (target, options) => { bot.interactions.push({target, options}); }; const target = {id: 2, name: 'cow', position: new Vec3(2, 0, 0), width: 0.9, height: 1.4, isValid: true}; bot.entities[2] = target; bot.entityAtCursor = (...args) => { bot.cursorArgs = args; return target; }; return {bot, target}; } test('nearest attack matches names and selects by eye-to-bounding-box distance', async () => { const {bot, target} = fixture(); const other = {id: 3, name: 'player', username: 'COW_keeper', isValid: true, position: new Vec3(2.2, 0, 0), width: 3, height: 2}; bot.entities[3] = other; assert.equal(await operations.attack_nearest_entity().run(bot, ['CoW', 1, false]), true); assert.deepEqual(bot.attacks, [other]); assert.equal(bot.looks.length, 0); other.isValid = false; assert.equal(await operations.attack_nearest_entity().run(bot, ['cow', 1, false]), false); assert.equal(await operations.attack_nearest_entity().run(bot, ['cow', 3, false]), true); assert.equal(bot.attacks.at(-1), target); for (const args of [['', 3, false], ['cow', 0, false], ['cow', Infinity, false], ['cow'], ['cow', 3]]) assert.equal(await operations.attack_nearest_entity().run(bot, args), false); }); test('cursor attack forwards reach and block options and rejects stale or distant targets', async () => { const {bot, target} = fixture(); assert.equal(await operations.attack_cursor_entity().run(bot, [3, false]), true); assert.deepEqual(bot.cursorArgs, [3, true, false]); target.position.x = 20; assert.equal(await operations.attack_cursor_entity().run(bot, [3, true]), false); target.position.x = 2; target.isValid = false; assert.equal(await operations.attack_cursor_entity().run(bot, [3, false]), false); assert.equal(bot.attacks.length, 1); }); test('all entity functions aim at the center only when need_look is true', async () => { for (const name of names) { for (const needLook of [undefined, false, true]) { const {bot, target} = fixture(); if (name.startsWith('null_main_hand')) bot.heldItem = null; const args = name.includes('cursor') ? [3, false] : ['cow', 3]; if (name.startsWith('feed') || name.startsWith('use_item')) args.push(false); if (name.includes('nearest')) args.push(false); if (needLook !== undefined) args.push(needLook); bot.activateEntity = () => { throw Error('automatic aim must not be used'); }; if (name.startsWith('open')) bot.interactEntity = (entity, options) => { assert.equal(entity, target); assert.deepEqual(options, {hand: 0}); bot.currentWindow = {id: 7}; bot.emit('windowOpen', bot.currentWindow); }; const label = `${name}: ${needLook}`; assert.equal(await operations[name]().run(bot, args), true, label); assert.deepEqual(bot.looks, needLook ? [target.position.offset(0, target.height / 2, 0)] : [], label); } } }); test('all entity functions revalidate after optional aiming before interaction', async () => { for (const name of names) { const {bot, target} = fixture(); if (name.startsWith('null_main_hand')) bot.heldItem = null; const args = name.includes('cursor') ? [3, false] : ['cow', 3]; if (name.startsWith('feed') || name.startsWith('use_item')) args.push(false); if (name.includes('nearest')) args.push(false); args.push(true); bot.lookAt = async () => { target.isValid = false; }; assert.equal(await operations[name]().run(bot, args), false, name); assert.deepEqual(bot.attacks, [], name); assert.deepEqual(bot.interactions, [], name); assert.equal(bot.listenerCount('windowOpen'), 0, name); } }); test('item interaction and feeding select hands and aim before interaction', async () => { for (const name of ['use_item_on_cursor_entity', 'feed_cursor_entity', 'use_item_on_nearest_entity', 'feed_nearest_entity']) { const {bot, target} = fixture(); const args = name.includes('cursor') ? [3, false, false, true] : ['cow', 4.5, false, false, true]; assert.equal(await operations[name]().run(bot, args), true); assert.equal(bot.interactions[0].target, target); assert.equal(bot.interactions[0].options.hand, 0); assert.ok(bot.interactions[0].options.position instanceof Vec3); assert.equal(bot.looks.length, 1); args[2] = true; assert.equal(await operations[name]().run(bot, args), false); bot.inventory.slots[45] = {name: 'wheat'}; assert.equal(await operations[name]().run(bot, args), true); assert.equal(bot.interactions.at(-1).options.hand, 1); bot.supportFeature = () => true; assert.equal(await operations[name]().run(bot, args), false); args[2] = false; bot.heldItem = null; assert.equal(await operations[name]().run(bot, args), false); } }); test('look completion rechecks entity validity, range and held items before side effects', async () => { for (const change of [ ({target}) => { target.isValid = false; }, ({target}) => { target.position.x = 30; }, ({bot}) => { delete bot.entities[2]; }, ({bot}) => { bot.entity = {...bot.entity}; }, ]) { const f = fixture(); f.bot.lookAt = async () => change(f); assert.equal(await operations.attack_nearest_entity().run(f.bot, ['cow', 3, false, true]), false); assert.equal(f.bot.attacks.length, 0); } const {bot} = fixture(); bot.lookAt = async () => { bot.heldItem = null; }; assert.equal(await operations.use_item_on_nearest_entity().run(bot, ['cow', 4.5, false, false, true]), false); assert.equal(bot.interactions.length, 0); }); test('entity container opening waits for windowOpen and cleans listeners', async () => { for (const name of ['open_cursor_entity_container', 'open_nearest_entity_container']) { const {bot} = fixture(); const args = name.includes('cursor') ? [3, false] : ['cow', 4.5, false]; bot.interactEntity = () => { bot.currentWindow = {id: 7}; bot.emit('windowOpen', bot.currentWindow); }; assert.equal(await operations[name]().run(bot, args), true); assert.equal(bot.listenerCount('windowOpen'), 0); assert.equal(bot.listenerCount('end'), 0); assert.equal(await operations[name]().run(bot, args), false); bot.currentWindow = null; bot.interactEntity = () => { throw Error('rejected'); }; assert.equal(await operations[name]().run(bot, args), false); assert.equal(bot.listenerCount('windowOpen'), 0); bot.interactEntity = () => { bot.emit('end', 'closed'); }; assert.equal(await operations[name]().run(bot, args), false); assert.equal(bot.listenerCount('windowOpen'), 0); } }); test('entity container opening times out and removes its listeners', async t => { t.mock.timers.enable({apis: ['setTimeout']}); const {bot} = fixture(); bot.interactEntity = () => {}; const pending = operations.open_cursor_entity_container().run(bot, [3, false]); for (let i = 0; i < 20; i++) await Promise.resolve(); t.mock.timers.tick(5000); assert.equal(await pending, false); assert.equal(bot.listenerCount('windowOpen'), 0); assert.equal(bot.listenerCount('end'), 0); }); test('entity functions reject malformed arguments and contain API failures', async () => { for (const name of names) { const {bot} = fixture(); const cursor = name.includes('cursor'); const item = name.startsWith('use_item') || name.startsWith('feed'); const args = cursor ? [3, false] : ['cow', 3]; if (item) args.push(false); if (!cursor) args.push(false); const fn = operations[name](); for (const invalid of [null, [], ...[undefined, null, 1, 'true', {}].map(value => [...args, value]), [...args, false, false], cursor ? [-1, false] : ['', 3]]) assert.equal(await fn.run(bot, invalid), false, name); if (!cursor) { for (const invalid of [args.slice(0, -1), ...[undefined, null, 0, 1, 'true'].map(lock => [...args.slice(0, -1), lock])]) assert.equal(await fn.run(bot, invalid), false, `${name}: mandatory boolean lock_target`); } bot.attack = () => { throw Error('failed'); }; bot.interactEntity = () => { throw Error('failed'); }; assert.equal(await fn.run(bot, args), false, name); } }); test('empty-main-hand interaction requires an empty hand before and after aiming', async () => { for (const name of names.filter(name => name.startsWith('null_main_hand'))) { const {bot, target} = fixture(); const args = name.includes('cursor') ? [3, false, true] : ['cow', 3, false, true]; const fn = operations[name](); assert.equal(await fn.run(bot, args), false); assert.equal(bot.looks.length, 0); assert.equal(bot.interactions.length, 0); bot.heldItem = null; bot.inventory.slots[45] = {name: 'shield'}; assert.equal(await fn.run(bot, args), true); assert.deepEqual(bot.interactions, [{target, options: {hand: 0}}]); assert.equal(bot.looks.length, 1); bot.lookAt = async () => { bot.heldItem = {name: 'wheat'}; }; assert.equal(await fn.run(bot, args), false); assert.equal(bot.interactions.length, 1); bot.heldItem = null; bot.lookAt = async () => { target.isValid = false; }; assert.equal(await fn.run(bot, args), false); assert.equal(bot.interactions.length, 1); } }); test('all nearest functions match partial type IDs with an optional Minecraft namespace', async () => { for (const name of names.filter(name => name.includes('nearest'))) { for (const query of ['co', 'MINECRAFT:CO', 'minecraft:cow']) { const {bot, target} = fixture(); if (name.startsWith('null_main_hand')) bot.heldItem = null; if (name.startsWith('open')) bot.interactEntity = entity => { assert.equal(entity, target); bot.currentWindow = {id: 7}; bot.emit('windowOpen', bot.currentWindow); }; const args = [query, 3]; if (name.startsWith('feed') || name.startsWith('use_item')) args.push(false); args.push(false); assert.equal(await operations[name]().run(bot, args), true, `${name}: ${query}`); } } const {bot, target} = fixture(); target.name = 'minecraft:zombie'; assert.equal(await operations.attack_nearest_entity().run(bot, ['zom', 3, false]), true); assert.equal(await operations.attack_nearest_entity().run(bot, ['minecraft:', 3, false]), false); }); const nearestNames = names.filter(name => name.includes('nearest')); function nearestArgs(name, {query = 'cow', reach = 5, offHand = false, lock = true, needLook} = {}) { const args = name.startsWith('feed') || name.startsWith('use_item') ? [query, reach, offHand, lock] : [query, reach, lock]; if (needLook !== undefined) args.push(needLook); return args; } function addCloser(bot) { const closer = {id: 3, name: 'cow', position: new Vec3(1, 0, 0), width: 0.9, height: 1.4, isValid: true}; bot.entities[closer.id] = closer; return closer; } async function runNearest(name, bot, options) { bot.heldItem = name.startsWith('null_main_hand') ? null : {name: 'wheat'}; bot.inventory.slots[45] = {name: 'wheat'}; bot.currentWindow = null; const acted = []; bot.attack = target => { acted.push(target); }; bot.interactEntity = target => { acted.push(target); if (name.startsWith('open')) { bot.currentWindow = {id: 7}; bot.emit('windowOpen', bot.currentWindow); } }; // Recreating init on every call verifies that lock state belongs to the bot and invocation key. const result = await operations[name]().run(bot, nearestArgs(name, options)); return {result, acted}; } test('all nearest locks survive init recreation and ignore a newly closer entity', async () => { for (const name of nearestNames) { const {bot, target} = fixture(); assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, name); addCloser(bot); assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, name); } }); test('all nearest locks share omitted and explicit false need_look keys', async () => { for (const name of nearestNames) { for (const firstLook of [undefined, false]) { const {bot, target} = fixture(); assert.deepEqual(await runNearest(name, bot, {needLook: firstLook}), {result: true, acted: [target]}, name); addCloser(bot); assert.deepEqual(await runNearest(name, bot, {needLook: firstLook === false ? undefined : false}), {result: true, acted: [target]}, name); assert.equal(bot.looks.length, 0, name); } } }); test('all nearest locks reacquire after target loss, invalidation or same-id object replacement', async () => { for (const name of nearestNames) { for (const reason of ['lost', 'invalid', 'replaced']) { const {bot, target} = fixture(); assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, `${name}: ${reason}`); const closer = addCloser(bot); if (reason === 'lost') delete bot.entities[target.id]; if (reason === 'invalid') target.isValid = false; if (reason === 'replaced') bot.entities[target.id] = {...target}; assert.deepEqual(await runNearest(name, bot), {result: true, acted: [closer]}, `${name}: ${reason}`); target.position.x = 0.5; if (reason === 'lost') bot.entities[target.id] = target; if (reason === 'invalid') target.isValid = true; assert.deepEqual(await runNearest(name, bot), {result: true, acted: [closer]}, `${name}: reacquired lock persists`); } } }); test('all nearest locks return false out of range and retain the original target', async () => { for (const name of nearestNames) { const {bot, target} = fixture(); assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, name); addCloser(bot); target.position.x = 20; const lookCount = bot.looks.length; assert.deepEqual(await runNearest(name, bot), {result: false, acted: []}, name); assert.equal(bot.looks.length, lookCount, `${name}: no aiming outside reach`); target.position.x = 2; assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, name); } }); test('all nearest functions with lock_target false select afresh without replacing a true lock', async () => { for (const name of nearestNames) { const {bot, target} = fixture(); assert.deepEqual(await runNearest(name, bot, {lock: false}), {result: true, acted: [target]}, name); assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, name); const closer = addCloser(bot); assert.deepEqual(await runNearest(name, bot, {lock: false}), {result: true, acted: [closer]}, name); closer.position.x = 4; assert.deepEqual(await runNearest(name, bot, {lock: false}), {result: true, acted: [target]}, name); closer.position.x = 1; assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, name); } }); test('all nearest locks isolate exact query, reach, off_hand and need_look argument values', async () => { for (const name of nearestNames) { const variations = [{query: 'COW'}, {query: ' cow '}, {query: 'minecraft:cow'}, {query: 'co'}, {reach: 6}, {needLook: true}]; if (name.startsWith('feed') || name.startsWith('use_item')) variations.push({offHand: true}); for (const variation of variations) { const {bot, target} = fixture(); const label = `${name}: ${JSON.stringify(variation)}`; assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, label); const closer = addCloser(bot); assert.deepEqual(await runNearest(name, bot, variation), {result: true, acted: [closer]}, label); assert.deepEqual(await runNearest(name, bot), {result: true, acted: [target]}, label); closer.position.x = 4; assert.deepEqual(await runNearest(name, bot, variation), {result: true, acted: [closer]}, label); } } }); test('nearest locks isolate function names, including feed and use_item sharing an operation', async () => { for (const first of nearestNames) { for (const second of nearestNames.filter(name => name !== first)) { const {bot, target} = fixture(); const label = `${first} / ${second}`; assert.deepEqual(await runNearest(first, bot), {result: true, acted: [target]}, label); const closer = addCloser(bot); assert.deepEqual(await runNearest(second, bot), {result: true, acted: [closer]}, label); assert.deepEqual(await runNearest(first, bot), {result: true, acted: [target]}, label); closer.position.x = 4; assert.deepEqual(await runNearest(second, bot), {result: true, acted: [closer]}, label); } } }); test('all nearest locks isolate bots with the same entity IDs and exact arguments', async () => { for (const name of nearestNames) { const first = fixture(); const second = fixture(); assert.deepEqual(await runNearest(name, first.bot), {result: true, acted: [first.target]}, name); addCloser(first.bot); assert.deepEqual(await runNearest(name, second.bot), {result: true, acted: [second.target]}, name); addCloser(second.bot); assert.deepEqual(await runNearest(name, first.bot), {result: true, acted: [first.target]}, name); assert.deepEqual(await runNearest(name, second.bot), {result: true, acted: [second.target]}, name); } }); test('dynamic loader publishes all entity functions with positional 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 === 'entity'); assert.deepEqual(category.function_list.map(item => item.name).sort(), [...names].sort()); for (const template of category.function_list) { const cursor = template.name.includes('cursor'); const item = template.name.startsWith('feed') || template.name.startsWith('use_item'); assert.deepEqual(template.args.map(arg => arg.type), [...(cursor ? ['number', 'boolean'] : ['string', 'number']), ...(item ? ['boolean'] : []), ...(cursor ? [] : ['boolean']), 'boolean']); if (!cursor) { assert.equal(template.args.at(-2).name, 'lock_target'); assert.equal(Object.hasOwn(template.args.at(-2), 'default'), false); } assert.equal(template.args.at(-1).name, 'need_look'); assert.equal(template.args.at(-1).default, false); assert.equal(template.args.at(-1).i18n.lang, 'zh-CN'); } });