fable
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user