'use strict'; const { sleep } = require('../utils'); const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21]; class FarmSupply { constructor(args) { this.bot = args.bot; const settings = require('./settings/manager'); this.config = { enabled: settings.get('farmSupply.enabled'), storageBotName: settings.get('farmSupply.storageBotName'), }; this._storageBotKey = this.config.storageBotName; this.isAction = true; this._onTimeListen = null; this._onReadyListen = null; this._resupplying = false; } get storageBotName() { const storageBot = this.bot.constructor.bots[this._storageBotKey]; if (storageBot && storageBot.bot && storageBot.bot.entity) { return storageBot.bot.entity.username; } return this._storageBotKey; } async init() { if (!this._storageBotKey) { console.log('FarmSupply: No storageBotName configured, plugin disabled'); return false; } console.log(`FarmSupply: Initialized for bot ${this.bot.name}, storage bot: ${this._storageBotKey}`); this._onReadyListen = this.bot.on('onReady', async () => { await sleep(3000); let lastTimeOfDay = this.bot.bot.time.timeOfDay; this._onTimeListen = this.bot.bot.on('time', async () => { const currentTime = this.bot.bot.time.timeOfDay; if (lastTimeOfDay < 12000 && currentTime >= 12000 && !this._resupplying) { lastTimeOfDay = currentTime; console.log('FarmSupply: Sunset detected, starting resupply'); try { await this.resupply(); } catch (error) { console.error('FarmSupply: Sunset resupply error:', error); } } else { lastTimeOfDay = currentTime; } }); }); return true; } unload() { if (this._onReadyListen) this._onReadyListen(); if (this._onTimeListen) this._onTimeListen = null; return true; } // ======================================== // Pause / Resume farm plugins // ======================================== pauseFarmPlugins() { const paused = []; for (const [name, plugin] of Object.entries(this.bot.plunginsLoaded)) { if (name === 'FarmSupply' || name === 'AutoEat') continue; if (plugin.isAction) { console.log(`FarmSupply: Pausing plugin ${name}`); try { plugin.unload(); } catch (error) { console.error(`FarmSupply: Error pausing ${name}:`, error); } paused.push(name); delete this.bot.plunginsLoaded[name]; } } return paused; } async resumeFarmPlugins(paused) { for (const name of paused) { console.log(`FarmSupply: Resuming plugin ${name}`); try { await this.bot.pluginLoad(name); } catch (error) { console.error(`FarmSupply: Error resuming ${name}:`, error); } } } // ======================================== // Wait after trade teleport — let server tp us back and chunks load // ======================================== async settleAfterTrade() { console.log('FarmSupply: Settling after trade teleport...'); await sleep(3000); // Wait for pathfinder to be idle while (this.bot.bot.pathfinder.isMoving()) { this.bot.bot.clearControlStates(); await sleep(500); } this.bot.bot.clearControlStates(); await sleep(1000); } // ======================================== // Main resupply flow // ======================================== async resupply() { if (this._resupplying) { console.log('FarmSupply: Already resupplying, skipping'); return; } this._resupplying = true; console.log('FarmSupply: Starting resupply...'); const paused = this.pauseFarmPlugins(); try { await this.emptyFilledBoxes(); await this.fillEmptyShulkers(); } catch (error) { console.error('FarmSupply: Resupply error:', error); } finally { // Always resume the farm and clear the flag — a stuck trade must // not leave the farm paused until restart try { await this.resumeFarmPlugins(paused); } catch (resumeError) { console.error('FarmSupply: Error resuming farm plugins:', resumeError); } this._resupplying = false; console.log('FarmSupply: Resupply complete, farm resumed.'); } } // ======================================== // Storage bot access // ======================================== /** * Bring the on-demand storage bot online (with plugins loaded) and return * its Storage plugin instance. Returns null if unavailable. */ async _ensureStorageOnline() { const storageBot = this.bot.constructor.bots[this._storageBotKey]; if (!storageBot) { console.log(`FarmSupply: Storage bot '${this._storageBotKey}' not configured`); return null; } if (!storageBot.isReady) { console.log('FarmSupply: Bringing storage bot online...'); try { await Promise.race([ storageBot.ensureConnected(async () => {}), sleep(60000).then(() => { throw new Error('Storage bot connect timeout'); }), ]); } catch (error) { console.error(`FarmSupply: Could not bring storage bot online: ${error.message}`); return null; } } return storageBot.plunginsLoaded['Storage'] || null; } // ======================================== // Empty "filled boxes" chest // ======================================== async emptyFilledBoxes() { let filledChest; try { filledChest = this.bot.findBlockBySign('filled boxes'); } catch (error) { console.log('FarmSupply: No "filled boxes" chest found, skipping'); return; } if (!filledChest) { console.log('FarmSupply: No "filled boxes" chest found, skipping'); return; } console.log('FarmSupply: Processing filled boxes chest'); // Boxes left in inventory by a previously failed deposit go first, // before taking more from the chest const leftover = this.bot.bot.inventory.items().filter(i => i.name.includes('shulker_box')); if (leftover.length > 0) { console.log(`FarmSupply: ${leftover.length} leftover box(es) in inventory, depositing those first`); // Player inventory slot 9+k maps to window.inventoryStart+k await this.tradeDeposit(leftover.slice(0, 12).map(i => i.slot - 9)); await this.waitForStorageBotIdle(); await this.settleAfterTrade(); } let hasMore = true; while (hasMore) { await this.bot.goToMust({ where: filledChest.position, range: 2 }); let window = await this.bot.openContainer(this.bot.findChestBySign('filled boxes')); await sleep(300); // Track slots RELATIVE to inventoryStart — the trade window has a // different inventoryStart than this chest window, so absolute slot // numbers from here would point at the wrong items there const takenSlots = []; for (let chestSlot = 0; chestSlot < window.inventoryStart; chestSlot++) { if (takenSlots.length >= 12) break; const item = window.slots[chestSlot]; if (!item || !item.name.includes('shulker_box')) continue; const destSlot = window.inventoryStart + takenSlots.length; if (window.slots[destSlot]) { console.log(`FarmSupply: Inventory slot ${destSlot} occupied, finding free slot`); // Find an actually empty slot let found = false; for (let freeSlot = window.inventoryStart; freeSlot < window.inventoryEnd; freeSlot++) { if (!window.slots[freeSlot]) { await this.bot.bot.moveSlotItem(chestSlot, freeSlot); await sleep(200); takenSlots.push(freeSlot - window.inventoryStart); found = true; break; } } if (!found) { console.log('FarmSupply: No empty inventory slots, stopping'); break; } } else { await this.bot.bot.moveSlotItem(chestSlot, destSlot); await sleep(200); takenSlots.push(destSlot - window.inventoryStart); } } await this.bot.bot.closeWindow(window); await sleep(300); if (takenSlots.length === 0) { console.log('FarmSupply: No more filled shulker boxes to deposit'); hasMore = false; break; } console.log(`FarmSupply: Took ${takenSlots.length} shulker boxes, initiating trade deposit`); await this.tradeDeposit(takenSlots); await this.waitForStorageBotIdle(); await this.settleAfterTrade(); } } // ======================================== // Fill "empty shulkers" chest // ======================================== async fillEmptyShulkers() { let emptyChest; try { emptyChest = this.bot.findBlockBySign('empty shulkers'); } catch (error) { console.log('FarmSupply: No "empty shulkers" chest found, skipping'); return; } if (!emptyChest) { console.log('FarmSupply: No "empty shulkers" chest found, skipping'); return; } console.log('FarmSupply: Processing empty shulkers chest'); await this.bot.goToMust({ where: emptyChest.position, range: 2 }); let window = await this.bot.openContainer(this.bot.findChestBySign('empty shulkers')); await sleep(300); let emptySlots = 0; for (let chestSlot = 0; chestSlot < window.inventoryStart; chestSlot++) { if (!window.slots[chestSlot]) emptySlots++; } await this.bot.bot.closeWindow(window); await sleep(300); if (emptySlots === 0) { console.log('FarmSupply: Empty shulkers chest is full, skipping'); return; } console.log(`FarmSupply: Need ${emptySlots} shulker boxes`); // Check current inventory for materials let shellsInInv = 0; let chestsInInv = 0; let shulkerBoxesInInv = 0; for (const item of this.bot.bot.inventory.items()) { if (item.name === 'shulker_shell') shellsInInv += item.count; if (item.name === 'chest') chestsInInv += item.count; if (item.name.includes('shulker_box')) shulkerBoxesInInv += item.count; } const canCraft = Math.min(Math.floor(shellsInInv / 2), chestsInInv); const totalAvailable = shulkerBoxesInInv + canCraft; const needed = Math.min(emptySlots, 27); if (totalAvailable < needed) { const toCraft = needed - shulkerBoxesInInv; const shellsNeeded = Math.max(0, (toCraft * 2) - shellsInInv); const chestsNeeded = Math.max(0, toCraft - chestsInInv); if (shellsNeeded > 0) { console.log(`FarmSupply: Withdrawing ${shellsNeeded} shulker_shell from storage`); await this.tradeWithdraw('shulker_shell', shellsNeeded); await this.waitForStorageBotIdle(); await this.settleAfterTrade(); } if (chestsNeeded > 0) { console.log(`FarmSupply: Withdrawing ${chestsNeeded} chest from storage`); await this.tradeWithdraw('chest', chestsNeeded); await this.waitForStorageBotIdle(); await this.settleAfterTrade(); } } // Recount after possible withdrawal shellsInInv = 0; chestsInInv = 0; shulkerBoxesInInv = 0; for (const item of this.bot.bot.inventory.items()) { if (item.name === 'shulker_shell') shellsInInv += item.count; if (item.name === 'chest') chestsInInv += item.count; if (item.name.includes('shulker_box')) shulkerBoxesInInv += item.count; } // Craft shulker boxes const craftCount = Math.min(Math.floor(shellsInInv / 2), chestsInInv, needed - shulkerBoxesInInv); if (craftCount > 0) { console.log(`FarmSupply: Crafting ${craftCount} shulker boxes`); for (let i = 0; i < craftCount; i++) { try { await this.craftShulkerBox(); await sleep(300); } catch (error) { console.error('FarmSupply: Crafting error:', error); break; } } } // Re-find chest and dump shulker boxes emptyChest = this.bot.findChestBySign('empty shulkers'); await this.bot.goToMust({ where: emptyChest.position, range: 2 }); window = await this.bot.openContainer(emptyChest); await sleep(300); let deposited = 0; for (let invSlot = window.inventoryStart; invSlot < window.inventoryEnd; invSlot++) { const item = window.slots[invSlot]; if (item && item.name.includes('shulker_box')) { let targetSlot = null; for (let emptyChestSlot = 0; emptyChestSlot < window.inventoryStart; emptyChestSlot++) { if (!window.slots[emptyChestSlot]) { targetSlot = emptyChestSlot; break; } } if (targetSlot === null) break; try { await this.bot.bot.moveSlotItem(invSlot, targetSlot); await sleep(200); deposited++; } catch (error) { console.error('FarmSupply: Error depositing shulker box:', error); } } } await this.bot.bot.closeWindow(window); console.log(`FarmSupply: Deposited ${deposited} shulker boxes into empty shulkers chest`); } // ======================================== // Trade: Withdraw items from storage bot // ======================================== async tradeWithdraw(itemName, count) { const storageName = this.storageBotName; console.log(`FarmSupply: Requesting ${count}x ${itemName} from ${storageName}`); // Both bots run in this process — call the storage plugin directly // instead of whispering a chat command (which raced against the storage // bot's command lock and required the bot to already be online) const storage = await this._ensureStorageOnline(); if (!storage) throw new Error(`Storage bot unavailable for ${itemName} withdraw`); // Listen for the "ready — sending trade request" whisper the storage // bot sends right before initiating /trade with us let cleanupWhisper = () => {}; const whisperPromise = new Promise((resolve, reject) => { const timeout = setTimeout(() => { cleanupWhisper(); reject(new Error(`Timeout waiting for ${storageName} to prepare ${itemName}`)); }, 180000); const onWhisper = (from, message) => { if (from !== storageName) return; if (message.includes('ready — sending trade')) { cleanupWhisper(); resolve(); } else if (message.includes('Item not found') || message.includes('Failed to withdraw') || message.includes('Cannot find') || message.includes('Storage busy')) { cleanupWhisper(); reject(new Error(`Storage bot: ${message}`)); } }; cleanupWhisper = () => { this.bot.bot.removeListener('whisper', onWhisper); clearTimeout(timeout); }; this.bot.bot.on('whisper', onWhisper); }); // Fire the withdraw — it pulls items from shulkers (can take minutes), // then trades with us. Don't await yet: we have to accept its trade // for it to complete. const myUsername = this.bot.bot.entity.username; const requestPromise = storage.handleWithdrawRequest(myUsername, itemName, count) .catch(error => console.error(`FarmSupply: Storage withdraw error: ${error.message}`)); try { await whisperPromise; } catch (error) { await requestPromise; throw error; } // Poll /trade accept until the trade window opens. // Set up windowOpen listener BEFORE chat to avoid missing the event. let window = null; const tradeStart = Date.now(); while (!window && (Date.now() - tradeStart) < 45000) { const winPromise = this.bot.once('windowOpen'); await this.bot.bot.chat('/trade accept'); window = await Promise.race([winPromise, sleep(2000).then(() => null)]); } if (!window) throw new Error(`Trade window did not open with ${storageName}`); // Click 1: lock our items — single left-click (moveSlotItem's // pickup+putdown pair desyncs on the cancelled GUI slot and trips // anti-cheat) await sleep(500); try { await this.bot.bot.clickWindow(37, 0, 0); } catch (e) { /* ignore */ } console.log('FarmSupply: Trade click 1 — items locked'); // Wait for storage bot to lock (slot 53 turns lime_dye), bounded — // an unbounded poll here used to hang resupply forever on a dead trade const locked = await this._waitForTradeLock(window, 120000); if (!locked) { try { this.bot.bot.closeWindow(window); } catch (e) { /* ignore */ } await requestPromise; throw new Error(`${storageName} never confirmed the ${itemName} trade`); } // Click 2: final confirmation if (this.bot.bot.currentWindow === window) { try { await this.bot.bot.clickWindow(37, 0, 0); } catch (e) { /* ignore */ } console.log('FarmSupply: Trade click 2 — final confirm'); } // Wait for trade to complete await Promise.race([ this.bot.once('windowClose'), sleep(120000), ]); await requestPromise; await sleep(500); console.log(`FarmSupply: Withdraw trade complete for ${itemName}`); } /** * Poll the trade window for the other party's lock indicator (slot 53 * turning lime_dye). Resolves true when locked, false on timeout or if * the window closes. */ async _waitForTradeLock(window, timeoutMs) { const start = Date.now(); while (Date.now() - start < timeoutMs) { try { const indicator = window.slots[53]; if (indicator && indicator.name === 'lime_dye') { console.log('FarmSupply: Storage bot has locked'); return true; } } catch (e) { return false; // window closed } if (this.bot.bot.currentWindow !== window) return false; await sleep(500); } return false; } // ======================================== // Trade: Deposit items to storage bot // ======================================== async tradeDeposit(slotsToTrade) { console.log(`FarmSupply: Depositing ${slotsToTrade.length} shulker boxes to ${this.storageBotName}`); // Storage bot is on-demand — it must be online to accept the trade const storage = await this._ensureStorageOnline(); if (!storage) throw new Error('Storage bot unavailable for deposit'); const windowPromise = this.bot.once('windowOpen'); await this.bot.say(`/trade ${this.storageBotName}`); let window = await Promise.race([ windowPromise, sleep(45000).then(() => null), ]); if (!window) throw new Error(`Trade window with ${this.storageBotName} never opened`); // slotsToTrade holds inventory-relative indices — offset them into // this window's inventory section let placed = 0; for (const tradeSlot of botSlots) { if (placed >= slotsToTrade.length) break; try { await this.bot.bot.moveSlotItem(window.inventoryStart + slotsToTrade[placed], tradeSlot); await sleep(200); placed++; } catch (error) { console.error('FarmSupply: Error placing item in trade:', error); } } console.log(`FarmSupply: Placed ${placed} shulker boxes in trade window`); // Click 1: lock items (green wool — first confirmation, single click) try { await this.bot.bot.clickWindow(37, 0, 0); console.log('FarmSupply: Trade click 1 — items locked'); } catch (error) { console.error('FarmSupply: Error on trade click 1:', error); } // Wait for storage bot to lock (slot 53 turns lime_dye from grey_dye) const locked = await this._waitForTradeLock(window, 120000); if (!locked) { try { this.bot.bot.closeWindow(window); } catch (e) { /* ignore */ } throw new Error('Storage bot never confirmed the deposit trade'); } // Click 2: final confirmation (green wool — second click, both locked) try { if (this.bot.bot.currentWindow === window) { await this.bot.bot.clickWindow(37, 0, 0); console.log('FarmSupply: Trade click 2 — final confirm'); } } catch (error) { console.error('FarmSupply: Error on trade click 2:', error); } try { await Promise.race([ this.bot.once('windowClose'), sleep(30000), ]); } catch (error) { console.error('FarmSupply: Trade deposit timeout or error:', error); } await sleep(500); console.log(`FarmSupply: Deposit trade complete`); } // ======================================== // Wait for storage bot to finish processing // ======================================== async waitForStorageBotIdle() { const storageBot = this.bot.constructor.bots[this._storageBotKey]; if (!storageBot) return; const storage = storageBot.plunginsLoaded['Storage']; if (!storage) return; console.log('FarmSupply: Waiting for storage bot to finish processing...'); const maxWait = 300000; const start = Date.now(); const isBusy = () => storage._busy || storage._operationLock; while (Date.now() - start < maxWait) { if (!isBusy()) { // There's a short gap between the trade handler releasing the // lock and the post-trade organize grabbing it — require the // bot to stay idle across a re-check before trusting it await sleep(3000); if (!isBusy()) break; } await sleep(2000); } if (isBusy()) { console.log('FarmSupply: Storage bot still busy after timeout, continuing anyway'); } else { console.log('FarmSupply: Storage bot idle'); } await sleep(1000); } // ======================================== // Craft a shulker box // ======================================== async craftShulkerBox() { let craftingTable; try { const signBlock = this.bot.findBlockBySign('crafting table'); if (signBlock) { craftingTable = this.bot.bot.findBlock({ point: signBlock.position, matching: this.bot.mcData.blocksByName.crafting_table.id, maxDistance: 4, }); } } catch (error) { // No sign, search nearby } if (!craftingTable) { craftingTable = this.bot.bot.findBlock({ matching: this.bot.mcData.blocksByName.crafting_table.id, maxDistance: 64, }); } if (!craftingTable) { throw new Error('FarmSupply: No crafting table found nearby'); } await this.bot.goToMust({ where: craftingTable.position, range: 2 }); const recipe = this.bot.bot.recipesAll( this.bot.mcData.itemsByName.shulker_box.id, null, craftingTable )[0]; if (!recipe) { throw new Error('FarmSupply: No recipe found for shulker box'); } const window = await this.bot.openCraftingTable(craftingTable); const inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd); const ingredientsByType = {}; for (let row = 0; row < recipe.inShape.length; row++) { for (let col = 0; col < recipe.inShape[row].length; col++) { const shape = recipe.inShape[row][col]; if (shape.id === -1) continue; const gridSlot = row * 3 + col + 1; if (!ingredientsByType[shape.id]) ingredientsByType[shape.id] = []; ingredientsByType[shape.id].push(gridSlot); } } for (const [typeId, gridSlots] of Object.entries(ingredientsByType)) { const invIdx = inventory.findIndex(el => el && el.type === parseInt(typeId)); if (invIdx === -1) { await window.close(); throw new Error(`FarmSupply: Missing ingredient type ${typeId} in inventory`); } const actualSlot = window.inventoryStart + invIdx; await this.bot.bot.clickWindow(actualSlot, 0, 0); await sleep(100); for (const gridSlot of gridSlots) { await this.bot.bot.clickWindow(gridSlot, 1, 0); await sleep(100); } await this.bot.bot.clickWindow(actualSlot, 0, 0); await sleep(100); } await sleep(500); if (window.slots[0]) { let outputSlot = null; for (let j = window.inventoryStart; j < window.inventoryEnd; j++) { if (!window.slots[j]) { outputSlot = j; break; } } if (outputSlot === null) outputSlot = window.inventoryStart; await this.bot.bot.clickWindow(0, 0, 0); await sleep(100); await this.bot.bot.clickWindow(outputSlot, 0, 0); await sleep(100); } await window.close(); await sleep(300); console.log('FarmSupply: Crafted a shulker box'); } } FarmSupply.getStatus = function(instance) { return { active: true, storageBotName: instance._storageBotKey, resupplying: instance._resupplying, }; }; module.exports = FarmSupply;