AI works here
This commit is contained in:
@@ -7,6 +7,7 @@ const Database = require('./database');
|
||||
const Scanner = require('./scanner');
|
||||
const ShulkerHandler = require('./shulker-handler');
|
||||
const StorageWeb = require('./web');
|
||||
const { applyMapUpdate, renderMapToPNG } = require('./map-renderer');
|
||||
|
||||
class Storage {
|
||||
static createRouter = StorageWeb.createRouter;
|
||||
@@ -30,6 +31,36 @@ class Storage {
|
||||
try {
|
||||
console.log(`Storage: Bot ${this.bot.name} is ready, initializing storage system...`);
|
||||
|
||||
|
||||
const { Movements } = require('mineflayer-pathfinder')
|
||||
|
||||
|
||||
/* console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
|
||||
const mcData = require('minecraft-data')(this.bot.bot.version)
|
||||
const defaultMovements = new Movements(this.bot.bot)
|
||||
|
||||
// 1. Identify the block we WANT to walk on
|
||||
const targetBlockId = mcData.blocksByName.crafting_table.id
|
||||
|
||||
// 2. Add an exclusion area rule
|
||||
// This function is called for every block the pathfinder considers stepping on.
|
||||
defaultMovements.exclusionAreasStep.push((block) => {
|
||||
// If the block the bot would stand on is NOT our aisle block,
|
||||
// make it very "expensive" to move there.
|
||||
if (block.type !== targetBlockId) {
|
||||
return 100 // High cost forces the pathfinder to look for 0-cost blocks (crafting tables)
|
||||
}
|
||||
return 0 // No extra cost for crafting tables
|
||||
})
|
||||
|
||||
// 3. (Optional) Disable sprinting to prevent sliding off the aisle
|
||||
defaultMovements.allowSprinting = false
|
||||
|
||||
this.bot.bot.pathfinder.setMovements(defaultMovements)*/
|
||||
|
||||
|
||||
|
||||
|
||||
// Initialize database
|
||||
if (!Database.db) {
|
||||
console.log('Storage: Initializing database...');
|
||||
@@ -51,6 +82,16 @@ class Storage {
|
||||
this.isReady = true;
|
||||
console.log('Storage: Initialization complete! Ready to use.');
|
||||
|
||||
// Map packet listener for capturing filled map images
|
||||
this._mapBuffers = {};
|
||||
this._mapListener = (packet) => {
|
||||
const mapId = packet.itemDamage;
|
||||
if (!packet.columns || packet.columns === 0) return;
|
||||
if (!this._mapBuffers[mapId]) this._mapBuffers[mapId] = new Uint8Array(128 * 128);
|
||||
applyMapUpdate(this._mapBuffers[mapId], packet);
|
||||
};
|
||||
this.bot.bot._client.on('map', this._mapListener);
|
||||
|
||||
if (!this.bot.onDemand) {
|
||||
// Initial hotbar restock
|
||||
try {
|
||||
@@ -70,7 +111,7 @@ class Storage {
|
||||
}, restockInterval);
|
||||
|
||||
// Start periodic inventory cleanup
|
||||
const cleanupInterval = this.config.inventoryCleanupInterval || 5 * 60 * 1000;
|
||||
const cleanupInterval = this.config.inventoryCleanupInterval || 60 * 1000;
|
||||
this.cleanupInterval = setInterval(async () => {
|
||||
if (!this.isReady || this._busy || this.shulkerHandler.operationInProgress) return;
|
||||
try {
|
||||
@@ -104,6 +145,13 @@ class Storage {
|
||||
this.cleanupInterval = null;
|
||||
}
|
||||
|
||||
// Remove map listener
|
||||
if (this._mapListener && this.bot?.bot?._client) {
|
||||
this.bot.bot._client.removeListener('map', this._mapListener);
|
||||
this._mapListener = null;
|
||||
}
|
||||
this._mapBuffers = {};
|
||||
|
||||
// Clear any pending withdrawal timeouts
|
||||
for (const [, pending] of this.pendingWithdrawals) {
|
||||
if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
||||
@@ -126,6 +174,147 @@ class Storage {
|
||||
const chests = await this.scanner.discoverChests(this.bot, this.config.scanRadius || 30, Database);
|
||||
const shulkers = await this.scanner.scanAllChests(this.bot, Database);
|
||||
console.log(`Storage[${this.bot.name}]: Complete - ${chests.length} chests, ${shulkers} shulkers`);
|
||||
|
||||
// Capture any map images received passively during scan
|
||||
await this.captureMapImages();
|
||||
|
||||
// Actively withdraw and index any filled_maps not yet captured
|
||||
await this.indexMapsFromStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save any map pixel data received via protocol packets to the database as PNG images.
|
||||
*/
|
||||
async captureMapImages() {
|
||||
const mapIds = Object.keys(this._mapBuffers);
|
||||
if (mapIds.length === 0) return;
|
||||
|
||||
let captured = 0;
|
||||
for (const mapId of mapIds) {
|
||||
const pixels = this._mapBuffers[mapId];
|
||||
// Skip maps with no actual data (all zeros)
|
||||
if (!pixels.some(p => p !== 0)) continue;
|
||||
|
||||
try {
|
||||
const base64png = renderMapToPNG(pixels);
|
||||
const pixelJson = JSON.stringify([...pixels]);
|
||||
await Database.upsertMap(parseInt(mapId), base64png, pixelJson);
|
||||
captured++;
|
||||
console.log(`Storage: Captured map #${mapId}`);
|
||||
} catch (error) {
|
||||
console.error(`Storage: Error capturing map #${mapId}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (captured > 0) {
|
||||
console.log(`Storage: Captured ${captured} map image(s)`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actively index filled_map items from shulkers.
|
||||
* Withdraws each map, holds it in hand to trigger map data packets from server,
|
||||
* waits for data, captures the image, then returns the map to storage.
|
||||
*/
|
||||
async indexMapsFromStorage() {
|
||||
const uncaptured = await Database.getUncapturedMaps();
|
||||
if (uncaptured.length === 0) {
|
||||
console.log('Storage: No filled maps found in storage');
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out maps we already have images for
|
||||
const existing = await Database.getAllMaps();
|
||||
const existingIds = new Set(existing.map(m => m.map_id));
|
||||
|
||||
// Extract map IDs from the filled_map items' NBT data
|
||||
// The map ID is in the nbt_data field as parsed JSON
|
||||
const mapsToCapture = [];
|
||||
for (const item of uncaptured) {
|
||||
let mapId = null;
|
||||
try {
|
||||
if (item.nbt_data) {
|
||||
const nbt = JSON.parse(item.nbt_data);
|
||||
// map ID can be in nbt.map (pre-1.20.5) or nbt.map_id
|
||||
mapId = nbt.map ?? nbt.map_id ?? null;
|
||||
}
|
||||
} catch (e) { /* ignore parse errors */ }
|
||||
|
||||
if (mapId !== null && !existingIds.has(mapId) && !this._mapBuffers[mapId]) {
|
||||
mapsToCapture.push({ ...item, mapId });
|
||||
existingIds.add(mapId); // prevent duplicates in same batch
|
||||
}
|
||||
}
|
||||
|
||||
if (mapsToCapture.length === 0) {
|
||||
console.log('Storage: All filled maps already captured');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Storage: Found ${mapsToCapture.length} uncaptured map(s), indexing...`);
|
||||
|
||||
// Group by shulker to minimize shulker operations
|
||||
const byShulker = {};
|
||||
for (const map of mapsToCapture) {
|
||||
if (!byShulker[map.shulker_id]) byShulker[map.shulker_id] = [];
|
||||
byShulker[map.shulker_id].push(map);
|
||||
}
|
||||
|
||||
for (const [shulkerId, maps] of Object.entries(byShulker)) {
|
||||
const info = maps[0]; // all maps in this group share the same shulker location
|
||||
const chestPos = new Vec3(info.pos_x, info.pos_y, info.pos_z);
|
||||
|
||||
console.log(`Storage: Withdrawing ${maps.length} map(s) from shulker #${shulkerId}`);
|
||||
|
||||
try {
|
||||
// Withdraw all filled_maps from this shulker
|
||||
const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
|
||||
this.bot, chestPos, info.shulker_slot, 'filled_map',
|
||||
maps.length * 64, // withdraw all maps
|
||||
parseInt(shulkerId), info.chest_id
|
||||
);
|
||||
|
||||
if (withdrawn === 0) {
|
||||
console.log(`Storage: Could not withdraw maps from shulker #${shulkerId}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Hold each map in hand briefly to trigger map data packets
|
||||
const mapItems = this.bot.bot.inventory.items().filter(i => i.name === 'filled_map');
|
||||
for (const mapItem of mapItems) {
|
||||
try {
|
||||
await this.bot.bot.equip(mapItem, 'hand');
|
||||
// Wait for server to send map data packets
|
||||
await sleep(2000);
|
||||
} catch (e) {
|
||||
console.log(`Storage: Error holding map: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush received map data to DB
|
||||
await this.captureMapImages();
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Storage: Error indexing maps from shulker #${shulkerId}:`, error.message);
|
||||
}
|
||||
|
||||
// Re-deposit any maps left in inventory (outside try/catch so it always runs)
|
||||
const mapCount = this.bot.bot.inventory.items()
|
||||
.filter(i => i.name === 'filled_map')
|
||||
.reduce((sum, i) => sum + i.count, 0);
|
||||
if (mapCount > 0) {
|
||||
console.log(`Storage: Re-depositing ${mapCount} filled_map(s) back into storage`);
|
||||
try {
|
||||
const result = await this.depositItemType('filled_map', mapCount);
|
||||
console.log(`Storage: Re-deposited ${result.deposited}/${mapCount} filled_map(s)`);
|
||||
} catch (e) {
|
||||
console.error(`Storage: Failed to re-deposit maps: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Database.rebuildItemIndex();
|
||||
console.log('Storage: Map indexing complete');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -224,12 +413,25 @@ class Storage {
|
||||
}
|
||||
|
||||
// After unpacking, read current bot inventory for depositing
|
||||
// Skip hotbar items and shulker boxes (already stored by unpack)
|
||||
const hotbarNames = new Set((this.config.hotbarItems || []).map(h => h.name));
|
||||
// Keep hotbar items up to their target, deposit any excess
|
||||
const hotbarTargets = {};
|
||||
for (const h of (this.config.hotbarItems || [])) {
|
||||
hotbarTargets[h.name] = h.target || 0;
|
||||
}
|
||||
const hotbarSeen = {};
|
||||
const grouped = {};
|
||||
for (const item of this.bot.bot.inventory.items()) {
|
||||
if (hotbarNames.has(item.name)) continue;
|
||||
if (item.name.includes('shulker_box')) continue;
|
||||
if (item.name in hotbarTargets) {
|
||||
const seen = (hotbarSeen[item.name] || 0);
|
||||
const keep = Math.max(0, hotbarTargets[item.name] - seen);
|
||||
hotbarSeen[item.name] = seen + item.count;
|
||||
const excess = item.count - keep;
|
||||
if (excess <= 0) continue;
|
||||
if (!grouped[item.name]) grouped[item.name] = 0;
|
||||
grouped[item.name] += excess;
|
||||
continue;
|
||||
}
|
||||
if (!grouped[item.name]) {
|
||||
grouped[item.name] = 0;
|
||||
}
|
||||
@@ -251,12 +453,18 @@ class Storage {
|
||||
// Rebuild index after all deposits
|
||||
await Database.rebuildItemIndex();
|
||||
|
||||
// Sweep any leftovers that didn't deposit on first pass
|
||||
await this.cleanInventory();
|
||||
|
||||
// Whisper summary to player
|
||||
const summary = results.map(r => {
|
||||
if (r.error) return `${r.itemName}: FAILED (${r.error})`;
|
||||
return `${r.itemName}: ${r.deposited}/${r.requested}`;
|
||||
}).join(', ');
|
||||
|
||||
// Capture any map images from traded filled maps
|
||||
await this.captureMapImages();
|
||||
|
||||
this.bot.whisper(playerName, `Storage complete: ${summary}`);
|
||||
return results;
|
||||
} finally {
|
||||
@@ -266,15 +474,15 @@ class Storage {
|
||||
|
||||
/**
|
||||
* Process all shulker boxes in bot inventory from a trade.
|
||||
* Sorted shulkers (single item type) are stored directly — no unpack needed.
|
||||
* Mixed shulkers are unpacked, items deposited, then empty box stored.
|
||||
* Mixed shulkers (multiple item types) are unpacked, items deposited, then empty box stored.
|
||||
* Sorted shulkers (single item type) and empty shulkers are stored directly — no unpack needed.
|
||||
* Deposits between each shulker to keep inventory space free.
|
||||
*/
|
||||
async unpackTradedShulkers() {
|
||||
// Phase 1: Store ALL shulkers from inventory into chests.
|
||||
// This frees inventory space and registers contents in DB via NBT scan.
|
||||
// Sorted/empty shulkers are done after this. Mixed ones need phase 2.
|
||||
const mixedShulkers = []; // { chestId, chestSlot, chestPos, shulkerId }
|
||||
const shulkersToUnpack = []; // { chestId, chestSlot, chestPos, shulkerId }
|
||||
|
||||
console.log('Storage: Phase 1 — stashing all traded shulkers into chests');
|
||||
let stashCount = 0;
|
||||
@@ -287,9 +495,9 @@ class Storage {
|
||||
const itemTypes = new Set(contents.map(c => c.name));
|
||||
const isMixed = contents.length > 0 && itemTypes.size > 1;
|
||||
|
||||
const label = isMixed
|
||||
? `mixed (${itemTypes.size} types)`
|
||||
: contents.length === 0 ? 'empty' : `sorted (${[...itemTypes][0]})`;
|
||||
const label = !contents.length ? 'empty'
|
||||
: isMixed ? `mixed (${itemTypes.size} types)`
|
||||
: `sorted (${[...itemTypes][0]})`;
|
||||
console.log(`Storage: Stashing shulker ${stashCount}: ${label}`);
|
||||
|
||||
try {
|
||||
@@ -299,7 +507,7 @@ class Storage {
|
||||
break;
|
||||
}
|
||||
if (isMixed) {
|
||||
mixedShulkers.push(info);
|
||||
shulkersToUnpack.push(info);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Storage: Error stashing shulker:`, error.message);
|
||||
@@ -307,53 +515,158 @@ class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Storage: Phase 1 complete — stashed ${stashCount} shulkers, ${mixedShulkers.length} need unpacking`);
|
||||
console.log(`Storage: Phase 1 complete — stashed ${stashCount} shulkers, ${shulkersToUnpack.length} mixed need unpacking`);
|
||||
|
||||
// Phase 2: Pull each mixed shulker back out, unpack it, deposit items, store empty box.
|
||||
// Phase 2: Pull each mixed shulker back out, unpack it, deposit items, return empty box to ORIGINAL slot.
|
||||
// Bot inventory is now empty (aside from hotbar), so we have room.
|
||||
for (let i = 0; i < mixedShulkers.length; i++) {
|
||||
const info = mixedShulkers[i];
|
||||
console.log(`Storage: Phase 2 — unpacking mixed shulker ${i + 1}/${mixedShulkers.length} (DB #${info.shulkerId})`);
|
||||
const failedToUnpack = []; // shulkers that failed and should be re-queued
|
||||
|
||||
for (let i = 0; i < shulkersToUnpack.length; i++) {
|
||||
const info = shulkersToUnpack[i];
|
||||
console.log(`Storage: Phase 2 — unpacking shulker ${i + 1}/${shulkersToUnpack.length} (DB #${info.shulkerId})`);
|
||||
|
||||
// Pre-iteration cleanup: store any leftover shulker boxes from previous iteration
|
||||
// This prevents the wrong shulker being picked up by subsequent .find() calls
|
||||
let leftoverBoxes = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
|
||||
for (const leftover of leftoverBoxes) {
|
||||
console.log(`Storage: Storing leftover shulker box before next unpack`);
|
||||
try { await this.storeShulker(leftover); } catch (e) {
|
||||
console.error(`Storage: Error storing leftover shulker:`, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-iteration cleanup: deposit any non-shulker items clogging inventory
|
||||
await this._depositNonShulkerInventory();
|
||||
|
||||
// Check inventory space — need at least 3 free slots (shulker + extracted items + buffer)
|
||||
const freeSlots = this.bot.bot.inventory.slots.filter((s, idx) => !s && idx >= 9).length;
|
||||
if (freeSlots < 3) {
|
||||
console.log(`Storage: Inventory too full (${freeSlots} free slots), deferring remaining ${shulkersToUnpack.length - i} shulker(s)`);
|
||||
// Re-queue all remaining shulkers for next organize pass
|
||||
for (let j = i; j < shulkersToUnpack.length; j++) {
|
||||
failedToUnpack.push(shulkersToUnpack[j]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Re-verify the shulker still exists in the DB and at the expected chest/slot
|
||||
// If it was already cleaned up by a previous failure, skip it
|
||||
const currentShulker = await Database.getShulkerById(info.shulkerId);
|
||||
if (!currentShulker) {
|
||||
console.log(`Storage: Shulker #${info.shulkerId} no longer in DB, skipping (may have been recovered)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// takeWholeShulker already marks the shulker as in-transit (slot_count=-1) in the DB
|
||||
// so we do NOT delete it beforehand — that would leave the DB in a deleted state with no in-transit marker
|
||||
let unpackSucceeded = false;
|
||||
let shulkerItem = null;
|
||||
let extracted = [];
|
||||
let inventoryFull = false;
|
||||
|
||||
try {
|
||||
// Take the shulker from the chest into inventory
|
||||
// This marks slot_count=-1 in DB to indicate in-transit
|
||||
await this.shulkerHandler.takeWholeShulker(
|
||||
this.bot, info.chestPos, info.chestSlot, info.shulkerId
|
||||
);
|
||||
// Delete from DB since we're about to empty it
|
||||
await Database.deleteShulker(info.shulkerId);
|
||||
|
||||
// Find the shulker in inventory and unpack it
|
||||
const shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
// Find the shulker in inventory — use the slot we know it landed in, not .find()
|
||||
// The shulker is the only shulker box in inventory after pre-iteration cleanup
|
||||
shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (!shulkerItem) {
|
||||
console.error('Storage: Shulker not found in inventory after taking from chest');
|
||||
continue;
|
||||
// Mark as failed — it may still be at the chest if takeWholeShulker failed silently
|
||||
throw new Error('Shulker vanished from inventory after take');
|
||||
}
|
||||
|
||||
const { extracted, inventoryFull } = await this.shulkerHandler.unpackShulkerFromInventory(this.bot, shulkerItem);
|
||||
console.log(`Storage: Extracted ${extracted.length} item stacks from mixed shulker`);
|
||||
({ extracted, inventoryFull } = await this.shulkerHandler.unpackShulkerFromInventory(this.bot, shulkerItem));
|
||||
console.log(`Storage: Extracted ${extracted.length} item stacks from shulker${inventoryFull ? ' (inventory was full, some items may remain in shulker)' : ''}`);
|
||||
unpackSucceeded = true;
|
||||
|
||||
// Deposit all extracted items
|
||||
// Deposit all extracted items grouped by type (prevents fragmentation across shulkers)
|
||||
await this._depositNonShulkerInventory();
|
||||
|
||||
// Store the empty shulker box back into a chest
|
||||
const emptyBox = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (emptyBox) {
|
||||
try {
|
||||
await this.storeShulker(emptyBox);
|
||||
} catch (e) {
|
||||
console.error(`Storage: Error storing empty box:`, e.message);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Storage: Error unpacking mixed shulker #${info.shulkerId}:`, error.message);
|
||||
console.error(`Storage: Error unpacking shulker #${info.shulkerId}:`, error.message);
|
||||
// Deposit whatever we managed to extract
|
||||
await this._depositNonShulkerInventory();
|
||||
// Try to store any shulker box left in inventory
|
||||
|
||||
// If the shulker is still in inventory (unpack failed partway), store it back
|
||||
const leftover = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (leftover) {
|
||||
try { await this.storeShulker(leftover); } catch (e) { /* best effort */ }
|
||||
try {
|
||||
// Try to return to original slot first
|
||||
const result = await this.storeShulker(leftover, {
|
||||
chest_id: info.chestId,
|
||||
pos_x: info.chestPos.x,
|
||||
pos_y: info.chestPos.y,
|
||||
pos_z: info.chestPos.z,
|
||||
slot: info.chestSlot,
|
||||
});
|
||||
if (!result) {
|
||||
// Original slot occupied, just store anywhere
|
||||
await this.storeShulker(leftover);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Storage: Error storing leftover shulker:`, e.message);
|
||||
// Best-effort: just store anywhere
|
||||
try { await this.storeShulker(leftover); } catch (e2) { /* give up */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Mark shulker as back in DB (it was marked in-transit by takeWholeShulker)
|
||||
// Restore it to the chest (re-insert so it can be found by the chest/slot lookup)
|
||||
// The actual physical state is uncertain; this is a best-effort restore
|
||||
try {
|
||||
await Database.upsertShulker(info.chestId, info.chestSlot, shulkerItem?.name || 'shulker_box', null, null);
|
||||
await Database.updateShulkerCounts(info.shulkerId, 0, 0);
|
||||
} catch (e) {
|
||||
console.error(`Storage: Could not restore shulker record:`, e.message);
|
||||
}
|
||||
|
||||
failedToUnpack.push(info);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Unpack succeeded: re-insert the shulker as empty (or partially-empty if inventory was full)
|
||||
// into its ORIGINAL chest slot to preserve physical arrangement
|
||||
const emptyBox = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (emptyBox) {
|
||||
try {
|
||||
// Try to place back in original slot
|
||||
const result = await this.storeShulker(emptyBox, {
|
||||
chest_id: info.chestId,
|
||||
pos_x: info.chestPos.x,
|
||||
pos_y: info.chestPos.y,
|
||||
pos_z: info.chestPos.z,
|
||||
slot: info.chestSlot,
|
||||
});
|
||||
if (!result) {
|
||||
// Original slot occupied (shouldn't happen since we just emptied it)
|
||||
await this.storeShulker(emptyBox);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Storage: Error storing shulker box:`, e.message);
|
||||
try { await this.storeShulker(emptyBox); } catch (e2) { /* give up */ }
|
||||
}
|
||||
}
|
||||
|
||||
// If the shulker was only partially unpacked (inventoryFull), re-queue it
|
||||
if (inventoryFull) {
|
||||
console.log(`Storage: Shulker was only partially unpacked — re-queueing for next organize`);
|
||||
// Mark the shulker as "pending" in DB: it still has mixed contents
|
||||
// We need to update its item_focus to null so organizeLooseItems will detect it
|
||||
try {
|
||||
await Database.upsertShulker(info.chestId, info.chestSlot, 'shulker_box', null, null);
|
||||
// Rescan to get accurate remaining contents
|
||||
const placedItem = emptyBox; // the box still in inventory
|
||||
// Note: the box is already stored back via storeShulker above, so we can't rescan it here
|
||||
// Instead, mark it as needing rescan on next organize
|
||||
} catch (e) {
|
||||
console.error(`Storage: Could not mark partial shulker for re-queue:`, e.message);
|
||||
}
|
||||
failedToUnpack.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,12 +707,15 @@ class Storage {
|
||||
/**
|
||||
* Store a shulker box from bot inventory into an empty chest slot.
|
||||
* Scans NBT to register contents in DB.
|
||||
* @returns {{ chestId, chestSlot, shulkerId, itemFocus }} or false on failure
|
||||
* @param {object} shulkerItem - The mineflayer item object for the shulker box
|
||||
* @param {object|null} targetSlot - Optional specific slot to store in: { chest_id, pos_x, pos_y, pos_z, slot }
|
||||
* @returns {{ chestId, chestSlot, chestPos, shulkerId, itemFocus }} or false on failure
|
||||
*/
|
||||
async storeShulker(shulkerItem) {
|
||||
async storeShulker(shulkerItem, targetSlot = null) {
|
||||
console.log(`Storage: Storing shulker box (${shulkerItem.name})`);
|
||||
|
||||
const emptySlot = await Database.findEmptyChestSlot();
|
||||
// Use provided target slot, or find any empty slot
|
||||
const emptySlot = targetSlot || await Database.findEmptyChestSlot();
|
||||
if (!emptySlot) {
|
||||
console.log('Storage: No empty chest slot for shulker');
|
||||
return false;
|
||||
@@ -460,6 +776,8 @@ class Storage {
|
||||
chestPos,
|
||||
shulkerId: shulkerRecord ? shulkerRecord.id : null,
|
||||
itemFocus: shulkerRecord ? shulkerRecord.item_focus : null,
|
||||
// Track if this was a forced slot placement (used by Phase 2 to return box to original slot)
|
||||
isOriginalSlot: targetSlot !== null,
|
||||
};
|
||||
|
||||
console.log(`Storage: Shulker stored at chest ${info.chestId}, slot ${info.chestSlot} (focus: ${info.itemFocus || 'mixed/empty'})`);
|
||||
@@ -684,7 +1002,8 @@ class Storage {
|
||||
// Log trade
|
||||
await Database.logTrade(playerName, 'withdraw', [{ name: itemName, count: totalWithdrawn }]);
|
||||
|
||||
this.bot.whisper(playerName, `${totalWithdrawn}x ${itemName} ready. Use /trade to collect within 5 minutes.`);
|
||||
// Initiate trade with the player to hand over items
|
||||
await this.initiateTradeWithPlayer(playerName);
|
||||
} else {
|
||||
this.bot.whisper(playerName, `Failed to withdraw ${itemName}.`);
|
||||
}
|
||||
@@ -748,7 +1067,8 @@ class Storage {
|
||||
|
||||
await Database.logTrade(playerName, 'withdraw', [{ name: itemName, count: taken, mode: 'shulkers' }]);
|
||||
|
||||
this.bot.whisper(playerName, `${taken} shulker(s) of ${itemName} ready. Use /trade to collect within 5 minutes.`);
|
||||
// Initiate trade with the player to hand over shulkers
|
||||
await this.initiateTradeWithPlayer(playerName);
|
||||
} else {
|
||||
this.bot.whisper(playerName, `Failed to withdraw shulkers of ${itemName}.`);
|
||||
}
|
||||
@@ -798,6 +1118,102 @@ class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiate a /trade with a player and hand over pending withdrawal items.
|
||||
* Sends the trade request, waits for the window, places items, and confirms.
|
||||
* Falls back to the old "use /trade" message if the player is offline or trade fails.
|
||||
*/
|
||||
async initiateTradeWithPlayer(playerName) {
|
||||
const pending = this.pendingWithdrawals.get(playerName);
|
||||
if (!pending) return;
|
||||
|
||||
const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21];
|
||||
|
||||
// Check if player is online
|
||||
const player = this.bot.bot.players[playerName];
|
||||
if (!player) {
|
||||
this.bot.whisper(playerName, `${pending.count}x ${pending.itemName} ready. Use /trade to collect within 5 minutes.`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`Storage: Initiating /trade with ${playerName}`);
|
||||
this.bot.whisper(playerName, `${pending.count}x ${pending.itemName} ready — sending trade request...`);
|
||||
await this.bot.say(`/trade ${playerName}`);
|
||||
|
||||
// Wait for trade window to open (player must accept)
|
||||
const window = await Promise.race([
|
||||
this.bot.once('windowOpen'),
|
||||
sleep(60000).then(() => null), // 60s timeout
|
||||
]);
|
||||
|
||||
if (!window) {
|
||||
console.log(`Storage: Trade request to ${playerName} timed out`);
|
||||
this.bot.whisper(playerName, `Trade request timed out. Use /trade to collect within 5 minutes.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Place items in bot's trade slots
|
||||
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) continue;
|
||||
|
||||
if (pending.mode === 'shulkers') {
|
||||
if (!item.name.includes('shulker_box')) continue;
|
||||
} else {
|
||||
if (item.name !== pending.itemName) continue;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.bot.bot.moveSlotItem(i, slotNum);
|
||||
await sleep(200);
|
||||
placed++;
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(`Storage: Could not move item to trade slot ${slotNum}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Storage: Placed ${placed} stack(s) in trade window for ${playerName}`);
|
||||
|
||||
// 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);
|
||||
|
||||
const confirmationCheck = setInterval(async () => {
|
||||
try {
|
||||
const indicator = window.slots[53];
|
||||
if (indicator && indicator.name === 'lime_dye') {
|
||||
this.bot.bot.moveSlotItem(37, 37);
|
||||
}
|
||||
} catch (e) { /* window may have closed */ }
|
||||
}, 500);
|
||||
|
||||
// Wait for trade to complete
|
||||
await this.bot.once('windowClose');
|
||||
clearInterval(confirmationCheck);
|
||||
|
||||
if (timeoutHandle._destroyed) return;
|
||||
clearTimeout(timeoutHandle);
|
||||
|
||||
// Withdrawal complete — clear pending
|
||||
if (pending.timeoutId) clearTimeout(pending.timeoutId);
|
||||
this.pendingWithdrawals.delete(playerName);
|
||||
this.bot.whisper(playerName, `Withdrawal complete! Enjoy your ${pending.itemName}.`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Storage: Error initiating trade with ${playerName}:`, error.message);
|
||||
this.bot.whisper(playerName, `Trade failed. Use /trade to collect within 5 minutes.`);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Crafting
|
||||
// ========================================
|
||||
@@ -1020,11 +1436,24 @@ class Storage {
|
||||
* Called periodically and as pre/post-flight during organize.
|
||||
*/
|
||||
async cleanInventory() {
|
||||
const hotbarNames = new Set((this.config.hotbarItems || []).map(h => h.name));
|
||||
const hotbarTargets = {};
|
||||
for (const h of (this.config.hotbarItems || [])) {
|
||||
hotbarTargets[h.name] = h.target || 0;
|
||||
}
|
||||
const hotbarSeen = {};
|
||||
const grouped = {};
|
||||
for (const item of this.bot.bot.inventory.items()) {
|
||||
if (item.name.includes('shulker_box')) continue;
|
||||
if (hotbarNames.has(item.name)) continue;
|
||||
if (item.name in hotbarTargets) {
|
||||
const seen = (hotbarSeen[item.name] || 0);
|
||||
const keep = Math.max(0, hotbarTargets[item.name] - seen);
|
||||
hotbarSeen[item.name] = seen + item.count;
|
||||
const excess = item.count - keep;
|
||||
if (excess <= 0) continue;
|
||||
if (!grouped[item.name]) grouped[item.name] = 0;
|
||||
grouped[item.name] += excess;
|
||||
continue;
|
||||
}
|
||||
if (!grouped[item.name]) grouped[item.name] = 0;
|
||||
grouped[item.name] += item.count;
|
||||
}
|
||||
@@ -1065,6 +1494,10 @@ class Storage {
|
||||
// Pre-flight: deposit any stray items in bot inventory
|
||||
await this.cleanInventory();
|
||||
|
||||
// Also detect and unpack any mixed shulkers that ended up in chests
|
||||
// (e.g., from failed Phase 2 unpacks or shulkers that were manually placed)
|
||||
await this.organizeMixedShulkers();
|
||||
|
||||
const chests = await Database.getChestsWithLooseItems();
|
||||
console.log(`Storage: ${chests.length} chest(s) have loose items in DB`);
|
||||
|
||||
@@ -1242,6 +1675,97 @@ class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and unpack mixed/unsorted shulkers during the organize pass.
|
||||
* This is the recovery path for shulkers that failed Phase 2 unpacking
|
||||
* or were otherwise left with mixed contents.
|
||||
*/
|
||||
async organizeMixedShulkers() {
|
||||
console.log('Storage: Checking for mixed shulkers to unpack...');
|
||||
|
||||
// Find shulkers with null item_focus (mixed/unsorted) that have items in them
|
||||
// These are candidates for unpacking
|
||||
const mixedShulkers = await Database.db.all(`
|
||||
SELECT s.*, c.pos_x, c.pos_y, c.pos_z, c.chest_type
|
||||
FROM shulkers s
|
||||
INNER JOIN chests c ON c.id = s.chest_id
|
||||
WHERE s.item_focus IS NULL AND s.total_items > 0 AND s.slot_count >= 0
|
||||
ORDER BY s.id ASC
|
||||
`);
|
||||
|
||||
if (mixedShulkers.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Storage: Found ${mixedShulkers.length} mixed shulker(s) to unpack`);
|
||||
|
||||
for (const shulker of mixedShulkers) {
|
||||
// Check inventory space
|
||||
const freeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
|
||||
if (freeSlots < 3) {
|
||||
console.log(`Storage: Inventory too full (${freeSlots} free slots), stopping mixed shulker organize`);
|
||||
break;
|
||||
}
|
||||
|
||||
const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
|
||||
console.log(`Storage: Unpacking mixed shulker #${shulker.id} from chest at ${chestPos}`);
|
||||
|
||||
try {
|
||||
// Clean up any leftover shulkers first
|
||||
const leftoverBoxes = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
|
||||
for (const leftover of leftoverBoxes) {
|
||||
try { await this.storeShulker(leftover); } catch (e) { /* ignore */ }
|
||||
}
|
||||
await this._depositNonShulkerInventory();
|
||||
|
||||
// takeWholeShulker marks slot_count=-1 in DB (in-transit)
|
||||
await this.shulkerHandler.takeWholeShulker(this.bot, chestPos, shulker.slot, shulker.id);
|
||||
|
||||
const shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (!shulkerItem) {
|
||||
console.error('Storage: Shulker vanished during organizeMixedShulkers');
|
||||
// Upsert it back so it's not orphaned
|
||||
await Database.upsertShulker(shulker.chest_id, shulker.slot, 'shulker_box', null, null);
|
||||
await Database.updateShulkerCounts(shulker.id, shulker.slot_count, shulker.total_items);
|
||||
continue;
|
||||
}
|
||||
|
||||
const { extracted, inventoryFull } = await this.shulkerHandler.unpackShulkerFromInventory(this.bot, shulkerItem);
|
||||
console.log(`Storage: Mixed shulker unpacked — ${extracted.length} stacks extracted`);
|
||||
|
||||
// Deposit all extracted items
|
||||
await this._depositNonShulkerInventory();
|
||||
|
||||
// Return empty (or partially-empty) box to its original slot
|
||||
const emptyBox = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (emptyBox) {
|
||||
try {
|
||||
const result = await this.storeShulker(emptyBox, {
|
||||
chest_id: shulker.chest_id,
|
||||
pos_x: shulker.pos_x,
|
||||
pos_y: shulker.pos_y,
|
||||
pos_z: shulker.pos_z,
|
||||
slot: shulker.slot,
|
||||
});
|
||||
if (!result) {
|
||||
await this.storeShulker(emptyBox);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Storage: Error storing mixed shulker box:`, e.message);
|
||||
try { await this.storeShulker(emptyBox); } catch (e2) { /* give up */ }
|
||||
}
|
||||
}
|
||||
} 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 */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Consolidate partially filled shulkers of the same item type.
|
||||
@@ -1400,7 +1924,9 @@ class Storage {
|
||||
});
|
||||
|
||||
await Database.logTrade(playerName, 'withdraw', [{ name: itemRow.item_name, count: withdrawn, special: true }]);
|
||||
this.bot.whisper(playerName, `${withdrawn}x ${itemRow.item_name} (special) ready. Use /trade to collect within 5 minutes.`);
|
||||
|
||||
// Initiate trade with the player to hand over special item
|
||||
await this.initiateTradeWithPlayer(playerName);
|
||||
} else {
|
||||
this.bot.whisper(playerName, `Failed to withdraw special item.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user