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