forked from wmantly/mc-bot-town
fable
This commit is contained in:
@@ -138,14 +138,15 @@ class Storage {
|
||||
constructor(args) {
|
||||
console.log('Storage: Constructor called');
|
||||
this.bot = args.bot;
|
||||
this.config = { ...conf.storage, ...args };
|
||||
const settings = require('../settings/manager');
|
||||
// conf.storage → DB/cache overrides → constructor args (highest priority)
|
||||
this.config = { ...conf.storage, ...settings.getSection('storage'), ...args };
|
||||
this.isReady = false;
|
||||
this.shulkerHandler = new ShulkerHandler();
|
||||
this.pendingWithdrawals = new Map(); // playerName → { itemName, count, mode, timeoutId }
|
||||
this._craftAvailable = true; // reset when crafting fails, so we don't spam retries
|
||||
this._busy = false; // true during organize/withdraw/trade to block interval restock
|
||||
this._operationLock = false;
|
||||
console.trace('Storage: lock RELEASED'); // prevents concurrent storage operations
|
||||
this._operationQueue = []; // queue for pending operations
|
||||
}
|
||||
|
||||
@@ -286,6 +287,14 @@ class Storage {
|
||||
throw new Error('Storage busy, scan operation timeout');
|
||||
}
|
||||
|
||||
// Keep on-demand bots alive during long scan operations
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
if (typeof this.bot._resetIdleTimer === 'function') {
|
||||
this.bot._resetIdleTimer();
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
const scanStart = Date.now();
|
||||
try {
|
||||
const chests = await this.scanner.discoverChests(this.bot, this.config.scanRadius || 30, Database);
|
||||
// Register as interruptible task
|
||||
@@ -294,7 +303,8 @@ class Storage {
|
||||
this.bot, Database,
|
||||
() => this.bot.wasInterrupted()
|
||||
);
|
||||
console.log(`Storage[${this.bot.name}]: Complete - ${chests.length} chests, ${shulkers} shulkers`);
|
||||
const elapsed = ((Date.now() - scanStart) / 1000).toFixed(1);
|
||||
console.log(`Storage[${this.bot.name}]: Complete — ${chests.length} chests discovered, ${shulkers} shulkers scanned, ${elapsed}s total`);
|
||||
|
||||
// Capture map images and index maps if not interrupted
|
||||
if (!this.bot.wasInterrupted()) {
|
||||
@@ -302,6 +312,7 @@ class Storage {
|
||||
await this.indexMapsFromStorage();
|
||||
}
|
||||
} finally {
|
||||
clearInterval(keepAliveInterval);
|
||||
this.bot.clearTask();
|
||||
this._releaseOperationLock();
|
||||
}
|
||||
@@ -453,8 +464,16 @@ class Storage {
|
||||
const hotbarItems = this.config.hotbarItems || [];
|
||||
if (hotbarItems.length === 0) return;
|
||||
|
||||
// Another task (scan, withdraw) is active — don't clobber its
|
||||
// interrupt state, just skip this restock cycle
|
||||
if (this.bot._currentTask || this._operationLock) return;
|
||||
|
||||
// Background task: yields to player commands via interruptTask
|
||||
this.bot.registerTask('Storage', 'hotbar-restock', null);
|
||||
try {
|
||||
|
||||
for (const spec of hotbarItems) {
|
||||
if (this._busy) {
|
||||
if (this._busy || this.bot.wasInterrupted()) {
|
||||
console.log('Storage: Hotbar restock interrupted — storage operation in progress');
|
||||
return;
|
||||
}
|
||||
@@ -495,6 +514,10 @@ class Storage {
|
||||
let consecutiveFailures = 0;
|
||||
for (const shulker of shulkers) {
|
||||
if (remaining <= 0) break;
|
||||
if (this.bot.wasInterrupted()) {
|
||||
console.log(`Storage: Hotbar restock interrupted mid-${spec.name}, yielding`);
|
||||
return;
|
||||
}
|
||||
if (consecutiveFailures >= 2) {
|
||||
console.log(`Storage: Too many failures restocking ${spec.name}, giving up`);
|
||||
break;
|
||||
@@ -525,6 +548,10 @@ class Storage {
|
||||
}
|
||||
|
||||
await Database.rebuildItemIndex();
|
||||
|
||||
} finally {
|
||||
this.bot.clearTask();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -546,6 +573,13 @@ class Storage {
|
||||
if (hasShulkers) {
|
||||
console.log('Storage: Traded items include shulker boxes, quick-stashing...');
|
||||
shulkersStashed = await this.quickStashAllShulkers();
|
||||
|
||||
// Anything still in inventory means chests are out of empty slots
|
||||
const leftOver = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box')).length;
|
||||
if (leftOver > 0) {
|
||||
console.error(`Storage: ${leftOver} shulker(s) could not be stashed — no chest space`);
|
||||
this.bot.whisper(playerName, `Warning: storage is out of chest space, ${leftOver} shulker(s) are still on me. Add more chests and run a scan.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Deposit any non-shulker items currently in inventory
|
||||
@@ -599,21 +633,28 @@ class Storage {
|
||||
|
||||
console.log(`Storage: Stashing ${batchSize} shulker(s) into chest at ${chestPos} (${chest.empty_slots} free slots)`);
|
||||
|
||||
await this.bot.goTo({ where: chestPos, range: 3 });
|
||||
await this.bot.goToMust({ where: chestPos, range: 3 });
|
||||
const chestBlock = this.bot.bot.blockAt(chestPos);
|
||||
const window = await this.bot.openContainer(chestBlock);
|
||||
await sleep(300);
|
||||
|
||||
// Resolve which chest slots are actually empty right now
|
||||
const existingShulkers = await Database.getShulkersByChest(chest.id);
|
||||
const usedSlots = new Set(existingShulkers.map(s => s.slot));
|
||||
const dbUsedSlots = new Set(existingShulkers.map(s => s.slot));
|
||||
const maxSlots = window.inventoryStart; // mineflayer returns the chest boundary
|
||||
const emptySlots = [];
|
||||
for (let s = 0; s < maxSlots; s++) {
|
||||
if (!usedSlots.has(s) && !window.slots[s]) {
|
||||
emptySlots.push(s);
|
||||
if (emptySlots.length >= batchSize) break;
|
||||
if (window.slots[s]) continue;
|
||||
// If DB says occupied but window shows empty, delete stale record
|
||||
if (dbUsedSlots.has(s)) {
|
||||
const stale = existingShulkers.find(sh => sh.slot === s);
|
||||
if (stale) {
|
||||
console.log(`Storage: Removing stale shulker record #${stale.id} from chest ${chest.id} slot ${s}`);
|
||||
await Database.deleteShulker(stale.id);
|
||||
}
|
||||
}
|
||||
emptySlots.push(s);
|
||||
if (emptySlots.length >= batchSize) break;
|
||||
}
|
||||
|
||||
if (emptySlots.length === 0) {
|
||||
@@ -707,7 +748,7 @@ class Storage {
|
||||
}
|
||||
|
||||
const chestPos = new Vec3(emptySlot.pos_x, emptySlot.pos_y, emptySlot.pos_z);
|
||||
await this.bot.goTo({ where: chestPos, range: 3 });
|
||||
await this.bot.goToMust({ where: chestPos, range: 3 });
|
||||
const chestBlock = this.bot.bot.blockAt(chestPos);
|
||||
const window = await this.bot.openContainer(chestBlock);
|
||||
await sleep(300);
|
||||
@@ -943,6 +984,9 @@ class Storage {
|
||||
return this.bot.whisper(playerName, `Storage busy, please try again in a moment.`);
|
||||
}
|
||||
|
||||
// Interrupted task has released the lock — reset the interrupt flag
|
||||
// so this withdrawal's own movement isn't treated as interrupted
|
||||
this.bot.registerTask('Storage', 'withdraw', null);
|
||||
this._busy = true;
|
||||
|
||||
try {
|
||||
@@ -1021,6 +1065,7 @@ class Storage {
|
||||
this.bot.whisper(playerName, `Failed to withdraw ${itemName}.`);
|
||||
}
|
||||
} finally {
|
||||
this.bot.clearTask();
|
||||
this._busy = false;
|
||||
this._releaseOperationLock();
|
||||
}
|
||||
@@ -1029,7 +1074,8 @@ class Storage {
|
||||
async handleWithdrawShulkers(playerName, itemName, shulkerCount) {
|
||||
console.log(`Storage[${this.bot.name}]: Shulker withdraw request from ${playerName}: ${shulkerCount} shulkers of ${itemName}`);
|
||||
|
||||
// Acquire operation lock to prevent concurrent storage operations
|
||||
// Interrupt any active task then acquire operation lock
|
||||
await this.bot.interruptTask(playerName);
|
||||
try {
|
||||
await this._acquireOperationLock();
|
||||
} catch (error) {
|
||||
@@ -1037,6 +1083,8 @@ class Storage {
|
||||
return this.bot.whisper(playerName, `Storage busy, please try again in a moment.`);
|
||||
}
|
||||
|
||||
// Interrupted task has released the lock — reset the interrupt flag
|
||||
this.bot.registerTask('Storage', 'withdraw-shulkers', null);
|
||||
this._busy = true;
|
||||
|
||||
try {
|
||||
@@ -1098,6 +1146,7 @@ class Storage {
|
||||
this.bot.whisper(playerName, `Failed to withdraw shulkers of ${itemName}.`);
|
||||
}
|
||||
} finally {
|
||||
this.bot.clearTask();
|
||||
this._busy = false;
|
||||
this._releaseOperationLock();
|
||||
}
|
||||
@@ -1207,17 +1256,33 @@ class Storage {
|
||||
|
||||
console.log(`Storage: Placed ${placed} stack(s) in trade window for ${playerName}`);
|
||||
|
||||
// Click 1: lock our items (ez locks first).
|
||||
// Single left-click — moveSlotItem(37,37) sent a pickup+putdown
|
||||
// pair that desyncs against the cancelled GUI slot and trips
|
||||
// anti-cheat ("unusual packets" kick).
|
||||
await sleep(500);
|
||||
try { await this.bot.bot.clickWindow(37, 0, 0); } catch (e) { /* ignore */ }
|
||||
console.log('Storage trade: click 1 — items locked');
|
||||
|
||||
// Poll for customer confirmation (lime_dye at slot 53)
|
||||
const timeoutHandle = setTimeout(() => {
|
||||
try { this.bot.bot.closeWindow(window); } catch (e) { /* ignore */ }
|
||||
this.bot.whisper(playerName, 'Trade timed out.');
|
||||
}, 120000);
|
||||
|
||||
let finalClicked = false;
|
||||
const confirmationCheck = setInterval(async () => {
|
||||
try {
|
||||
if (finalClicked) return;
|
||||
// Never click a window that is no longer open — invalid
|
||||
// window IDs are an instant anti-cheat flag
|
||||
if (this.bot.bot.currentWindow !== window) return;
|
||||
const indicator = window.slots[53];
|
||||
if (indicator && indicator.name === 'lime_dye') {
|
||||
this.bot.bot.moveSlotItem(37, 37);
|
||||
finalClicked = true;
|
||||
// Click 2: finalize (once)
|
||||
await this.bot.bot.clickWindow(37, 0, 0);
|
||||
console.log('Storage trade: click 2 — final confirm');
|
||||
}
|
||||
} catch (e) { /* window may have closed */ }
|
||||
}, 500);
|
||||
@@ -1378,7 +1443,7 @@ class Storage {
|
||||
throw new Error('No crafting table found nearby');
|
||||
}
|
||||
|
||||
await this.bot.goTo({ where: craftingTable.position, range: 3 });
|
||||
await this.bot.goToMust({ where: craftingTable.position, range: 3 });
|
||||
|
||||
// Craft shulker box manually (bot.craft() broken on 1.21+)
|
||||
const shulkerBoxRecipes = this.bot.bot.recipesAll(
|
||||
@@ -1453,7 +1518,7 @@ class Storage {
|
||||
const emptySlot = await Database.findEmptyChestSlot();
|
||||
if (emptySlot) {
|
||||
const chestPos = new Vec3(emptySlot.pos_x, emptySlot.pos_y, emptySlot.pos_z);
|
||||
await this.bot.goTo({ where: chestPos, range: 3 });
|
||||
await this.bot.goToMust({ where: chestPos, range: 3 });
|
||||
const chestBlock = this.bot.bot.blockAt(chestPos);
|
||||
const storeWindow = await this.bot.openContainer(chestBlock);
|
||||
await sleep(300);
|
||||
@@ -1546,7 +1611,7 @@ class Storage {
|
||||
// Organize
|
||||
// ========================================
|
||||
|
||||
async organizeLooseItems() {
|
||||
async organizeLooseItems(skipConsolidation = false) {
|
||||
console.log('Storage: Organizing loose items into shulkers...');
|
||||
|
||||
// Acquire operation lock to prevent concurrent storage operations
|
||||
@@ -1562,6 +1627,14 @@ class Storage {
|
||||
this.bot.registerTask('Storage', 'organize', async () => {});
|
||||
let organized = 0;
|
||||
|
||||
// Keep on-demand bots alive — organize can run for many minutes and
|
||||
// the idle timer would otherwise disconnect the bot mid-sort
|
||||
const keepAliveInterval = setInterval(() => {
|
||||
if (typeof this.bot._resetIdleTimer === 'function') {
|
||||
this.bot._resetIdleTimer();
|
||||
}
|
||||
}, 15000);
|
||||
|
||||
try {
|
||||
// Pre-flight: deposit any stray items in bot inventory
|
||||
await this.cleanInventory();
|
||||
@@ -1592,7 +1665,7 @@ class Storage {
|
||||
const failedItems = new Set();
|
||||
while (true) {
|
||||
|
||||
await this.bot.goTo({ where: chestPos, range: 3 });
|
||||
await this.bot.goToMust({ where: chestPos, range: 3 });
|
||||
const chestBlock = this.bot.bot.blockAt(chestPos);
|
||||
const window = await this.bot.openContainer(chestBlock);
|
||||
await sleep(300);
|
||||
@@ -1735,10 +1808,12 @@ class Storage {
|
||||
}
|
||||
|
||||
// Consolidate partially filled shulkers of the same item type
|
||||
try {
|
||||
await this.consolidateShulkers();
|
||||
} catch (error) {
|
||||
console.error('Storage: Consolidation error:', error.message);
|
||||
if (!skipConsolidation) {
|
||||
try {
|
||||
await this.consolidateShulkers();
|
||||
} catch (error) {
|
||||
console.error('Storage: Consolidation error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Post-flight: deposit any items still in inventory (may succeed now that organize freed shulker space)
|
||||
@@ -1748,6 +1823,7 @@ class Storage {
|
||||
console.log(`Storage: Organized ${organized} item types into shulkers`);
|
||||
return organized;
|
||||
} finally {
|
||||
clearInterval(keepAliveInterval);
|
||||
this.bot.clearTask();
|
||||
this._busy = false;
|
||||
this._releaseOperationLock();
|
||||
@@ -1836,11 +1912,39 @@ class Storage {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Storage: Error unpacking mixed shulker #${shulker.id}:`, error.message);
|
||||
// Upsert it back so it can be retried
|
||||
try {
|
||||
await Database.upsertShulker(shulker.chest_id, shulker.slot, 'shulker_box', null, null);
|
||||
await Database.updateShulkerCounts(shulker.id, shulker.slot_count, shulker.total_items);
|
||||
} catch (e2) { /* ignore */ }
|
||||
|
||||
// Figure out where the box actually ended up before touching the
|
||||
// DB. takeShulkerFromChest marks the record in-transit
|
||||
// (slot_count = -1) the moment the box leaves the chest.
|
||||
const record = await Database.getShulkerById(shulker.id).catch(() => null);
|
||||
const leftChest = record && record.slot_count === -1;
|
||||
const strayBox = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
|
||||
if (error.message.includes('No shulker at chest slot')) {
|
||||
// Slot was empty — record is stale, delete it
|
||||
try { await Database.deleteShulker(shulker.id); } catch (e2) { /* ignore */ }
|
||||
} else if (!leftChest) {
|
||||
// Failure before the box left the chest (navigation, chest
|
||||
// won't open) — leave the record alone so we retry next pass
|
||||
console.log(`Storage: Mixed shulker #${shulker.id} still in chest, will retry next organize`);
|
||||
} else if (strayBox) {
|
||||
// Box made it back to inventory — store it (registers a fresh
|
||||
// DB record via NBT scan) and drop the old record
|
||||
try {
|
||||
await this.storeShulker(strayBox);
|
||||
await Database.deleteShulker(shulker.id);
|
||||
} catch (e2) {
|
||||
console.error(`Storage: Could not store recovered box:`, e2.message);
|
||||
}
|
||||
} else if (error.placedPos) {
|
||||
// Box is physically on the ground and couldn't be collected
|
||||
console.error(`Storage: MIXED SHULKER LEFT ON GROUND at ${error.placedPos} — manual pickup needed`);
|
||||
try { await Database.deleteShulker(shulker.id); } catch (e2) { /* ignore */ }
|
||||
} else {
|
||||
// Left the chest but isn't in inventory or on known ground —
|
||||
// delete the record; a rescan will re-register it if it turns up
|
||||
try { await Database.deleteShulker(shulker.id); } catch (e2) { /* ignore */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1928,7 +2032,7 @@ class Storage {
|
||||
if (itemsInInv.length === 0) return;
|
||||
|
||||
console.log(`Storage: Returning ${itemsInInv.length} stack(s) of ${itemName} to chest at ${chestPos}`);
|
||||
await this.bot.goTo({ where: chestPos, range: 3 });
|
||||
await this.bot.goToMust({ where: chestPos, range: 3 });
|
||||
const chestBlock = this.bot.bot.blockAt(chestPos);
|
||||
const window = await this.bot.openContainer(chestBlock);
|
||||
await sleep(300);
|
||||
@@ -2150,13 +2254,17 @@ class Storage {
|
||||
}
|
||||
|
||||
case 'organize':
|
||||
this.bot.whisper(from, 'Starting organize...');
|
||||
if (from !== 'ai') this.bot.whisper(from, 'Starting organize...');
|
||||
try {
|
||||
const count = await this.organizeLooseItems();
|
||||
this.bot.whisper(from, `Organize complete! Sorted ${count} item stacks.`);
|
||||
const msg = `Organize complete! Sorted ${count} item stacks.`;
|
||||
if (from === 'ai') return msg;
|
||||
this.bot.whisper(from, msg);
|
||||
} catch (error) {
|
||||
console.error('Storage: Organize error:', error);
|
||||
this.bot.whisper(from, `Organize failed: ${error.message}`);
|
||||
const msg = `Organize failed: ${error.message}`;
|
||||
if (from === 'ai') return msg;
|
||||
this.bot.whisper(from, msg);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user