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); });