const assert = require('assert') const { Vec3 } = require('vec3') const { once } = require('events') const { sleep, createDoneTask, createTask, withTimeout } = require('../promise_utils') module.exports = inject // ms to wait before clicking on a tool so the server can send the new // damage information const DIG_CLICK_TIMEOUT = 500 // The number of milliseconds to wait for the server to respond with consume completion. // This number is larger than the eat time of 1.61 seconds to account for latency and low tps. // The eat time comes from https://minecraft.fandom.com/wiki/Food#Usage const CONSUME_TIMEOUT = 2500 // milliseconds to wait for the server to respond to a window click transaction const WINDOW_TIMEOUT = 5000 const ALWAYS_CONSUMABLES = [ 'potion', 'milk_bucket', 'enchanted_golden_apple', 'golden_apple' ] function inject (bot, { hideErrors }) { const Item = require('prismarine-item')(bot.registry) const windows = require('prismarine-windows')(bot.version) let eatingTask = createDoneTask() let nextActionNumber = 0 // < 1.17 let stateId = -1 if (bot.supportFeature('stateIdUsed')) { const listener = packet => { stateId = packet.stateId } bot._client.on('window_items', listener) bot._client.on('set_slot', listener) } const windowClickQueue = [] let windowItems // 0-8, null = uninitialized // which quick bar slot is selected bot.quickBarSlot = null bot.inventory = windows.createWindow(0, 'minecraft:inventory', 'Inventory') bot.currentWindow = null bot.heldItem = null bot.usingHeldItem = false bot._client.on('entity_status', (packet) => { if (packet.entityId === bot.entity.id && packet.entityStatus === 9 && !eatingTask.done) { eatingTask.finish() } bot.usingHeldItem = false }) let previousHeldItem = null bot.on('heldItemChanged', (heldItem) => { // we only disable the item if the item type or count changes if ( heldItem?.type === previousHeldItem?.type && heldItem?.count === previousHeldItem?.count ) { previousHeldItem = heldItem return } if (!eatingTask.done) { eatingTask.finish() } bot.usingHeldItem = false }) bot._client.on('set_cooldown', (packet) => { if (bot.heldItem && bot.heldItem.type !== packet.itemID) return if (!eatingTask.done) { eatingTask.finish() } bot.usingHeldItem = false }) async function consume () { if (!eatingTask.done) { eatingTask.cancel(new Error('Consuming cancelled due to calling bot.consume() again')) } if (bot.game.gameMode !== 'creative' && !ALWAYS_CONSUMABLES.includes(bot.heldItem.name) && bot.food === 20) { throw new Error('Food is full') } eatingTask = createTask() activateItem() await withTimeout(eatingTask.promise, CONSUME_TIMEOUT) } function activateItem (offHand = false) { bot.usingHeldItem = true if (bot.supportFeature('useItemWithBlockPlace')) { bot._client.write('block_place', { location: new Vec3(-1, 255, -1), direction: -1, heldItem: Item.toNotch(bot.heldItem), cursorX: -1, cursorY: -1, cursorZ: -1 }) } else if (bot.supportFeature('useItemWithOwnPacket')) { bot._client.write('use_item', { hand: offHand ? 1 : 0 }) } } function deactivateItem () { bot._client.write('block_dig', { status: 5, location: new Vec3(0, 0, 0), face: 5 }) bot.usingHeldItem = false } async function putSelectedItemRange (start, end, window, slot) { // put the selected item back indow the slot range in window // try to put it in an item that already exists and just increase // the count. while (window.selectedItem) { const item = window.findItemRange(start, end, window.selectedItem.type, window.selectedItem.metadata, true, window.selectedItem.nbt) if (item && item.stackSize !== item.count) { // something to join with await clickWindow(item.slot, 0, 0) } else { // nothing to join with const emptySlot = window.firstEmptySlotRange(start, end) if (emptySlot === null) { // no room left if (slot === null) { // no room => drop it await tossLeftover() } else { // if there is still some leftover and slot is not null, click slot await clickWindow(slot, 0, 0) await tossLeftover() } } else { await clickWindow(emptySlot, 0, 0) } } } async function tossLeftover () { if (window.selectedItem) { await clickWindow(-999, 0, 0) } } } async function activateBlock (block, direction, cursorPos) { direction = direction ?? new Vec3(0, 1, 0) const directionNum = vectorToDirection(direction) // The packet needs a number as the direction cursorPos = cursorPos ?? new Vec3(0.5, 0.5, 0.5) // TODO: tell the server that we are not sneaking while doing this await bot.lookAt(block.position.offset(0.5, 0.5, 0.5), false) // place block message if (bot.supportFeature('blockPlaceHasHeldItem')) { bot._client.write('block_place', { location: block.position, direction: directionNum, heldItem: Item.toNotch(bot.heldItem), cursorX: cursorPos.scaled(16).x, cursorY: cursorPos.scaled(16).y, cursorZ: cursorPos.scaled(16).z }) } else if (bot.supportFeature('blockPlaceHasHandAndIntCursor')) { bot._client.write('block_place', { location: block.position, direction: directionNum, hand: 0, cursorX: cursorPos.scaled(16).x, cursorY: cursorPos.scaled(16).y, cursorZ: cursorPos.scaled(16).z }) } else if (bot.supportFeature('blockPlaceHasHandAndFloatCursor')) { bot._client.write('block_place', { location: block.position, direction: directionNum, hand: 0, cursorX: cursorPos.x, cursorY: cursorPos.y, cursorZ: cursorPos.z }) } else if (bot.supportFeature('blockPlaceHasInsideBlock')) { bot._client.write('block_place', { location: block.position, direction: directionNum, hand: 0, cursorX: cursorPos.x, cursorY: cursorPos.y, cursorZ: cursorPos.z, insideBlock: false }) } // swing arm animation bot.swingArm() } async function activateEntity (entity) { // TODO: tell the server that we are not sneaking while doing this await bot.lookAt(entity.position.offset(0, 1, 0), false) bot._client.write('use_entity', { target: entity.id, mouse: 0, // interact with entity sneaking: false, hand: 0 // interact with the main hand }) } async function activateEntityAt (entity, position) { // TODO: tell the server that we are not sneaking while doing this await bot.lookAt(position, false) bot._client.write('use_entity', { target: entity.id, mouse: 2, // interact with entity at sneaking: false, hand: 0, // interact with the main hand x: position.x - entity.position.x, y: position.y - entity.position.y, z: position.z - entity.position.z }) } async function transfer (options) { const window = options.window || bot.currentWindow || bot.inventory const itemType = options.itemType const metadata = options.metadata const nbt = options.nbt let count = options.count === null ? 1 : options.count let firstSourceSlot = null // ranges const sourceStart = options.sourceStart const destStart = options.destStart assert.notStrictEqual(sourceStart, null) assert.notStrictEqual(destStart, null) const sourceEnd = options.sourceEnd === null ? sourceStart + 1 : options.sourceEnd const destEnd = options.destEnd === null ? destStart + 1 : options.destEnd await transferOne() async function transferOne () { if (count === 0) { await putSelectedItemRange(sourceStart, sourceEnd, window, firstSourceSlot) return } if (!window.selectedItem || window.selectedItem.type !== itemType || (metadata != null && window.selectedItem.metadata !== metadata) || (nbt != null && window.selectedItem.nbt !== nbt)) { // we are not holding the item we need. click it. const sourceItem = window.findItemRange(sourceStart, sourceEnd, itemType, metadata, false, nbt) const mcDataEntry = bot.registry.itemsArray.find(x => x.id === itemType) assert(mcDataEntry, 'Invalid itemType') if (!sourceItem) throw new Error(`Can't find ${mcDataEntry.name} in slots [${sourceStart} - ${sourceEnd}], (item id: ${itemType})`) if (firstSourceSlot === null) firstSourceSlot = sourceItem.slot // number of item that can be moved from that slot await clickWindow(sourceItem.slot, 0, 0) } await clickDest() async function clickDest () { assert.notStrictEqual(window.selectedItem.type, null) assert.notStrictEqual(window.selectedItem.metadata, null) let destItem let destSlot // special case for tossing if (destStart === -999) { destSlot = -999 } else { // find a non full item that we can drop into destItem = window.findItemRange(destStart, destEnd, window.selectedItem.type, window.selectedItem.metadata, true, nbt) // if that didn't work find an empty slot to drop into destSlot = destItem ? destItem.slot : window.firstEmptySlotRange(destStart, destEnd) // if that didn't work, give up if (destSlot === null) { throw new Error('destination full') } } // move the maximum number of item that can be moved const destSlotCount = destItem && destItem.count ? destItem.count : 0 const movedItems = Math.min(window.selectedItem.stackSize - destSlotCount, window.selectedItem.count) // if the number of item the left click moves is less than the number of item we want to move // several at the same time (left click) if (movedItems <= count) { await clickWindow(destSlot, 0, 0) // update the number of item we want to move (count) count -= movedItems await transferOne() } else { // one by one (right click) await clickWindow(destSlot, 1, 0) count -= 1 await transferOne() } } } } function extendWindow (window) { window.close = () => { closeWindow(window) window.emit('close') } window.withdraw = async (itemType, metadata, count, nbt) => { if (bot.inventory.emptySlotCount() === 0) { throw new Error('Unable to withdraw, Bot inventory is full.') } const options = { window, itemType, metadata, count, nbt, sourceStart: 0, sourceEnd: window.inventoryStart, destStart: window.inventoryStart, destEnd: window.inventoryEnd } await transfer(options) } window.deposit = async (itemType, metadata, count, nbt) => { const options = { window, itemType, metadata, count, nbt, sourceStart: window.inventoryStart, sourceEnd: window.inventoryEnd, destStart: 0, destEnd: window.inventoryStart } await transfer(options) } } async function openBlock (block, direction, cursorPos) { bot.activateBlock(block, direction, cursorPos) const [window] = await once(bot, 'windowOpen') extendWindow(window) return window } async function openEntity (entity) { bot.activateEntity(entity) const [window] = await once(bot, 'windowOpen') extendWindow(window) return window } function createActionNumber () { nextActionNumber = nextActionNumber === 32767 ? 1 : nextActionNumber + 1 return nextActionNumber } function updateHeldItem () { bot.heldItem = bot.inventory.slots[bot.QUICK_BAR_START + bot.quickBarSlot] bot.entity.heldItem = bot.heldItem bot.emit('heldItemChanged', bot.entity.heldItem) } function closeWindow (window) { bot._client.write('close_window', { windowId: window.id }) copyInventory(window) bot.currentWindow = null bot.emit('windowClose', window) } function copyInventory (window) { const slotOffset = window.inventoryStart - bot.inventory.inventoryStart for (let i = window.inventoryStart; i < window.inventoryEnd; i++) { const item = window.slots[i] const slot = i - slotOffset if (item) { item.slot = slot } if (!Item.equal(bot.inventory.slots[slot], item, true)) bot.inventory.updateSlot(slot, item) } } function tradeMatch (limitItem, targetItem) { return targetItem.type === limitItem.type && targetItem.count >= limitItem.count } function expectTradeUpdate (window) { const trade = window.selectedTrade const hasItem = !!window.slots[2] if (hasItem !== tradeMatch(trade.inputItem1, window.slots[0])) { if (trade.hasItems2) { return hasItem !== tradeMatch(trade.inputItem2, window.slots[1]) } return true } return false } async function waitForWindowUpdate (window, slot) { if (window.type === 'minecraft:inventory') { if (slot >= 1 && slot <= 4) { await once(bot.inventory, 'updateSlot:0') } } else if (window.type === 'minecraft:crafting') { if (slot >= 1 && slot <= 9) { await once(bot.currentWindow, 'updateSlot:0') } } else if (window.type === 'minecraft:merchant') { const toUpdate = [] if (slot <= 2 && !window.selectedTrade.tradeDisabled && expectTradeUpdate(window)) { toUpdate.push(once(bot.currentWindow, 'updateSlot:2')) } if (slot === 2) { for (const item of bot.currentWindow.containerItems()) { toUpdate.push(once(bot.currentWindow, `updateSlot:${item.slot}`)) } } await Promise.all(toUpdate) } } function confirmTransaction (windowId, actionId, accepted) { // drop the queue entries for all the clicks that the server did not send // transaction packets for. // Also reject transactions that aren't sent from mineflayer let click = windowClickQueue[0] if (click === undefined || !windowClickQueue.some(clicks => clicks.id === actionId)) { // mimic vanilla client and send a rejection for faulty transaction packets bot._client.write('transaction', { windowId, action: actionId, accepted: true // bot.emit(`confirmTransaction${click.id}`, false) }) return } // shift it later if packets are sent out of order click = windowClickQueue.shift() assert.ok(click.id <= actionId) while (actionId > click.id) { onAccepted() click = windowClickQueue.shift() } assert.ok(click) if (accepted) { onAccepted() } else { onRejected() } updateHeldItem() function onAccepted () { const window = windowId === 0 ? bot.inventory : bot.currentWindow if (!window || window.id !== click.windowId) return window.acceptClick(click) bot.emit(`confirmTransaction${click.id}`, true) } function onRejected () { bot._client.write('transaction', { windowId: click.windowId, action: click.id, accepted: true }) bot.emit(`confirmTransaction${click.id}`, false) } } function getChangedSlots (oldSlots, newSlots) { assert.equal(oldSlots.length, newSlots.length) const changedSlots = [] for (let i = 0; i < newSlots.length; i++) { if (!Item.equal(oldSlots[i], newSlots[i])) { changedSlots.push(i) } } return changedSlots } async function clickWindow (slot, mouseButton, mode) { // if you click on the quick bar and have dug recently, // wait a bit if (slot >= bot.QUICK_BAR_START && bot.lastDigTime != null) { let timeSinceLastDig while ((timeSinceLastDig = new Date() - bot.lastDigTime) < DIG_CLICK_TIMEOUT) { await sleep(DIG_CLICK_TIMEOUT - timeSinceLastDig) } } const window = bot.currentWindow || bot.inventory assert.ok(mode >= 0 && mode <= 4) const actionId = createActionNumber() const click = { slot, mouseButton, mode, id: actionId, windowId: window.id, item: slot === -999 ? null : window.slots[slot] } let changedSlots if (bot.supportFeature('transactionPacketExists')) { windowClickQueue.push(click) } else { if ( // this array indicates the clicks that return changedSlots [ 0, // 1, // 2, 3, 4 // 5, // 6 ].includes(click.mode)) { changedSlots = window.acceptClick(click) } else { // this is used as a fallback const oldSlots = JSON.parse(JSON.stringify(window.slots)) window.acceptClick(click) changedSlots = getChangedSlots(oldSlots, window.slots) } changedSlots = changedSlots.map(slot => { return { location: slot, item: Item.toNotch(window.slots[slot]) } }) } // WHEN ADDING SUPPORT FOR OTHER CLICKS, MAKE SURE TO CHANGE changedSlots TO SUPPORT THEM if (bot.supportFeature('stateIdUsed')) { // 1.17.1 + bot._client.write('window_click', { windowId: window.id, stateId, slot, mouseButton, mode, changedSlots, cursorItem: Item.toNotch(window.selectedItem) }) } else if (bot.supportFeature('actionIdUsed')) { // <= 1.16.5 bot._client.write('window_click', { windowId: window.id, slot, mouseButton, action: actionId, mode, // protocol expects null even if there is an item at the slot in mode 2 and 4 item: Item.toNotch((mode === 2 || mode === 4) ? null : click.item) }) } else { // 1.17 bot._client.write('window_click', { windowId: window.id, slot, mouseButton, mode, changedSlots, cursorItem: Item.toNotch(window.selectedItem) }) } if (bot.supportFeature('transactionPacketExists')) { const response = once(bot, `confirmTransaction${actionId}`) if (!window.transactionRequiresConfirmation(click)) { confirmTransaction(window.id, actionId, true) } const [success] = await withTimeout(response, WINDOW_TIMEOUT) .catch(() => { throw new Error(`Server didn't respond to transaction for clicking on slot ${slot} on window with id ${window?.id}.`) }) if (!success) { throw new Error(`Server rejected transaction for clicking on slot ${slot}, on window with id ${window?.id}.`) } } else { await waitForWindowUpdate(window, slot) } } async function putAway (slot) { const window = bot.currentWindow || bot.inventory const promisePutAway = once(window, `updateSlot:${slot}`) await clickWindow(slot, 0, 0) const start = window.inventoryStart const end = window.inventoryEnd await putSelectedItemRange(start, end, window, null) await promisePutAway } async function moveSlotItem (sourceSlot, destSlot) { await clickWindow(sourceSlot, 0, 0) await clickWindow(destSlot, 0, 0) // if we're holding an item, put it back where the source item was. // otherwise we're done. updateHeldItem() if (bot.inventory.selectedItem) { await clickWindow(sourceSlot, 0, 0) } } bot._client.on('transaction', (packet) => { // confirm transaction confirmTransaction(packet.windowId, packet.action, packet.accepted) }) bot._client.on('held_item_slot', (packet) => { // held item change bot.setQuickBarSlot(packet.slot) }) function prepareWindow (window) { if (!windowItems || window.id !== windowItems.windowId) { // don't emit windowOpen until we have the slot data bot.once(`setWindowItems:${window.id}`, () => { extendWindow(window) bot.emit('windowOpen', window) }) } else { for (let i = 0; i < windowItems.items.length; ++i) { const item = Item.fromNotch(windowItems.items[i]) window.updateSlot(i, item) } updateHeldItem() extendWindow(window) bot.emit('windowOpen', window) } } bot._client.on('open_window', (packet) => { // open window bot.currentWindow = windows.createWindow(packet.windowId, packet.inventoryType, packet.windowTitle, packet.slotCount) prepareWindow(bot.currentWindow) }) bot._client.on('open_horse_window', (packet) => { // open window bot.currentWindow = windows.createWindow(packet.windowId, 'HorseWindow', 'Horse', packet.nbSlots) prepareWindow(bot.currentWindow) }) bot._client.on('close_window', (packet) => { // close window const oldWindow = bot.currentWindow bot.currentWindow = null bot.emit('windowClose', oldWindow) }) bot._client.on('set_slot', (packet) => { // set slot const window = packet.windowId === 0 ? bot.inventory : bot.currentWindow if (!window || window.id !== packet.windowId) return const newItem = Item.fromNotch(packet.item) const oldItem = window.slots[packet.slot] window.updateSlot(packet.slot, newItem) updateHeldItem() bot.emit(`setSlot:${window.id}`, oldItem, newItem) }) bot._client.on('window_items', (packet) => { const window = packet.windowId === 0 ? bot.inventory : bot.currentWindow if (!window || window.id !== packet.windowId) { windowItems = packet return } // set window items for (let i = 0; i < packet.items.length; ++i) { const item = Item.fromNotch(packet.items[i]) window.updateSlot(i, item) } updateHeldItem() bot.emit(`setWindowItems:${window.id}`) }) /** * Convert a vector direction to minecraft packet number direction * @param {Vec3} v * @returns {number} */ function vectorToDirection (v) { if (v.y < 0) { return 0 } else if (v.y > 0) { return 1 } else if (v.z < 0) { return 2 } else if (v.z > 0) { return 3 } else if (v.x < 0) { return 4 } else if (v.x > 0) { return 5 } assert.ok(false, `invalid direction vector ${v}`) } bot.activateBlock = activateBlock bot.activateEntity = activateEntity bot.activateEntityAt = activateEntityAt bot.consume = consume bot.activateItem = activateItem bot.deactivateItem = deactivateItem // not really in the public API bot.clickWindow = clickWindow bot.putSelectedItemRange = putSelectedItemRange bot.putAway = putAway bot.closeWindow = closeWindow bot.transfer = transfer bot.openBlock = openBlock bot.openEntity = openEntity bot.moveSlotItem = moveSlotItem bot.updateHeldItem = updateHeldItem }