Files
mc-bot-town/nodejs/controller/storage/index.js
T
2026-02-22 20:27:09 -05:00

1553 lines
52 KiB
JavaScript

'use strict';
const Vec3 = require('vec3');
const conf = require('../../conf');
const { sleep } = require('../../utils');
const Database = require('./database');
const Scanner = require('./scanner');
const ShulkerHandler = require('./shulker-handler');
const StorageWeb = require('./web');
class Storage {
static createRouter = StorageWeb.createRouter;
static webUI = StorageWeb.webUI;
constructor(args) {
console.log('Storage: Constructor called');
this.bot = args.bot;
this.config = { ...conf.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
}
init() {
console.log('Storage: init() called');
return new Promise((resolve, reject) => {
this.bot.on('onReady', async () => {
try {
console.log(`Storage: Bot ${this.bot.name} is ready, initializing storage system...`);
// Initialize database
if (!Database.db) {
console.log('Storage: Initializing database...');
await Database.initialize(this.config.dbPath || './storage/storage.db');
} else {
console.log('Storage: Database already initialized');
}
// Initialize scanner
console.log('Storage: Creating Scanner...');
this.scanner = new Scanner();
this.shulkerHandler.scanner = this.scanner;
if (this.config.startupTasks) {
console.log('Storage: Running startup tasks...');
await this.config.startupTasks();
}
this.isReady = true;
console.log('Storage: Initialization complete! Ready to use.');
if (!this.bot.onDemand) {
// Initial hotbar restock
try {
await this.restockHotbar();
} catch (error) {
console.error('Storage: Initial hotbar restock failed:', error.message);
}
// Start hotbar restock interval
const restockInterval = this.config.hotbarRestockInterval || 60000;
this.hotbarInterval = setInterval(async () => {
try {
await this.restockHotbar();
} catch (error) {
console.error('Storage: Hotbar restock interval failed:', error.message);
}
}, restockInterval);
// Start periodic inventory cleanup
const cleanupInterval = this.config.inventoryCleanupInterval || 5 * 60 * 1000;
this.cleanupInterval = setInterval(async () => {
if (!this.isReady || this._busy || this.shulkerHandler.operationInProgress) return;
try {
await this.cleanInventory();
} catch (error) {
console.error('Storage: Periodic inventory cleanup failed:', error.message);
}
}, cleanupInterval);
}
resolve();
} catch (error) {
console.error('Storage: Error in init:', error);
reject(error);
}
});
});
}
async unload(keepDb = false) {
console.log('Storage: Unloading...');
if (this.hotbarInterval) {
clearInterval(this.hotbarInterval);
this.hotbarInterval = null;
}
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
this.cleanupInterval = null;
}
// Clear any pending withdrawal timeouts
for (const [, pending] of this.pendingWithdrawals) {
if (pending.timeoutId) clearTimeout(pending.timeoutId);
}
this.pendingWithdrawals.clear();
if (this.scanner) delete this.scanner;
if (!keepDb && Database.db) {
console.log('Storage: Closing database...');
Database.close();
}
this.isReady = false;
console.log('Storage: Unload complete');
}
async scanArea(force = false) {
console.log(`Storage[${this.bot.name}]: Scanning storage area...`);
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`);
}
// ========================================
// Hotbar Management
// ========================================
async restockHotbar() {
if (!this.isReady) return;
if (this._busy) {
console.log('Storage: Skipping hotbar restock — storage operation in progress');
return;
}
if (this.shulkerHandler.operationInProgress) {
console.log('Storage: Skipping hotbar restock — shulker operation in progress');
return;
}
const hotbarItems = this.config.hotbarItems || [];
if (hotbarItems.length === 0) return;
for (const spec of hotbarItems) {
// Count current inventory
let currentCount = 0;
for (const item of this.bot.bot.inventory.items()) {
if (item.name === spec.name) {
currentCount += item.count;
}
}
if (currentCount >= spec.min) continue;
const needed = spec.target - currentCount;
console.log(`Storage: Hotbar restock — ${spec.name}: have ${currentCount}, need ${needed} more (target ${spec.target})`);
// Check storage availability
const available = await Database.getItemTotalCount(spec.name);
if (available === 0) {
console.log(`Storage: No ${spec.name} in storage, skipping`);
continue;
}
const toWithdraw = Math.min(needed, available);
const shulkers = await Database.findShulkersWithItem(spec.name);
let remaining = toWithdraw;
let consecutiveFailures = 0;
for (const shulker of shulkers) {
if (remaining <= 0) break;
if (consecutiveFailures >= 2) {
console.log(`Storage: Too many failures restocking ${spec.name}, giving up`);
break;
}
const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
try {
const { withdrawn, updatedSlotItem } = await this.shulkerHandler.withdrawFromShulker(
this.bot, chestPos, shulker.slot, spec.name, remaining, shulker.id, shulker.chest_id
);
if (withdrawn > 0) {
remaining -= withdrawn;
consecutiveFailures = 0;
} else {
consecutiveFailures++;
}
} catch (error) {
console.error(`Storage: Hotbar restock error for ${spec.name}:`, error.message);
consecutiveFailures++;
}
}
console.log(`Storage: Restocked ${toWithdraw - remaining}x ${spec.name}`);
}
await Database.rebuildItemIndex();
}
// ========================================
// Deposit Flow
// ========================================
async handleTrade(playerName, itemsReceived) {
console.log(`Storage[${this.bot.name}]: Processing trade from ${playerName}, received ${itemsReceived.length} item types`);
this._craftAvailable = true; // new items may include craft materials
this._busy = true;
try {
// Log trade
await Database.logTrade(playerName, 'deposit', itemsReceived);
// Unpack any traded shulker boxes before depositing
const hasShulkers = itemsReceived.some(item => item.name.includes('shulker_box'));
if (hasShulkers) {
console.log('Storage: Traded items include shulker boxes, unpacking...');
await this.unpackTradedShulkers();
}
// 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));
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 (!grouped[item.name]) {
grouped[item.name] = 0;
}
grouped[item.name] += item.count;
}
// Deposit each item type
const results = [];
for (const [itemName, totalCount] of Object.entries(grouped)) {
try {
const result = await this.depositItemType(itemName, totalCount);
results.push({ itemName, requested: totalCount, deposited: result.deposited });
} catch (error) {
console.error(`Storage: Error depositing ${itemName}:`, error);
results.push({ itemName, requested: totalCount, deposited: 0, error: error.message });
}
}
// Rebuild index after all deposits
await Database.rebuildItemIndex();
// 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(', ');
this.bot.whisper(playerName, `Storage complete: ${summary}`);
return results;
} finally {
this._busy = false;
}
}
/**
* 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.
* 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 }
console.log('Storage: Phase 1 — stashing all traded shulkers into chests');
let stashCount = 0;
while (stashCount < 50) {
const shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
if (!shulkerItem) break;
stashCount++;
const contents = this.scanner.extractShulkerContents(this.bot, shulkerItem);
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]})`;
console.log(`Storage: Stashing shulker ${stashCount}: ${label}`);
try {
const info = await this.storeShulker(shulkerItem);
if (!info) {
console.error('Storage: No empty chest slot, cannot stash more shulkers');
break;
}
if (isMixed) {
mixedShulkers.push(info);
}
} catch (error) {
console.error(`Storage: Error stashing shulker:`, error.message);
break;
}
}
console.log(`Storage: Phase 1 complete — stashed ${stashCount} shulkers, ${mixedShulkers.length} need unpacking`);
// Phase 2: Pull each mixed shulker back out, unpack it, deposit items, store empty box.
// 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})`);
try {
// Take the shulker from the chest into inventory
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'));
if (!shulkerItem) {
console.error('Storage: Shulker not found in inventory after taking from chest');
continue;
}
const { extracted, inventoryFull } = await this.shulkerHandler.unpackShulkerFromInventory(this.bot, shulkerItem);
console.log(`Storage: Extracted ${extracted.length} item stacks from mixed shulker`);
// Deposit all extracted items
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);
// Deposit whatever we managed to extract
await this._depositNonShulkerInventory();
// Try to store any shulker box left in inventory
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 */ }
}
}
}
// Final cleanup: store any shulker boxes still in inventory
let remaining = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
for (const leftover of remaining) {
try { await this.storeShulker(leftover); } catch (e) {
console.error(`Storage: Error storing leftover shulker:`, e.message);
}
}
console.log('Storage: All traded shulkers processed');
}
/**
* Deposit all non-shulker, non-hotbar items from bot inventory.
* Used during trade unpack to free inventory space between shulkers.
*/
async _depositNonShulkerInventory() {
const hotbarNames = new Set((this.config.hotbarItems || []).map(h => h.name));
const todeposit = {};
for (const item of this.bot.bot.inventory.items()) {
if (item.name.includes('shulker_box')) continue;
if (hotbarNames.has(item.name)) continue;
if (!todeposit[item.name]) todeposit[item.name] = 0;
todeposit[item.name] += item.count;
}
for (const [itemName, count] of Object.entries(todeposit)) {
try {
await this.depositItemType(itemName, count);
} catch (error) {
console.error(`Storage: Error depositing ${itemName} during unpack:`, error.message);
}
}
}
/**
* 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
*/
async storeShulker(shulkerItem) {
console.log(`Storage: Storing shulker box (${shulkerItem.name})`);
const emptySlot = await Database.findEmptyChestSlot();
if (!emptySlot) {
console.log('Storage: No empty chest slot for shulker');
return false;
}
const chestPos = new Vec3(emptySlot.pos_x, emptySlot.pos_y, emptySlot.pos_z);
await this.bot.goTo({ where: chestPos, range: 3 });
const chestBlock = this.bot.bot.blockAt(chestPos);
const window = await this.bot.openContainer(chestBlock);
await sleep(300);
// Find the specific shulker in inventory portion of the window by matching slot
let shulkerWindowSlot = null;
for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
const item = window.slots[i];
if (item && item.slot === shulkerItem.slot && item.name.includes('shulker_box')) {
shulkerWindowSlot = i;
break;
}
}
// Fallback: find any shulker box if slot match failed
if (shulkerWindowSlot === null) {
for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
const item = window.slots[i];
if (item && item.name.includes('shulker_box')) {
shulkerWindowSlot = i;
break;
}
}
}
if (shulkerWindowSlot === null) {
await this.bot.bot.closeWindow(window);
console.log('Storage: Shulker not found in window inventory');
return false;
}
await this.bot.bot.moveSlotItem(shulkerWindowSlot, emptySlot.slot);
await sleep(300);
// Read NBT from the placed slot and scan actual contents
const placedItem = window.slots[emptySlot.slot];
await this.bot.bot.closeWindow(window);
await sleep(200);
// Register in DB with actual contents via NBT scan
let shulkerRecord = null;
if (placedItem) {
await this.scanner.scanShulkerFromNBT(this.bot, Database, emptySlot.chest_id, emptySlot.slot, placedItem);
// Look up the record that was just created/updated
const chestShulkers = await Database.getShulkersByChest(emptySlot.chest_id);
shulkerRecord = chestShulkers.find(s => s.slot === emptySlot.slot);
}
const info = {
chestId: emptySlot.chest_id,
chestSlot: emptySlot.slot,
chestPos,
shulkerId: shulkerRecord ? shulkerRecord.id : null,
itemFocus: shulkerRecord ? shulkerRecord.item_focus : null,
};
console.log(`Storage: Shulker stored at chest ${info.chestId}, slot ${info.chestSlot} (focus: ${info.itemFocus || 'mixed/empty'})`);
return info;
}
/**
* Check whether a mineflayer inventory item has special NBT (custom name, lore, custom model data).
*/
_isItemSpecial(item) {
if (!item.nbt) return false;
const tag = item.nbt?.value?.tag?.value || item.nbt?.value || item.nbt;
const parsed = this.scanner.parseNBT(tag);
return Scanner.isSpecialItem(parsed);
}
async depositItemType(itemName, count, excludeShulkerId = null) {
console.log(`Storage: Depositing ${count}x ${itemName}`);
// Split inventory items into regular and special
const allInvItems = this.bot.bot.inventory.items().filter(i => i.name === itemName);
const regularCount = allInvItems.filter(i => !this._isItemSpecial(i)).reduce((s, i) => s + i.count, 0);
const specialCount = allInvItems.filter(i => this._isItemSpecial(i)).reduce((s, i) => s + i.count, 0);
let totalDeposited = 0;
// Deposit regular items first (filter out special ones during shift-click)
if (regularCount > 0) {
const toDeposit = Math.min(regularCount, count);
const result = await this._depositItemBatch(
itemName, toDeposit, itemName,
(item) => !this._isItemSpecial(item),
excludeShulkerId
);
totalDeposited += result.deposited;
}
// Then deposit special items (filter out regular ones during shift-click)
if (specialCount > 0 && totalDeposited < count) {
const remaining = Math.min(specialCount, count - totalDeposited);
console.log(`Storage: Depositing ${remaining}x ${itemName} (special) separately`);
const result = await this._depositItemBatch(
itemName, remaining, itemName + '#special',
(item) => this._isItemSpecial(item),
excludeShulkerId
);
totalDeposited += result.deposited;
}
console.log(`Storage: Deposited ${totalDeposited}/${count} ${itemName}`);
return { deposited: totalDeposited };
}
/**
* Internal: deposit items of a given type into shulkers with a specific focus.
* @param {string} itemName - The minecraft item name to match
* @param {number} count - How many to deposit
* @param {string} focus - The shulker item_focus to target (e.g. 'diamond_sword' or 'diamond_sword#special')
* @param {Function|null} itemFilter - Filter passed to depositIntoShulker to control which slots get shift-clicked
* @param {number|null} excludeShulkerId - Shulker ID to skip
*/
async _depositItemBatch(itemName, count, focus, itemFilter, excludeShulkerId = null) {
let remaining = count;
let totalDeposited = 0;
let failedAttempts = 0;
while (remaining > 0) {
if (failedAttempts >= 3) {
console.log(`Storage: ${failedAttempts} consecutive deposit failures for ${itemName} (focus=${focus}), giving up`);
break;
}
// Verify we actually still have matching items in inventory
const actualInvItems = this.bot.bot.inventory.items().filter(i => i.name === itemName);
const matchingCount = itemFilter
? actualInvItems.filter(itemFilter).reduce((sum, i) => sum + i.count, 0)
: actualInvItems.reduce((sum, i) => sum + i.count, 0);
if (matchingCount === 0) {
console.log(`Storage: No more matching ${itemName} in inventory, done`);
break;
}
remaining = Math.min(remaining, matchingCount);
// Try to find an existing shulker with this focus that has space
let shulker = await Database.findShulkerWithSpace(focus, excludeShulkerId);
let wasEmpty = false;
console.log(`Storage: findShulkerWithSpace('${focus}') → ${shulker ? `shulker ${shulker.id} (slot_count=${shulker.slot_count}, focus=${shulker.item_focus})` : 'none'}`);
if (!shulker) {
// No matching shulker, find an empty one
shulker = await Database.findEmptyShulker(excludeShulkerId);
if (!shulker) {
// No empty shulkers - try crafting one (skip if crafting already failed this cycle)
if (this._craftAvailable) {
console.log(`Storage: No empty shulkers, attempting to craft one`);
try {
await this.craftShulkerBox();
shulker = await Database.findEmptyShulker();
} catch (error) {
console.error('Storage: Failed to craft shulker:', error.message);
this._craftAvailable = false;
}
}
if (!shulker) {
console.log(`Storage: Cannot deposit ${remaining}x ${itemName} - no available shulkers`);
break;
}
}
// Set the item focus for this empty shulker
await Database.updateShulkerItemFocus(shulker.id, focus);
wasEmpty = true;
}
const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
try {
const { deposited, updatedSlotItem } = await this.shulkerHandler.depositIntoShulker(
this.bot, chestPos, shulker.slot, itemName, remaining, shulker.id, shulker.chest_id, itemFilter
);
totalDeposited += deposited;
remaining -= deposited;
if (deposited === 0) {
failedAttempts++;
console.log(`Storage: Shulker full or deposit failed (attempt ${failedAttempts}/3), trying next`);
// Reset item_focus if we just assigned it — don't orphan empty shulkers
if (wasEmpty) {
await Database.updateShulkerItemFocus(shulker.id, null);
}
continue;
}
failedAttempts = 0;
} catch (error) {
console.error(`Storage: Error in deposit cycle:`, error);
if (wasEmpty) {
await Database.updateShulkerItemFocus(shulker.id, null);
}
break;
}
}
return { deposited: totalDeposited };
}
// ========================================
// Withdrawal Flow
// ========================================
async handleWithdrawRequest(playerName, itemName, count) {
console.log(`Storage[${this.bot.name}]: Withdraw request from ${playerName}: ${itemName} x${count}`);
this._busy = true;
try {
// Check total available
const totalAvailable = await Database.getItemTotalCount(itemName);
if (totalAvailable === 0) {
return this.bot.whisper(playerName, `Item not found: ${itemName}`);
}
const actualCount = Math.min(count, totalAvailable);
if (actualCount < count) {
this.bot.whisper(playerName, `Only ${totalAvailable} ${itemName} available. Withdrawing ${actualCount}.`);
}
// Find shulkers containing this item
const shulkers = await Database.findShulkersWithItem(itemName);
if (!shulkers || shulkers.length === 0) {
return this.bot.whisper(playerName, `Cannot find ${itemName} in any shulker`);
}
let remaining = actualCount;
let totalWithdrawn = 0;
let consecutiveFailures = 0;
for (const shulker of shulkers) {
if (remaining <= 0) break;
if (consecutiveFailures >= 3) {
console.log(`Storage: Too many withdraw failures for ${itemName}, stopping`);
break;
}
const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
try {
const { withdrawn, updatedSlotItem } = await this.shulkerHandler.withdrawFromShulker(
this.bot, chestPos, shulker.slot, itemName, remaining, shulker.id, shulker.chest_id
);
if (withdrawn > 0) {
totalWithdrawn += withdrawn;
remaining -= withdrawn;
consecutiveFailures = 0;
} else {
consecutiveFailures++;
}
} catch (error) {
console.error(`Storage: Error withdrawing from shulker ${shulker.id}:`, error.message);
consecutiveFailures++;
}
}
// Rebuild index after withdrawals
await Database.rebuildItemIndex();
if (totalWithdrawn > 0) {
// Set up timeout to re-store items if not collected
const timeoutId = setTimeout(() => {
this.restoreWithdrawal(playerName);
}, 5 * 60 * 1000);
// Store pending withdrawal for trade pickup
this.pendingWithdrawals.set(playerName, {
itemName,
count: totalWithdrawn,
mode: 'items',
timeoutId,
});
// 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.`);
} else {
this.bot.whisper(playerName, `Failed to withdraw ${itemName}.`);
}
} finally {
this._busy = false;
}
}
async handleWithdrawShulkers(playerName, itemName, shulkerCount) {
console.log(`Storage[${this.bot.name}]: Shulker withdraw request from ${playerName}: ${shulkerCount} shulkers of ${itemName}`);
this._busy = true;
try {
if (shulkerCount > 12) {
this.bot.whisper(playerName, `Trade window only has 12 slots. Limiting to 12 shulkers.`);
shulkerCount = 12;
}
// Find shulkers with this item that have items in them
const shulkers = await Database.findShulkersWithItem(itemName);
if (!shulkers || shulkers.length === 0) {
return this.bot.whisper(playerName, `No shulkers containing ${itemName} found`);
}
const available = shulkers.length;
const actualCount = Math.min(shulkerCount, available);
if (actualCount < shulkerCount) {
this.bot.whisper(playerName, `Only ${available} shulkers of ${itemName} available. Withdrawing ${actualCount}.`);
}
let taken = 0;
for (let i = 0; i < actualCount; i++) {
const shulker = shulkers[i];
const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
try {
await this.shulkerHandler.takeWholeShulker(this.bot, chestPos, shulker.slot, shulker.id);
// Also fully delete from DB — it's leaving storage permanently
await Database.deleteShulker(shulker.id);
taken++;
} catch (error) {
console.error(`Storage: Error taking whole shulker ${shulker.id}:`, error.message);
}
}
await Database.rebuildItemIndex();
if (taken > 0) {
// Set up timeout to re-store shulkers if not collected
const timeoutId = setTimeout(() => {
this.restoreWithdrawal(playerName);
}, 5 * 60 * 1000);
this.pendingWithdrawals.set(playerName, {
itemName,
count: taken,
mode: 'shulkers',
timeoutId,
});
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.`);
} else {
this.bot.whisper(playerName, `Failed to withdraw shulkers of ${itemName}.`);
}
} finally {
this._busy = false;
}
}
async restoreWithdrawal(playerName) {
const pending = this.pendingWithdrawals.get(playerName);
if (!pending) return;
console.log(`Storage: Withdrawal timeout for ${playerName} — restoring ${pending.count}x ${pending.itemName} (mode: ${pending.mode || 'items'})`);
// Clear timeout ref
if (pending.timeoutId) {
clearTimeout(pending.timeoutId);
}
this.pendingWithdrawals.delete(playerName);
try {
if (pending.mode === 'shulkers') {
// Re-store whole shulker boxes
const shulkerItems = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
for (const shulkerItem of shulkerItems) {
await this.storeShulker(shulkerItem);
}
} else {
// Re-deposit individual items
const grouped = {};
for (const item of this.bot.bot.inventory.items()) {
if (item.name === pending.itemName) {
if (!grouped[item.name]) grouped[item.name] = 0;
grouped[item.name] += item.count;
}
}
for (const [itemName, count] of Object.entries(grouped)) {
await this.depositItemType(itemName, count);
}
}
await Database.rebuildItemIndex();
this.bot.whisper(playerName, `Withdrawal cancelled — ${pending.count}x ${pending.itemName} returned to storage.`);
} catch (error) {
console.error(`Storage: Error restoring withdrawal for ${playerName}:`, error.message);
this.bot.whisper(playerName, `Withdrawal timed out but failed to re-store items. Please contact an admin.`);
}
}
// ========================================
// Crafting
// ========================================
async craftShulkerBox() {
console.log('Storage: Attempting to craft shulker box');
// Check bot inventory first for materials
let shellsInInv = 0;
let chestsInInv = 0;
for (const item of this.bot.bot.inventory.items()) {
if (item.name === 'shulker_shell') shellsInInv += item.count;
if (item.name === 'chest') chestsInInv += item.count;
}
// Take enough shells for the recipe AND to restock to hotbar target (saves repeat trips)
const shellHotbar = (this.config.hotbarItems || []).find(h => h.name === 'shulker_shell');
const shellTarget = shellHotbar ? shellHotbar.target : 2;
let shellsNeeded = Math.max(0, Math.max(2, shellTarget) - shellsInInv);
let chestsNeeded = Math.max(0, 1 - chestsInInv);
console.log(`Storage: Craft materials — inventory: ${shellsInInv} shells, ${chestsInInv} chests. Need from storage: ${shellsNeeded} shells (target ${shellTarget}), ${chestsNeeded} chests`);
// Only check/withdraw from storage if inventory doesn't have enough
if (shellsNeeded > 0) {
const shellCount = await Database.getItemTotalCount('shulker_shell');
const minForRecipe = Math.max(0, 2 - shellsInInv);
if (shellCount < minForRecipe) {
throw new Error(`Not enough shulker shells (have ${shellsInInv} inv + ${shellCount} storage, need 2)`);
}
// Take up to target, but at least enough for the recipe
shellsNeeded = Math.min(shellsNeeded, shellCount);
const shellShulkers = await Database.findShulkersWithItem('shulker_shell');
// Sort by distance so the bot tries nearby chests first
const botPos = this.bot.bot.entity.position;
shellShulkers.sort((a, b) => {
const da = botPos.distanceTo(new Vec3(a.pos_x, a.pos_y, a.pos_z));
const db = botPos.distanceTo(new Vec3(b.pos_x, b.pos_y, b.pos_z));
return da - db;
});
let shellsObtained = 0;
for (const shulker of shellShulkers) {
if (shellsObtained >= shellsNeeded) break;
const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
this.bot, chestPos, shulker.slot, 'shulker_shell', shellsNeeded - shellsObtained,
shulker.id, shulker.chest_id
);
shellsObtained += withdrawn;
}
if (shellsObtained < minForRecipe) {
throw new Error(`Failed to gather shells: got ${shellsObtained}/${minForRecipe} from storage`);
}
}
if (chestsNeeded > 0) {
const chestCount = await Database.getItemTotalCount('chest');
if (chestCount < chestsNeeded) {
throw new Error(`Not enough chests (have ${chestsInInv} inv + ${chestCount} storage, need 1)`);
}
const chestShulkers = await Database.findShulkersWithItem('chest');
chestShulkers.sort((a, b) => {
const da = botPos.distanceTo(new Vec3(a.pos_x, a.pos_y, a.pos_z));
const db = botPos.distanceTo(new Vec3(b.pos_x, b.pos_y, b.pos_z));
return da - db;
});
let chestsObtained = 0;
for (const shulker of chestShulkers) {
if (chestsObtained >= chestsNeeded) break;
const chestPos = new Vec3(shulker.pos_x, shulker.pos_y, shulker.pos_z);
const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
this.bot, chestPos, shulker.slot, 'chest', chestsNeeded - chestsObtained,
shulker.id, shulker.chest_id
);
chestsObtained += withdrawn;
}
if (chestsObtained < chestsNeeded) {
throw new Error(`Failed to gather chests: got ${chestsObtained}/${chestsNeeded} from storage`);
}
}
// Find or navigate to crafting table
const craftingTablePos = this.config.craftingTablePos;
let craftingTable;
if (craftingTablePos) {
craftingTable = this.bot.bot.blockAt(new Vec3(craftingTablePos.x, craftingTablePos.y, craftingTablePos.z));
} else {
craftingTable = this.bot.bot.findBlock({
matching: this.bot.mcData.blocksByName.crafting_table?.id,
maxDistance: 32,
});
}
if (!craftingTable) {
throw new Error('No crafting table found nearby');
}
await this.bot.goTo({ where: craftingTable.position, range: 3 });
// Craft shulker box manually (bot.craft() broken on 1.21+)
const shulkerBoxRecipes = this.bot.bot.recipesAll(
this.bot.mcData.itemsByName.shulker_box.id,
null,
craftingTable
);
if (!shulkerBoxRecipes || shulkerBoxRecipes.length === 0) {
throw new Error('No recipe found for shulker box');
}
const recipe = shulkerBoxRecipes[0];
const window = await this.bot.openCraftingTable(craftingTable);
const inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd);
// Group grid slots by ingredient type (handles single stack needing multiple grid slots)
const ingredientsByType = {};
for (let row = 0; row < recipe.inShape.length; row++) {
for (let col = 0; col < recipe.inShape[row].length; col++) {
const shape = recipe.inShape[row][col];
if (shape.id === -1) continue;
const gridSlot = row * 3 + col + 1;
if (!ingredientsByType[shape.id]) ingredientsByType[shape.id] = [];
ingredientsByType[shape.id].push(gridSlot);
}
}
// For each ingredient type: pick up stack, right-click into each grid slot, put stack back
for (const [typeId, gridSlots] of Object.entries(ingredientsByType)) {
const invIdx = inventory.findIndex(el => el && el.type === parseInt(typeId));
if (invIdx === -1) {
await window.close();
throw new Error(`Missing ingredient type ${typeId} in inventory`);
}
const actualSlot = window.inventoryStart + invIdx;
// Left-click to pick up the stack
await this.bot.bot.clickWindow(actualSlot, 0, 0);
await sleep(100);
// Right-click on each grid slot to place one item
for (const gridSlot of gridSlots) {
await this.bot.bot.clickWindow(gridSlot, 1, 0);
await sleep(100);
}
// Left-click to put remaining stack back
await this.bot.bot.clickWindow(actualSlot, 0, 0);
await sleep(100);
}
await sleep(500);
// Take crafted shulker box from result slot
if (window.slots[0]) {
let outputSlot = null;
for (let j = window.inventoryStart; j < window.inventoryEnd; j++) {
if (!window.slots[j]) { outputSlot = j; break; }
}
if (outputSlot === null) outputSlot = window.inventoryStart;
await this.bot.bot.clickWindow(0, 0, 0);
await sleep(100);
await this.bot.bot.clickWindow(outputSlot, 0, 0);
await sleep(100);
}
await window.close();
await sleep(500);
// Find an empty chest slot to store the new shulker
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 });
const chestBlock = this.bot.bot.blockAt(chestPos);
const storeWindow = await this.bot.openContainer(chestBlock);
await sleep(300);
// Find shulker in inventory portion of the window
let shulkerWindowSlot = null;
for (let i = storeWindow.inventoryStart; i < storeWindow.inventoryEnd; i++) {
const item = storeWindow.slots[i];
if (item && item.name.includes('shulker_box')) {
shulkerWindowSlot = i;
break;
}
}
if (shulkerWindowSlot !== null) {
await this.bot.bot.moveSlotItem(shulkerWindowSlot, emptySlot.slot);
await sleep(300);
}
await this.bot.bot.closeWindow(storeWindow);
await sleep(200);
// Register in DB as empty shulker
await Database.upsertShulker(emptySlot.chest_id, emptySlot.slot, 'shulker_box', null, null);
const craftedShulkers = await Database.getShulkersByChest(emptySlot.chest_id);
const newShulker = craftedShulkers.find(s => s.slot === emptySlot.slot);
if (newShulker) {
await Database.updateShulkerCounts(newShulker.id, 0, 0);
}
}
this._craftAvailable = true; // crafting succeeded, reset flag
console.log('Storage: Successfully crafted shulker box');
}
// ========================================
// Inventory Cleanup
// ========================================
/**
* Deposit any non-shulker, non-hotbar items from bot inventory into storage.
* Called periodically and as pre/post-flight during organize.
*/
async cleanInventory() {
const hotbarNames = new Set((this.config.hotbarItems || []).map(h => h.name));
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 (!grouped[item.name]) grouped[item.name] = 0;
grouped[item.name] += item.count;
}
const itemTypes = Object.entries(grouped);
if (itemTypes.length === 0) return 0;
let cleaned = 0;
for (const [itemName, count] of itemTypes) {
try {
console.log(`Storage: Cleaning inventory — depositing ${count}x ${itemName}`);
await this.depositItemType(itemName, count);
cleaned++;
} catch (error) {
console.log(`Storage: Inventory cleanup failed for ${itemName}: ${error.message}`);
}
}
if (cleaned > 0) {
await Database.rebuildItemIndex();
console.log(`Storage: Inventory cleanup deposited ${cleaned} item type(s)`);
}
return cleaned;
}
// ========================================
// Organize
// ========================================
async organizeLooseItems() {
console.log('Storage: Organizing loose items into shulkers...');
this._craftAvailable = true; // fresh organize, allow craft retry
this._busy = true;
let organized = 0;
try {
// Pre-flight: deposit any stray items in bot inventory
await this.cleanInventory();
const chests = await Database.getChestsWithLooseItems();
console.log(`Storage: ${chests.length} chest(s) have loose items in DB`);
for (const chest of chests) {
// Check inventory has room before processing next chest (need 3+ free: items + shulker + buffer)
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 organize loop`);
break;
}
const chestPos = new Vec3(chest.pos_x, chest.pos_y, chest.pos_z);
try {
// Process one item type at a time from this chest
const failedItems = new Set();
while (true) {
await this.bot.goTo({ where: chestPos, range: 3 });
const chestBlock = this.bot.bot.blockAt(chestPos);
const window = await this.bot.openContainer(chestBlock);
await sleep(300);
// Find the first non-shulker item in the chest (skip already-failed types)
const chestSlotCount = window.inventoryStart;
let firstLooseSlot = null;
for (let i = 0; i < chestSlotCount; i++) {
const slot = window.slots[i];
if (slot && !slot.name.includes('shulker_box') && !failedItems.has(slot.name)) {
firstLooseSlot = i;
break;
}
}
if (firstLooseSlot === null) {
// No loose items left in this chest
await Database.clearLooseItems(chest.id);
await this.bot.bot.closeWindow(window);
break;
}
const itemName = window.slots[firstLooseSlot].name;
console.log(`Storage: Picking up ${itemName} from chest at ${chestPos}`);
// Count empty inventory slots
let emptyInvSlots = 0;
for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
if (!window.slots[i]) emptyInvSlots++;
}
// Pick up all stacks of THIS item type only
const pickedUpSlots = [];
let totalCount = 0;
for (let i = 0; i < chestSlotCount; i++) {
const srcItem = window.slots[i];
if (!srcItem || srcItem.name !== itemName) continue;
// Find stackable slot first (doesn't consume an empty slot)
let targetSlot = null;
for (let j = window.inventoryStart; j < window.inventoryEnd; j++) {
const invItem = window.slots[j];
if (invItem && invItem.name === itemName && invItem.count < invItem.stackSize) {
targetSlot = j;
break;
}
}
// Then find empty slot, but keep at least 1 free for shulker
if (targetSlot === null) {
if (emptyInvSlots <= 1) {
console.log('Storage: Reserving last inventory slot for shulker operations');
break;
}
for (let j = window.inventoryStart; j < window.inventoryEnd; j++) {
if (!window.slots[j]) {
targetSlot = j;
break;
}
}
emptyInvSlots--;
}
if (targetSlot === null) {
console.log('Storage: Bot inventory full');
break;
}
try {
await this.bot.bot.moveSlotItem(i, targetSlot);
await sleep(200);
// Measure actual items removed from chest slot (handles partial stacking)
const afterItem = window.slots[i];
const moved = srcItem.count - (afterItem ? afterItem.count : 0);
pickedUpSlots.push(i);
totalCount += moved;
} catch (error) {
console.log(`Storage: Could not pick up slot ${i}: ${error.message}`);
}
}
await this.bot.bot.closeWindow(window);
// Sync DB: remove picked-up items individually
for (const slot of pickedUpSlots) {
await Database.deleteLooseItem(chest.id, slot);
}
if (totalCount === 0) {
console.log(`Storage: Could not pick up any ${itemName}, skipping chest`);
break;
}
// Deposit into shulker(s)
try {
const result = await this.depositItemType(itemName, totalCount);
organized++;
// If deposit was partial, return remaining items to the chest
if (result.deposited < totalCount) {
console.log(`Storage: Partial deposit for ${itemName} (${result.deposited}/${totalCount}), returning remainder to chest`);
await this._returnItemsToChest(chestPos, itemName);
}
} catch (error) {
console.log(`Storage: Could not deposit ${itemName}: ${error.message}`);
failedItems.add(itemName);
// Try to return items to the chest instead of leaving them in inventory
try {
await this._returnItemsToChest(chestPos, itemName);
} catch (returnError) {
console.log(`Storage: Could not return ${itemName} to chest: ${returnError.message}`);
break; // Can't recover, stop this chest
}
}
// Check if inventory is nearly full before processing next item type
const innerFreeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
if (innerFreeSlots < 3) {
console.log(`Storage: Inventory nearly full (${innerFreeSlots} free), stopping this chest`);
break;
}
await sleep(250);
// Loop back to re-open chest for next item type
}
} catch (error) {
console.log(`Storage: Error organizing chest at ${chestPos}: ${error.message}`);
}
// Check if inventory is too full to continue organizing other chests
const outerFreeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
if (outerFreeSlots < 3) {
console.log(`Storage: Inventory too full (${outerFreeSlots} free slots), stopping organize`);
break;
}
await sleep(250);
}
// Consolidate partially filled shulkers of the same item type
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)
await this.cleanInventory();
await Database.rebuildItemIndex();
console.log(`Storage: Organized ${organized} item types into shulkers`);
return organized;
} finally {
this._busy = false;
}
}
/**
* Consolidate partially filled shulkers of the same item type.
* Withdraws from least-full shulker and deposits into the most-full one.
*/
async consolidateShulkers() {
console.log('Storage: Consolidating partially filled shulkers...');
let consolidated = 0;
const candidates = await Database.findConsolidatableItems();
console.log(`Storage: ${candidates.length} item type(s) have multiple non-full shulkers`);
for (const candidate of candidates) {
const itemName = candidate.item_focus;
// Re-fetch shulkers each iteration (DB may have changed from previous consolidation)
const shulkers = await Database.getShulkersByItemFocus(itemName);
// Need at least 2 non-full shulkers to consolidate
const nonFull = shulkers.filter(s => s.slot_count < 27);
if (nonFull.length < 2) continue;
// Withdraw from the least-full shulker (first in list, sorted ASC)
const source = nonFull[0];
console.log(`Storage: Consolidating ${itemName} — withdrawing from shulker ${source.id} (${source.slot_count} slots used)`);
const chestPos = new Vec3(source.pos_x, source.pos_y, source.pos_z);
try {
const { withdrawn } = await this.shulkerHandler.withdrawFromShulker(
this.bot, chestPos, source.slot, itemName, source.total_items,
source.id, source.chest_id
);
if (withdrawn === 0) {
console.log(`Storage: Could not withdraw from shulker ${source.id}, skipping`);
continue;
}
console.log(`Storage: Withdrew ${withdrawn}x ${itemName}, depositing into fuller shulker(s)`);
// Deposit will fill the most-full matching shulker first (exclude source to prevent depositing back)
const result = await this.depositItemType(itemName, withdrawn, source.id);
consolidated++;
// If source shulker is now empty, clear its item_focus
const updatedSource = await Database.getShulkerById(source.id);
if (updatedSource && updatedSource.total_items === 0) {
await Database.updateShulkerItemFocus(source.id, null);
console.log(`Storage: Shulker ${source.id} is now empty, cleared item_focus`);
}
} catch (error) {
console.error(`Storage: Error consolidating ${itemName}:`, error.message);
}
// Check inventory space before continuing
const freeSlots = this.bot.bot.inventory.slots.filter((s, i) => !s && i >= 9).length;
if (freeSlots < 3) {
console.log('Storage: Inventory too full, stopping consolidation');
break;
}
await sleep(250);
}
if (consolidated > 0) {
await Database.rebuildItemIndex();
}
console.log(`Storage: Consolidated ${consolidated} item type(s)`);
return consolidated;
}
/**
* Return items of a given type from bot inventory back into a chest.
* Used by organize when deposit is partial to prevent inventory buildup.
*/
async _returnItemsToChest(chestPos, itemName) {
const itemsInInv = this.bot.bot.inventory.items().filter(i => i.name === itemName);
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 });
const chestBlock = this.bot.bot.blockAt(chestPos);
const window = await this.bot.openContainer(chestBlock);
await sleep(300);
for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
const item = window.slots[i];
if (!item || item.name !== itemName) continue;
// Find empty chest slot
let targetSlot = null;
for (let j = 0; j < window.inventoryStart; j++) {
if (!window.slots[j]) {
targetSlot = j;
break;
}
}
if (targetSlot === null) {
console.log('Storage: Chest full, cannot return all items');
break;
}
await this.bot.bot.moveSlotItem(i, targetSlot);
await sleep(200);
}
await this.bot.bot.closeWindow(window);
await sleep(200);
}
// ========================================
// Special Item Withdrawal
// ========================================
async handleWithdrawSpecialItem(playerName, shulkerItemId) {
console.log(`Storage[${this.bot.name}]: Special item withdraw request from ${playerName}: shulker_item #${shulkerItemId}`);
this._busy = true;
try {
// Look up the specific shulker_items row
const itemRow = await Database.getShulkerItemById(shulkerItemId);
if (!itemRow) {
return this.bot.whisper(playerName, `Special item #${shulkerItemId} not found.`);
}
const shulker = await Database.getShulkerById(itemRow.shulker_id);
if (!shulker) {
return this.bot.whisper(playerName, `Shulker for item #${shulkerItemId} not found.`);
}
const chest = await Database.getChestById(shulker.chest_id);
if (!chest) {
return this.bot.whisper(playerName, `Chest for shulker #${shulker.id} not found.`);
}
const chestPos = new Vec3(chest.pos_x, chest.pos_y, chest.pos_z);
try {
const { withdrawn } = await this.shulkerHandler.withdrawFromShulkerSlot(
this.bot, chestPos, shulker.slot, itemRow.slot, itemRow.count, shulker.id, shulker.chest_id
);
if (withdrawn > 0) {
await Database.rebuildItemIndex();
const timeoutId = setTimeout(() => {
this.restoreWithdrawal(playerName);
}, 5 * 60 * 1000);
this.pendingWithdrawals.set(playerName, {
itemName: itemRow.item_name,
count: withdrawn,
mode: 'items',
timeoutId,
});
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.`);
} else {
this.bot.whisper(playerName, `Failed to withdraw special item.`);
}
} catch (error) {
console.error(`Storage: Error withdrawing special item:`, error.message);
this.bot.whisper(playerName, `Withdraw failed: ${error.message}`);
}
} finally {
this._busy = false;
}
}
// ========================================
// Status & Search
// ========================================
async getStatus(playerName) {
const stats = await Database.getStats();
this.bot.whisper(playerName, `Storage: ${stats.totalItems} items in ${stats.totalShulkers} shulkers (${stats.totalChests} chests)`);
}
async findItem(itemName) {
return await Database.searchItems(itemName);
}
async checkPermission(name, requiredRole) {
return await Database.checkPermission(name, requiredRole);
}
// ========================================
// Command Handler
// ========================================
async handleCommand(from, command, ...args) {
console.log(`Storage: Command '${command}' from ${from} with args:`, args);
this._currentCommand = command;
try { switch (command) {
case 'scan':
this.bot.whisper(from, 'Starting storage scan...');
try {
await this.scanArea(true);
const stats = await Database.getStats();
this.bot.whisper(from, `Scan complete! ${stats.totalChests} chests, ${stats.totalShulkers} shulkers, ${stats.totalItems} items`);
} catch (error) {
console.error('Storage: Scan error:', error);
this.bot.whisper(from, `Scan failed: ${error.message}`);
}
break;
case 'status':
await this.getStatus(from);
break;
case 'withdraw': {
const [itemName, count] = args;
const parsedCount = parseInt(count) || 1;
await this.handleWithdrawRequest(from, itemName, parsedCount);
break;
}
case 'withdraw-shulkers': {
const [itemName, shulkerCount] = args;
await this.handleWithdrawShulkers(from, itemName, shulkerCount);
break;
}
case 'find': {
const [searchTerm] = args;
const items = await this.findItem(searchTerm);
if (items.length === 0) {
this.bot.whisper(from, `No items found matching '${searchTerm}'`);
} else {
const results = items.slice(0, 5).map(i => `${i.item_name}: ${i.total_count}`);
this.bot.whisper(from, `Found: ${results.join(', ')}`);
}
break;
}
case 'chests': {
const chests = await Database.getChests();
this.bot.whisper(from, `Tracking ${chests.length} chests`);
break;
}
case 'organize':
this.bot.whisper(from, 'Starting organize...');
try {
const count = await this.organizeLooseItems();
this.bot.whisper(from, `Organize complete! Sorted ${count} item stacks.`);
} catch (error) {
console.error('Storage: Organize error:', error);
this.bot.whisper(from, `Organize failed: ${error.message}`);
}
break;
case 'consolidate':
this.bot.whisper(from, 'Starting consolidation...');
try {
this._busy = true;
const count = await this.consolidateShulkers();
this.bot.whisper(from, `Consolidation complete! Merged ${count} item type(s).`);
} catch (error) {
console.error('Storage: Consolidate error:', error);
this.bot.whisper(from, `Consolidation failed: ${error.message}`);
} finally {
this._busy = false;
}
break;
case 'addplayer': {
const [playerName, role] = args;
try {
await Database.addPlayer(playerName, role || 'team');
this.bot.whisper(from, `Added ${playerName} as ${role || 'team'}`);
} catch (error) {
this.bot.whisper(from, `Failed to add player: ${error.message}`);
}
break;
}
case 'removeplayer': {
const [removePlayer] = args;
try {
await Database.removePlayer(removePlayer);
this.bot.whisper(from, `Removed ${removePlayer}`);
} catch (error) {
this.bot.whisper(from, `Failed to remove player: ${error.message}`);
}
break;
}
case 'players': {
const players = await Database.getAllPlayers();
const playerList = players.map(p => `${p.player_name}(${p.role})`).join(', ');
this.bot.whisper(from, `Players: ${playerList}`);
break;
}
default:
this.bot.whisper(from, `Unknown command: ${command}`);
}
} finally {
this._currentCommand = null;
}
}
}
module.exports = Storage;