Files
mc-bot-town-2/nodejs/controller/commands/storage.js
T
2026-07-12 17:26:59 -04:00

325 lines
11 KiB
JavaScript

'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 */ }
}
},
};