This commit is contained in:
2026-07-12 17:26:59 -04:00
parent 60663b8d4a
commit ba1bef8ff9
25 changed files with 158880 additions and 1529 deletions
+265 -81
View File
@@ -1,6 +1,5 @@
'use strict';
const conf = require('../conf');
const { sleep } = require('../utils');
const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21];
@@ -8,7 +7,11 @@ const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21];
class FarmSupply {
constructor(args) {
this.bot = args.bot;
this.config = conf.farmSupply || {};
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;
@@ -129,11 +132,48 @@ class FarmSupply {
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;
}
await this.resumeFarmPlugins(paused);
this._resupplying = false;
console.log('FarmSupply: Resupply complete, farm resumed.');
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;
}
// ========================================
@@ -143,7 +183,7 @@ class FarmSupply {
async emptyFilledBoxes() {
let filledChest;
try {
filledChest = this.bot.findChestBySign('filled boxes');
filledChest = this.bot.findBlockBySign('filled boxes');
} catch (error) {
console.log('FarmSupply: No "filled boxes" chest found, skipping');
return;
@@ -155,40 +195,68 @@ class FarmSupply {
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) {
// Re-find chest each loop — block ref may be stale after trade teleport
filledChest = this.bot.findChestBySign('filled boxes');
await this.bot.goTo({ where: filledChest.position, range: 2 });
let window = await this.bot.openContainer(filledChest);
await this.bot.goToMust({ where: filledChest.position, range: 2 });
let window = await this.bot.openContainer(this.bot.findChestBySign('filled boxes'));
await sleep(300);
let taken = 0;
for (let i = 0; i < window.inventoryStart; i++) {
if (taken >= 12) break;
const item = window.slots[i];
if (item && item.name.includes('shulker_box')) {
try {
await this.bot.bot.moveSlotItem(i, window.inventoryStart + taken);
await sleep(200);
taken++;
} catch (error) {
console.error('FarmSupply: Error taking shulker from chest:', error);
// 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 (taken === 0) {
if (takenSlots.length === 0) {
console.log('FarmSupply: No more filled shulker boxes to deposit');
hasMore = false;
break;
}
console.log(`FarmSupply: Took ${taken} shulker boxes, initiating trade deposit`);
await this.tradeDeposit();
console.log(`FarmSupply: Took ${takenSlots.length} shulker boxes, initiating trade deposit`);
await this.tradeDeposit(takenSlots);
await this.waitForStorageBotIdle();
await this.settleAfterTrade();
}
@@ -201,7 +269,7 @@ class FarmSupply {
async fillEmptyShulkers() {
let emptyChest;
try {
emptyChest = this.bot.findChestBySign('empty shulkers');
emptyChest = this.bot.findBlockBySign('empty shulkers');
} catch (error) {
console.log('FarmSupply: No "empty shulkers" chest found, skipping');
return;
@@ -213,15 +281,13 @@ class FarmSupply {
console.log('FarmSupply: Processing empty shulkers chest');
// Re-find and navigate to chest
emptyChest = this.bot.findChestBySign('empty shulkers');
await this.bot.goTo({ where: emptyChest.position, range: 2 });
let window = await this.bot.openContainer(emptyChest);
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 i = 0; i < window.inventoryStart; i++) {
if (!window.slots[i]) emptySlots++;
for (let chestSlot = 0; chestSlot < window.inventoryStart; chestSlot++) {
if (!window.slots[chestSlot]) emptySlots++;
}
await this.bot.bot.closeWindow(window);
@@ -295,25 +361,25 @@ class FarmSupply {
// Re-find chest and dump shulker boxes
emptyChest = this.bot.findChestBySign('empty shulkers');
await this.bot.goTo({ where: emptyChest.position, range: 2 });
await this.bot.goToMust({ where: emptyChest.position, range: 2 });
window = await this.bot.openContainer(emptyChest);
await sleep(300);
let deposited = 0;
for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
const item = window.slots[i];
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 j = 0; j < window.inventoryStart; j++) {
if (!window.slots[j]) {
targetSlot = j;
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(i, targetSlot);
await this.bot.bot.moveSlotItem(invSlot, targetSlot);
await sleep(200);
deposited++;
} catch (error) {
@@ -331,77 +397,186 @@ class FarmSupply {
// ========================================
async tradeWithdraw(itemName, count) {
console.log(`FarmSupply: Requesting ${count}x ${itemName} from ${this.storageBotName}`);
const storageName = this.storageBotName;
console.log(`FarmSupply: Requesting ${count}x ${itemName} from ${storageName}`);
await this.bot.whisper(this.storageBotName, `.withdraw ${itemName} ${count}`);
await sleep(3000);
// 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`);
await this.bot.say(`/trade ${this.storageBotName}`);
let window = await this.bot.once('windowOpen');
// 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);
});
// Wait for storage bot to place items and confirm
await sleep(2000);
// Confirm our side
try {
this.bot.bot.moveSlotItem(37, 37);
} catch (error) {
console.error('FarmSupply: Error confirming trade:', error);
}
// 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 Promise.race([
this.bot.once('windowClose'),
sleep(15000),
]);
await whisperPromise;
} catch (error) {
console.error('FarmSupply: Trade withdraw timeout or error:', 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() {
console.log(`FarmSupply: Depositing shulker boxes to ${this.storageBotName}`);
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 this.bot.once('windowOpen');
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 slotNum of botSlots) {
if (placed >= 12) break;
for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
const item = window.slots[i];
if (!item || !item.name.includes('shulker_box')) continue;
try {
await this.bot.bot.moveSlotItem(i, slotNum);
await sleep(200);
placed++;
} catch (error) {
console.error('FarmSupply: Error placing item in trade:', error);
}
break;
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`);
await sleep(500);
// Click 1: lock items (green wool — first confirmation, single click)
try {
this.bot.bot.moveSlotItem(37, 37);
await this.bot.bot.clickWindow(37, 0, 0);
console.log('FarmSupply: Trade click 1 — items locked');
} catch (error) {
console.error('FarmSupply: Error confirming deposit trade:', 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(15000),
sleep(30000),
]);
} catch (error) {
console.error('FarmSupply: Trade deposit timeout or error:', error);
@@ -423,13 +598,22 @@ class FarmSupply {
if (!storage) return;
console.log('FarmSupply: Waiting for storage bot to finish processing...');
const maxWait = 120000;
const maxWait = 300000;
const start = Date.now();
while (storage._busy && (Date.now() - start) < maxWait) {
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 (storage._busy) {
if (isBusy()) {
console.log('FarmSupply: Storage bot still busy after timeout, continuing anyway');
} else {
console.log('FarmSupply: Storage bot idle');
@@ -467,7 +651,7 @@ class FarmSupply {
throw new Error('FarmSupply: No crafting table found nearby');
}
await this.bot.goTo({ where: craftingTable.position, range: 2 });
await this.bot.goToMust({ where: craftingTable.position, range: 2 });
const recipe = this.bot.bot.recipesAll(
this.bot.mcData.itemsByName.shulker_box.id,