chore: initial commit
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
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');
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user