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
+137 -29
View File
@@ -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;