'use strict'; const { sleep } = require('../../utils'); const { CJbot } = require('../../model/minecraft'); // Owner players who can run admin commands const owners = ['wmantly', 'useless666', 'tux4242']; // Team players who can use basic storage features const _team = [...owners, 'pi_chef', 'Ethan', 'Vince_NL']; // Dynamic team list that includes all bot usernames Object.defineProperty(Array.prototype, '_includesWithBots', { value: undefined, writable: true }); const team = new Proxy(_team, { get(target, prop) { if (prop === 'includes') { return (name) => { if (target.includes(name)) return true; for (const bot of Object.values(CJbot.bots)) { if (bot.bot && bot.bot.entity && bot.bot.entity.username === name) return true; } return false; }; } return target[prop]; } }); const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21]; const customerSlots = [5, 6, 7, 8, 14, 15, 16, 17, 23, 24, 25, 26]; module.exports = { 'scan': { desc: 'Force chest area scan', allowed: owners, ignoreLock: true, async function(from) { console.log(`Storage command 'scan' from ${from}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'scan'); } }, 'status': { desc: 'Show storage stats', allowed: team, async function(from) { console.log(`Storage command 'status' from ${from}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'status'); } }, 'withdraw': { desc: 'Withdraw items from storage (use "3s" for 3 shulkers)', allowed: team, async function(from, itemName, countStr) { console.log(`Storage command 'withdraw' from ${from}: ${itemName} x${countStr}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); // Parse count — "3s" means 3 shulkers, "10" means 10 items const str = (countStr || '1').toString().trim(); if (str.endsWith('s') || str.endsWith('S')) { const shulkerCount = parseInt(str) || 1; await storage.handleCommand(from, 'withdraw-shulkers', itemName, shulkerCount); } else { const count = parseInt(str) || 1; await storage.handleCommand(from, 'withdraw', itemName, count); } } }, 'find': { desc: 'Search for an item', allowed: team, async function(from, itemName) { console.log(`Storage command 'find' from ${from}: ${itemName}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'find', itemName); } }, 'chests': { desc: 'List tracked chests', allowed: owners, ignoreLock: true, async function(from) { console.log(`Storage command 'chests' from ${from}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'chests'); } }, 'organize': { desc: 'Force full re-sort', allowed: owners, ignoreLock: true, async function(from) { console.log(`Storage command 'organize' from ${from}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'organize'); } }, 'consolidate': { desc: 'Merge partially filled shulkers', allowed: owners, ignoreLock: true, async function(from) { console.log(`Storage command 'consolidate' from ${from}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'consolidate'); } }, 'addplayer': { desc: 'Add player to storage', allowed: owners, ignoreLock: true, async function(from, name, role = 'team') { console.log(`Storage command 'addplayer' from ${from}: ${name} as ${role}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'addplayer', name, role); } }, 'removeplayer': { desc: 'Remove player from storage', allowed: owners, ignoreLock: true, async function(from, name) { console.log(`Storage command 'removeplayer' from ${from}: ${name}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'removeplayer', name); } }, 'players': { desc: 'List authorized players', allowed: owners, ignoreLock: true, async function(from) { console.log(`Storage command 'players' from ${from}`); const storage = this.plunginsLoaded['Storage']; if (!storage) return this.whisper(from, 'Storage plugin not loaded'); await storage.handleCommand(from, 'players'); } }, '.trade': { desc: 'Handle trade deposits/withdrawals for storage', allowed: team, ignoreLock: true, async function(from) { const storage = this.plunginsLoaded['Storage']; if (!storage) return; // Interrupt any active task and acquire operation lock await this.interruptTask(from); try { await storage._acquireOperationLock(5000); } catch (e) { this.whisper(from, 'Storage is busy, try again in a moment.'); return; } let tradeResult = null; let pending = null; let itemsReceived = []; try { storage._busy = true; // The task we interrupted has exited (it released the lock); // reset the interrupt flag so our own work isn't flagged this.registerTask('Storage', 'trade', null); pending = storage.pendingWithdrawals.get(from); // Set up listener BEFORE accepting to avoid race with window opening const windowPromise = this.once('windowOpen'); await this.say('/trade accept'); // If no window ever opens (expired request, player left), bail // instead of holding the storage lock forever let window = await Promise.race([ windowPromise, sleep(30000).then(() => null), ]); if (!window) { this.whisper(from, 'Trade window never opened — send the trade request again.'); return; } // If there's a pending withdrawal, place items in bot's trade slots if (pending) { console.log(`Storage trade: Withdrawal pickup for ${from} — ${pending.count}x ${pending.itemName} (mode: ${pending.mode})`); let placed = 0; for (const slotNum of botSlots) { if (placed >= 12) break; // Find matching item in bot inventory portion of trade window 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.moveSlotItem(i, slotNum); await sleep(200); placed++; break; } catch (error) { console.log(`Storage trade: Could not move item to slot ${slotNum}: ${error.message}`); } } } console.log(`Storage trade: Placed ${placed} stack(s) in trade window`); } // Poll for other party's lock indicator (slot 53 grey_dye → lime_dye) // Both parties must click green wool (slot 37) twice: 1st locks, 2nd finalizes let timeoutCheck = setTimeout(() => { this.bot.closeWindow(window); this.whisper(from, 'Trade timed out.'); }, 120000); let confirmationCheck = setInterval(async () => { try { // Never click a closed window — invalid window IDs // trip anti-cheat ("unusual packets") if (this.bot.currentWindow !== window) return; const indicator = window.slots[53]; if (indicator && indicator.name === 'lime_dye') { clearInterval(confirmationCheck); // Click 1: lock items — single left-click, not the // pickup+putdown pair moveSlotItem sends await this.bot.clickWindow(37, 0, 0); console.log('Storage trade: click 1 — items locked'); await sleep(1000); // Both now locked, brief pause // Click 2: finalize (second confirmation) if (this.bot.currentWindow === window) { await this.bot.clickWindow(37, 0, 0); console.log('Storage trade: click 2 — final confirm'); } } } catch (e) { // window may have closed } }, 500); // Wait for trade to complete await this.once('windowClose'); clearInterval(confirmationCheck); if (timeoutCheck._destroyed) { storage._busy = false; return; } clearTimeout(timeoutCheck); if (pending) { // Withdrawal complete — clear pending if (pending.timeoutId) clearTimeout(pending.timeoutId); storage.pendingWithdrawals.delete(from); this.whisper(from, `Withdrawal complete! Enjoy your ${pending.itemName}.`); } else { // Deposit — collect items from bot inventory and sort into storage await sleep(500); const hotbarNames = new Set((storage.config.hotbarItems || []).map(h => h.name)); itemsReceived = []; for (const item of this.bot.inventory.items()) { if (hotbarNames.has(item.name)) continue; itemsReceived.push({ name: item.name, id: item.type, count: item.count, nbt: item.nbt }); } if (itemsReceived.length > 0) { this.whisper(from, `Received ${itemsReceived.length} item type(s). Sorting into storage.`); tradeResult = await storage.handleTrade(from, itemsReceived); } else { this.whisper(from, 'No items received.'); } } } finally { this.clearTask(); storage._busy = false; storage._releaseOperationLock(); } // Organize after lock is released (handleTrade runs under the lock) if (tradeResult && tradeResult.needsOrganize) { storage._busy = true; try { await storage.organizeLooseItems(true); } catch (error) { console.error('Storage: Post-trade organize failed:', error.message); } } // Notify the AI face bot about this trade so it can respond naturally try { const { getInstance } = require('../ai/manager'); const manager = getInstance(); if (manager.isActive) { if (pending) { manager.notifySystemEvent(`${this.bot.entity.username} completed withdrawal: ${pending.count}x ${pending.itemName} for ${from}. Trade finished.`); } else if (itemsReceived && itemsReceived.length > 0) { const itemSummary = itemsReceived.slice(0, 5).map(i => `${i.count}x ${i.name}`).join(', '); const extra = itemsReceived.length > 5 ? ` +${itemsReceived.length - 5} more types` : ''; manager.notifySystemEvent(`${this.bot.entity.username} received deposit from ${from}: ${itemSummary}${extra}. All items stored.`); } } } catch (e) { /* ignore */ } } }, };