chore: initial commit

This commit is contained in:
2026-10-22 17:07:08 +08:00
commit 0acef64f48
200 changed files with 35546 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
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 {init: InventoryInfo} = await tsImport('../bot/info/bot/inventory.ts', import.meta.url);
const {create_info_manager} = await tsImport('../bot/info/init.ts', import.meta.url);
const require = createRequire(import.meta.url);
const mf = createRequire(require.resolve('@wp2508/mineflayer'));
const registry = mf('prismarine-registry')('1.21.4');
const windows = mf('prismarine-windows')(registry);
const Item = mf('prismarine-item')(registry);
function createBot() {
const bot = new EventEmitter();
bot._client = new EventEmitter();
bot.inventory = windows.createWindow(0, 'minecraft:inventory', 'Inventory');
bot.currentWindow = null;
bot.quickBarSlot = 2;
bot.registry = registry;
return bot;
}
test('inventory snapshots cover all nine window kinds and special properties without operations', t => {
t.mock.timers.enable({apis: ['setTimeout']});
const bot = createBot();
const info = new InventoryInfo(bot);
t.after(() => info.dispose());
const tick = () => t.mock.timers.tick(50);
const open = (type, id = 1) => {
const window = windows.createWindow(id, `minecraft:${type}`, type);
bot.currentWindow = window;
bot.emit('windowOpen', window);
tick();
return window;
};
const property = (id, value) => bot._client.emit('craft_progress_bar', {
windowId: bot.currentWindow.id, property: id, value,
});
assert.equal(info.get_info().window.kind, 'inventory');
assert.equal(info.get_info().handSlot, 38);
assert.equal(bot.listenerCount('end'), 0);
for (const [type, kind, handSlot] of [
['generic_9x3', 'chest', 56],
['generic_9x6', 'large_chest', 83],
['crafting', 'crafting', 39],
['enchantment', 'enchantment', 31],
['stonecutter', 'stonecutter', 31],
['furnace', 'furnace', 32],
['merchant', 'merchant', 32],
['lectern', 'lectern', null],
]) {
const previous = bot.currentWindow;
const window = open(type);
assert.equal(info.get_info().window.kind, kind);
assert.equal(info.get_info().handSlot, handSlot);
if (previous) assert.equal(previous.listenerCount('updateSlot'), 0);
if (kind === 'lectern') {
assert.equal(info.get_info().window.hasPlayerInventory, false);
assert.equal(info.get_info().window.inventoryStart, null);
assert.equal(info.get_info().window.slots.length, 1);
}
assert.equal(window.listenerCount('updateSlot'), 1);
}
open('furnace');
assert.equal(info.get_info().special.furnace.fuelRatio, null);
property(0, 40);
property(1, 80);
property(2, 50);
property(3, 200);
tick();
assert.equal(info.get_info().special.furnace.fuelRatio, 0.5);
assert.equal(info.get_info().special.furnace.progressRatio, 0.25);
open('furnace');
assert.deepEqual(info.get_info().window.properties, {});
// Properties can arrive before windowOpen, while Mineflayer awaits slot contents.
bot.currentWindow = windows.createWindow(2, 'minecraft:enchantment', 'Enchant');
property(0, 30);
property(4, 5);
property(7, 3);
bot.emit('windowOpen', bot.currentWindow);
tick();
const option = info.get_info().special.enchantment.options[0];
assert.deepEqual(option, {index: 0, requiredLevel: 30, levelCost: 1, lapisCost: 1, enchantmentId: 5, enchantmentLevel: 3});
assert.equal(info.get_info().special.enchantment.options[1].requiredLevel, null);
const merchant = open('merchant');
assert.equal(info.get_info().special.merchant.ready, false);
merchant.trades = [{
inputItem1: new Item(registry.itemsByName.emerald.id, 2),
outputItem: new Item(registry.itemsByName.bread.id, 1),
hasItem2: false, realPrice: 1, tradeDisabled: false, nbTradeUses: 0, maximumNbTradeUses: 12,
}];
bot._client.emit('trade_list');
tick();
const offers = info.get_info().special.merchant.offers;
assert.equal(offers[0].input1.type, registry.itemsByName.emerald.id);
assert.equal(offers[0].realPrice, 1);
assert.equal(offers[0].input2, null);
const lectern = open('lectern');
const book = new Item(registry.itemsByName.written_book.id, 1);
book.components = [{type: 'written_book_content', data: {
rawTitle: 'Title', author: 'Author', pages: [{content: {
type: 'compound', value: {text: {type: 'string', value: 'Page'}, color: {type: 'string', value: 'red'}},
}}],
}}];
lectern.updateSlot(0, book);
property(0, 0);
tick();
assert.equal(info.get_info().special.lectern.book.title, 'Title');
assert.equal(info.get_info().special.lectern.book.pages[0][0].message, 'Page');
assert.equal(info.get_info().special.lectern.book.pages[0][0].font_color, '#FF5555');
assert.equal(info.get_info().special.lectern.page, 0);
const oldBook = {
type: book.type, name: 'written_book', displayName: 'Written Book', count: 1, metadata: 0,
nbt: {type: 'compound', value: {
title: {type: 'string', value: 'Legacy'}, author: {type: 'string', value: 'Writer'},
pages: {type: 'list', value: {type: 'string', value: ['{"text":"Old page"}']}},
long: {type: 'long', value: 123n}, bytes: Buffer.from([1, 2]),
}},
};
lectern.slots[0] = oldBook;
lectern.emit('updateSlot');
tick();
assert.equal(info.get_info().special.lectern.book.pages[0][0].message, 'Old page');
assert.equal(info.get_info().window.slots[0].details.extra.legacy.long, '123');
assert.deepEqual(info.get_info().window.slots[0].details.extra.legacy.bytes, [1, 2]);
assert.equal('nbt' in info.get_info().window.slots[0], false);
assert.doesNotThrow(() => JSON.stringify(info.get_info()));
assert.equal('icon' in info.get_info().window.slots[0], false);
const cyclic = {number: 1n};
cyclic.self = cyclic;
Object.defineProperty(cyclic, 'accessor', {enumerable: true, get() {throw new Error('must not invoke');}});
oldBook.components = [{type: 'custom_data', data: cyclic}];
lectern.emit('updateSlot');
tick();
const data = info.get_info().window.slots[0].details.extra.custom_data;
assert.deepEqual(data, {number: '1', self: null, accessor: null});
assert.equal(info.get_info().window.slots[0].truncated, true);
bot.currentWindow = null;
bot.emit('windowClose', lectern);
tick();
assert.equal(info.get_info().window.kind, 'inventory');
assert.deepEqual(info.get_info().special, {});
assert.equal(lectern.listenerCount('updateSlot'), 0);
let updates = 0;
const unsubscribe = info.on_update(() => updates++);
bot.inventory.selectedItem = new Item(registry.itemsByName.stone.id, 3);
bot._client.emit('set_slot');
tick();
assert.equal(info.get_info().cursorItem.count, 3);
assert.equal(info.get_info().cursorItem.slot, null);
bot.emit('physicsTick');
tick();
assert.equal(updates, 1);
unsubscribe();
bot.emit('physicsTick');
info.dispose();
tick();
assert.equal(bot.eventNames().length, 0);
assert.equal(bot._client.eventNames().length, 0);
assert.equal(bot.inventory.listenerCount('updateSlot'), 0);
assert.throws(() => info.on_update(() => {}), /已销毁/);
});
test('the real info manager loads inventory and owns its cleanup', async () => {
const bot = createBot();
const manager = await create_info_manager(bot);
assert.equal(bot.listenerCount('end'), 1);
assert.equal(manager.get_info('inventory').value.window.kind, 'inventory');
assert.ok(manager.get_info('inventory').i18n.fields.window.fields.slots.fields.type);
bot.emit('end');
manager.dispose();
assert.equal(bot.eventNames().length, 0);
assert.equal(bot._client.eventNames().length, 0);
assert.equal(bot.inventory.listenerCount('updateSlot'), 0);
});