forked from wmantly/mc-bot-town
AI works here
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
const axios = require('axios');
|
||||
|
||||
axios.defaults.timeout = 0;
|
||||
|
||||
class OllamaProvider {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
@@ -25,11 +27,13 @@ class OllamaProvider {
|
||||
top_p: this.config.topP || 0.95,
|
||||
top_k: this.config.topK || 64,
|
||||
num_predict: this.config.maxOutputTokens || 2048,
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
__jsonFormat() {
|
||||
return {
|
||||
return 'json'
|
||||
/* return {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
@@ -39,7 +43,7 @@ class OllamaProvider {
|
||||
},
|
||||
required: ['text', 'delay']
|
||||
}
|
||||
};
|
||||
};*/
|
||||
}
|
||||
|
||||
async chat(message, retryCount = 0) {
|
||||
@@ -65,6 +69,7 @@ class OllamaProvider {
|
||||
model: this.model,
|
||||
messages: messages,
|
||||
stream: false,
|
||||
think: false,
|
||||
format: this.__jsonFormat(),
|
||||
options: this.__settings()
|
||||
};
|
||||
@@ -74,7 +79,7 @@ class OllamaProvider {
|
||||
`${this.baseUrl}/api/chat`,
|
||||
requestBody,
|
||||
{
|
||||
timeout: this.config.timeout || 30000,
|
||||
// timeout: this.config.timeout || 30000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
@@ -83,6 +88,7 @@ class OllamaProvider {
|
||||
|
||||
// Log raw response for debugging
|
||||
const rawContent = response.data.message.content;
|
||||
console.log('Ollama response', rawContent)
|
||||
// console.log('Ollama raw response:', JSON.stringify(rawContent));
|
||||
// console.log('Ollama raw response length:', rawContent?.length);
|
||||
|
||||
|
||||
@@ -166,6 +166,20 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
},
|
||||
'resupply': {
|
||||
desc: 'Trigger farm resupply manually',
|
||||
allowed: ['wmantly', 'useless666', 'tux4242'],
|
||||
ignoreLock: true,
|
||||
async function(from, botName) {
|
||||
const target = botName ? this.constructor.bots[botName] : this;
|
||||
if (!target) return this.whisper(from, 'Unknown bot');
|
||||
const fs = target.plunginsLoaded['FarmSupply'];
|
||||
if (!fs) return this.whisper(from, 'FarmSupply not loaded');
|
||||
this.whisper(from, 'Starting resupply...');
|
||||
await fs.resupply();
|
||||
this.whisper(from, 'Resupply complete');
|
||||
}
|
||||
},
|
||||
'dismiss': {
|
||||
desc: `Send a bot offline`,
|
||||
allowed: ['wmantly', 'useless666', 'tux4242'],
|
||||
|
||||
@@ -1,11 +1,29 @@
|
||||
'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'];
|
||||
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];
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
'use strict';
|
||||
|
||||
const conf = require('../conf');
|
||||
const { sleep } = require('../utils');
|
||||
|
||||
const botSlots = [0, 1, 2, 3, 9, 10, 11, 12, 18, 19, 20, 21];
|
||||
|
||||
class FarmSupply {
|
||||
constructor(args) {
|
||||
this.bot = args.bot;
|
||||
this.config = conf.farmSupply || {};
|
||||
this._storageBotKey = this.config.storageBotName;
|
||||
this.isAction = true;
|
||||
this._onTimeListen = null;
|
||||
this._onReadyListen = null;
|
||||
this._resupplying = false;
|
||||
}
|
||||
|
||||
get storageBotName() {
|
||||
const storageBot = this.bot.constructor.bots[this._storageBotKey];
|
||||
if (storageBot && storageBot.bot && storageBot.bot.entity) {
|
||||
return storageBot.bot.entity.username;
|
||||
}
|
||||
return this._storageBotKey;
|
||||
}
|
||||
|
||||
async init() {
|
||||
if (!this._storageBotKey) {
|
||||
console.log('FarmSupply: No storageBotName configured, plugin disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log(`FarmSupply: Initialized for bot ${this.bot.name}, storage bot: ${this._storageBotKey}`);
|
||||
|
||||
this._onReadyListen = this.bot.on('onReady', async () => {
|
||||
await sleep(3000);
|
||||
let lastTimeOfDay = this.bot.bot.time.timeOfDay;
|
||||
|
||||
this._onTimeListen = this.bot.bot.on('time', async () => {
|
||||
const currentTime = this.bot.bot.time.timeOfDay;
|
||||
if (lastTimeOfDay < 12000 && currentTime >= 12000 && !this._resupplying) {
|
||||
lastTimeOfDay = currentTime;
|
||||
console.log('FarmSupply: Sunset detected, starting resupply');
|
||||
try {
|
||||
await this.resupply();
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Sunset resupply error:', error);
|
||||
}
|
||||
} else {
|
||||
lastTimeOfDay = currentTime;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
unload() {
|
||||
if (this._onReadyListen) this._onReadyListen();
|
||||
if (this._onTimeListen) this._onTimeListen = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Pause / Resume farm plugins
|
||||
// ========================================
|
||||
|
||||
pauseFarmPlugins() {
|
||||
const paused = [];
|
||||
for (const [name, plugin] of Object.entries(this.bot.plunginsLoaded)) {
|
||||
if (name === 'FarmSupply' || name === 'AutoEat') continue;
|
||||
if (plugin.isAction) {
|
||||
console.log(`FarmSupply: Pausing plugin ${name}`);
|
||||
try {
|
||||
plugin.unload();
|
||||
} catch (error) {
|
||||
console.error(`FarmSupply: Error pausing ${name}:`, error);
|
||||
}
|
||||
paused.push(name);
|
||||
delete this.bot.plunginsLoaded[name];
|
||||
}
|
||||
}
|
||||
return paused;
|
||||
}
|
||||
|
||||
async resumeFarmPlugins(paused) {
|
||||
for (const name of paused) {
|
||||
console.log(`FarmSupply: Resuming plugin ${name}`);
|
||||
try {
|
||||
await this.bot.pluginLoad(name);
|
||||
} catch (error) {
|
||||
console.error(`FarmSupply: Error resuming ${name}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Wait after trade teleport — let server tp us back and chunks load
|
||||
// ========================================
|
||||
|
||||
async settleAfterTrade() {
|
||||
console.log('FarmSupply: Settling after trade teleport...');
|
||||
await sleep(3000);
|
||||
// Wait for pathfinder to be idle
|
||||
while (this.bot.bot.pathfinder.isMoving()) {
|
||||
this.bot.bot.pathfinder.stop();
|
||||
await sleep(500);
|
||||
}
|
||||
this.bot.bot.clearControlStates();
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Main resupply flow
|
||||
// ========================================
|
||||
|
||||
async resupply() {
|
||||
if (this._resupplying) {
|
||||
console.log('FarmSupply: Already resupplying, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
this._resupplying = true;
|
||||
console.log('FarmSupply: Starting resupply...');
|
||||
const paused = this.pauseFarmPlugins();
|
||||
|
||||
try {
|
||||
await this.emptyFilledBoxes();
|
||||
await this.fillEmptyShulkers();
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Resupply error:', error);
|
||||
}
|
||||
|
||||
await this.resumeFarmPlugins(paused);
|
||||
this._resupplying = false;
|
||||
console.log('FarmSupply: Resupply complete, farm resumed.');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Empty "filled boxes" chest
|
||||
// ========================================
|
||||
|
||||
async emptyFilledBoxes() {
|
||||
let filledChest;
|
||||
try {
|
||||
filledChest = this.bot.findChestBySign('filled boxes');
|
||||
} catch (error) {
|
||||
console.log('FarmSupply: No "filled boxes" chest found, skipping');
|
||||
return;
|
||||
}
|
||||
if (!filledChest) {
|
||||
console.log('FarmSupply: No "filled boxes" chest found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('FarmSupply: Processing filled boxes chest');
|
||||
|
||||
let hasMore = true;
|
||||
while (hasMore) {
|
||||
// Re-find chest each loop — block ref may be stale after trade teleport
|
||||
filledChest = this.bot.findChestBySign('filled boxes');
|
||||
await this.bot.goTo({ where: filledChest.position, range: 2 });
|
||||
let window = await this.bot.openContainer(filledChest);
|
||||
await sleep(300);
|
||||
|
||||
let taken = 0;
|
||||
for (let i = 0; i < window.inventoryStart; i++) {
|
||||
if (taken >= 12) break;
|
||||
const item = window.slots[i];
|
||||
if (item && item.name.includes('shulker_box')) {
|
||||
try {
|
||||
await this.bot.bot.moveSlotItem(i, window.inventoryStart + taken);
|
||||
await sleep(200);
|
||||
taken++;
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Error taking shulker from chest:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.bot.bot.closeWindow(window);
|
||||
await sleep(300);
|
||||
|
||||
if (taken === 0) {
|
||||
console.log('FarmSupply: No more filled shulker boxes to deposit');
|
||||
hasMore = false;
|
||||
break;
|
||||
}
|
||||
|
||||
console.log(`FarmSupply: Took ${taken} shulker boxes, initiating trade deposit`);
|
||||
await this.tradeDeposit();
|
||||
await this.waitForStorageBotIdle();
|
||||
await this.settleAfterTrade();
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Fill "empty shulkers" chest
|
||||
// ========================================
|
||||
|
||||
async fillEmptyShulkers() {
|
||||
let emptyChest;
|
||||
try {
|
||||
emptyChest = this.bot.findChestBySign('empty shulkers');
|
||||
} catch (error) {
|
||||
console.log('FarmSupply: No "empty shulkers" chest found, skipping');
|
||||
return;
|
||||
}
|
||||
if (!emptyChest) {
|
||||
console.log('FarmSupply: No "empty shulkers" chest found, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('FarmSupply: Processing empty shulkers chest');
|
||||
|
||||
// Re-find and navigate to chest
|
||||
emptyChest = this.bot.findChestBySign('empty shulkers');
|
||||
await this.bot.goTo({ where: emptyChest.position, range: 2 });
|
||||
let window = await this.bot.openContainer(emptyChest);
|
||||
await sleep(300);
|
||||
|
||||
let emptySlots = 0;
|
||||
for (let i = 0; i < window.inventoryStart; i++) {
|
||||
if (!window.slots[i]) emptySlots++;
|
||||
}
|
||||
|
||||
await this.bot.bot.closeWindow(window);
|
||||
await sleep(300);
|
||||
|
||||
if (emptySlots === 0) {
|
||||
console.log('FarmSupply: Empty shulkers chest is full, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`FarmSupply: Need ${emptySlots} shulker boxes`);
|
||||
|
||||
// Check current inventory for materials
|
||||
let shellsInInv = 0;
|
||||
let chestsInInv = 0;
|
||||
let shulkerBoxesInInv = 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;
|
||||
if (item.name.includes('shulker_box')) shulkerBoxesInInv += item.count;
|
||||
}
|
||||
|
||||
const canCraft = Math.min(Math.floor(shellsInInv / 2), chestsInInv);
|
||||
const totalAvailable = shulkerBoxesInInv + canCraft;
|
||||
const needed = Math.min(emptySlots, 27);
|
||||
|
||||
if (totalAvailable < needed) {
|
||||
const toCraft = needed - shulkerBoxesInInv;
|
||||
const shellsNeeded = Math.max(0, (toCraft * 2) - shellsInInv);
|
||||
const chestsNeeded = Math.max(0, toCraft - chestsInInv);
|
||||
|
||||
if (shellsNeeded > 0) {
|
||||
console.log(`FarmSupply: Withdrawing ${shellsNeeded} shulker_shell from storage`);
|
||||
await this.tradeWithdraw('shulker_shell', shellsNeeded);
|
||||
await this.waitForStorageBotIdle();
|
||||
await this.settleAfterTrade();
|
||||
}
|
||||
|
||||
if (chestsNeeded > 0) {
|
||||
console.log(`FarmSupply: Withdrawing ${chestsNeeded} chest from storage`);
|
||||
await this.tradeWithdraw('chest', chestsNeeded);
|
||||
await this.waitForStorageBotIdle();
|
||||
await this.settleAfterTrade();
|
||||
}
|
||||
}
|
||||
|
||||
// Recount after possible withdrawal
|
||||
shellsInInv = 0;
|
||||
chestsInInv = 0;
|
||||
shulkerBoxesInInv = 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;
|
||||
if (item.name.includes('shulker_box')) shulkerBoxesInInv += item.count;
|
||||
}
|
||||
|
||||
// Craft shulker boxes
|
||||
const craftCount = Math.min(Math.floor(shellsInInv / 2), chestsInInv, needed - shulkerBoxesInInv);
|
||||
if (craftCount > 0) {
|
||||
console.log(`FarmSupply: Crafting ${craftCount} shulker boxes`);
|
||||
for (let i = 0; i < craftCount; i++) {
|
||||
try {
|
||||
await this.craftShulkerBox();
|
||||
await sleep(300);
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Crafting error:', error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-find chest and dump shulker boxes
|
||||
emptyChest = this.bot.findChestBySign('empty shulkers');
|
||||
await this.bot.goTo({ where: emptyChest.position, range: 2 });
|
||||
window = await this.bot.openContainer(emptyChest);
|
||||
await sleep(300);
|
||||
|
||||
let deposited = 0;
|
||||
for (let i = window.inventoryStart; i < window.inventoryEnd; i++) {
|
||||
const item = window.slots[i];
|
||||
if (item && item.name.includes('shulker_box')) {
|
||||
let targetSlot = null;
|
||||
for (let j = 0; j < window.inventoryStart; j++) {
|
||||
if (!window.slots[j]) {
|
||||
targetSlot = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetSlot === null) break;
|
||||
|
||||
try {
|
||||
await this.bot.bot.moveSlotItem(i, targetSlot);
|
||||
await sleep(200);
|
||||
deposited++;
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Error depositing shulker box:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this.bot.bot.closeWindow(window);
|
||||
console.log(`FarmSupply: Deposited ${deposited} shulker boxes into empty shulkers chest`);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Trade: Withdraw items from storage bot
|
||||
// ========================================
|
||||
|
||||
async tradeWithdraw(itemName, count) {
|
||||
console.log(`FarmSupply: Requesting ${count}x ${itemName} from ${this.storageBotName}`);
|
||||
|
||||
await this.bot.whisper(this.storageBotName, `.withdraw ${itemName} ${count}`);
|
||||
await sleep(3000);
|
||||
|
||||
await this.bot.say(`/trade ${this.storageBotName}`);
|
||||
let window = await this.bot.once('windowOpen');
|
||||
|
||||
// Wait for storage bot to place items and confirm
|
||||
await sleep(2000);
|
||||
|
||||
// Confirm our side
|
||||
try {
|
||||
this.bot.bot.moveSlotItem(37, 37);
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Error confirming trade:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
this.bot.once('windowClose'),
|
||||
sleep(15000),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Trade withdraw timeout or error:', error);
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
console.log(`FarmSupply: Withdraw trade complete for ${itemName}`);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Trade: Deposit items to storage bot
|
||||
// ========================================
|
||||
|
||||
async tradeDeposit() {
|
||||
console.log(`FarmSupply: Depositing shulker boxes to ${this.storageBotName}`);
|
||||
|
||||
await this.bot.say(`/trade ${this.storageBotName}`);
|
||||
let window = await this.bot.once('windowOpen');
|
||||
|
||||
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 || !item.name.includes('shulker_box')) continue;
|
||||
try {
|
||||
await this.bot.bot.moveSlotItem(i, slotNum);
|
||||
await sleep(200);
|
||||
placed++;
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Error placing item in trade:', error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`FarmSupply: Placed ${placed} shulker boxes in trade window`);
|
||||
|
||||
await sleep(500);
|
||||
try {
|
||||
this.bot.bot.moveSlotItem(37, 37);
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Error confirming deposit trade:', error);
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
this.bot.once('windowClose'),
|
||||
sleep(15000),
|
||||
]);
|
||||
} catch (error) {
|
||||
console.error('FarmSupply: Trade deposit timeout or error:', error);
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
console.log(`FarmSupply: Deposit trade complete`);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Wait for storage bot to finish processing
|
||||
// ========================================
|
||||
|
||||
async waitForStorageBotIdle() {
|
||||
const storageBot = this.bot.constructor.bots[this._storageBotKey];
|
||||
if (!storageBot) return;
|
||||
|
||||
const storage = storageBot.plunginsLoaded['Storage'];
|
||||
if (!storage) return;
|
||||
|
||||
console.log('FarmSupply: Waiting for storage bot to finish processing...');
|
||||
const maxWait = 120000;
|
||||
const start = Date.now();
|
||||
while (storage._busy && (Date.now() - start) < maxWait) {
|
||||
await sleep(2000);
|
||||
}
|
||||
|
||||
if (storage._busy) {
|
||||
console.log('FarmSupply: Storage bot still busy after timeout, continuing anyway');
|
||||
} else {
|
||||
console.log('FarmSupply: Storage bot idle');
|
||||
}
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Craft a shulker box
|
||||
// ========================================
|
||||
|
||||
async craftShulkerBox() {
|
||||
let craftingTable;
|
||||
try {
|
||||
const signBlock = this.bot.findBlockBySign('crafting table');
|
||||
if (signBlock) {
|
||||
craftingTable = this.bot.bot.findBlock({
|
||||
point: signBlock.position,
|
||||
matching: this.bot.mcData.blocksByName.crafting_table.id,
|
||||
maxDistance: 4,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// No sign, search nearby
|
||||
}
|
||||
|
||||
if (!craftingTable) {
|
||||
craftingTable = this.bot.bot.findBlock({
|
||||
matching: this.bot.mcData.blocksByName.crafting_table.id,
|
||||
maxDistance: 64,
|
||||
});
|
||||
}
|
||||
|
||||
if (!craftingTable) {
|
||||
throw new Error('FarmSupply: No crafting table found nearby');
|
||||
}
|
||||
|
||||
await this.bot.goTo({ where: craftingTable.position, range: 2 });
|
||||
|
||||
const recipe = this.bot.bot.recipesAll(
|
||||
this.bot.mcData.itemsByName.shulker_box.id,
|
||||
null,
|
||||
craftingTable
|
||||
)[0];
|
||||
|
||||
if (!recipe) {
|
||||
throw new Error('FarmSupply: No recipe found for shulker box');
|
||||
}
|
||||
|
||||
const window = await this.bot.openCraftingTable(craftingTable);
|
||||
const inventory = window.slots.slice(window.inventoryStart, window.inventoryEnd);
|
||||
|
||||
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 (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(`FarmSupply: Missing ingredient type ${typeId} in inventory`);
|
||||
}
|
||||
const actualSlot = window.inventoryStart + invIdx;
|
||||
|
||||
await this.bot.bot.clickWindow(actualSlot, 0, 0);
|
||||
await sleep(100);
|
||||
|
||||
for (const gridSlot of gridSlots) {
|
||||
await this.bot.bot.clickWindow(gridSlot, 1, 0);
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
await this.bot.bot.clickWindow(actualSlot, 0, 0);
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
|
||||
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(300);
|
||||
console.log('FarmSupply: Crafted a shulker box');
|
||||
}
|
||||
}
|
||||
|
||||
FarmSupply.getStatus = function(instance) {
|
||||
return {
|
||||
active: true,
|
||||
storageBotName: instance._storageBotKey,
|
||||
resupplying: instance._resupplying,
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = FarmSupply;
|
||||
@@ -17,6 +17,7 @@ CJbot.pluginAdd(require('./guardianFarm'));
|
||||
CJbot.pluginAdd(require('./goldFarm'));
|
||||
CJbot.pluginAdd(require('./storage'));
|
||||
CJbot.pluginAdd(require('./auto-eat'));
|
||||
CJbot.pluginAdd(require('./farm-supply'));
|
||||
|
||||
for(let name in conf.mc.bots){
|
||||
if(CJbot.bots[name]) continue;
|
||||
|
||||
@@ -130,6 +130,17 @@ class Database {
|
||||
)
|
||||
`);
|
||||
|
||||
// Maps table (filled map images)
|
||||
await this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS maps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
map_id INTEGER UNIQUE NOT NULL,
|
||||
image_data TEXT,
|
||||
pixel_data TEXT,
|
||||
captured_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Invite sites table
|
||||
await this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS invite_sites (
|
||||
@@ -340,7 +351,7 @@ class Database {
|
||||
`, [excludeId, excludeId]);
|
||||
}
|
||||
|
||||
// Find shulkers containing a specific item (for withdrawal, excludes in-transit)
|
||||
// Find shulkers containing a specific item (for withdrawal, excludes in-transit and special/named items)
|
||||
async findShulkersWithItem(itemName) {
|
||||
return await this.db.all(`
|
||||
SELECT s.*, c.pos_x, c.pos_y, c.pos_z, c.chest_type,
|
||||
@@ -349,6 +360,15 @@ class Database {
|
||||
INNER JOIN chests c ON c.id = s.chest_id
|
||||
INNER JOIN shulker_items si ON si.shulker_id = s.id
|
||||
WHERE si.item_name = ? AND s.slot_count >= 0
|
||||
AND NOT (
|
||||
si.nbt_data IS NOT NULL
|
||||
AND si.nbt_data != 'null'
|
||||
AND (
|
||||
si.nbt_data LIKE '%"displayName"%'
|
||||
OR si.nbt_data LIKE '%"lore"%'
|
||||
OR si.nbt_data LIKE '%"customModelData"%'
|
||||
)
|
||||
)
|
||||
GROUP BY s.id
|
||||
ORDER BY available_count ASC
|
||||
`, [itemName]);
|
||||
@@ -414,12 +434,21 @@ class Database {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get total count of a specific item across all shulkers
|
||||
// Get total count of a specific item across all shulkers (excludes special/named items)
|
||||
async getItemTotalCount(itemName) {
|
||||
const result = await this.db.get(`
|
||||
SELECT SUM(si.count) as total
|
||||
FROM shulker_items si
|
||||
WHERE si.item_name = ?
|
||||
AND NOT (
|
||||
si.nbt_data IS NOT NULL
|
||||
AND si.nbt_data != 'null'
|
||||
AND (
|
||||
si.nbt_data LIKE '%"displayName"%'
|
||||
OR si.nbt_data LIKE '%"lore"%'
|
||||
OR si.nbt_data LIKE '%"customModelData"%'
|
||||
)
|
||||
)
|
||||
`, [itemName]);
|
||||
return result?.total || 0;
|
||||
}
|
||||
@@ -711,6 +740,50 @@ class Database {
|
||||
);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Maps
|
||||
// ========================================
|
||||
|
||||
async upsertMap(mapId, imageData, pixelData) {
|
||||
return await this.db.run(`
|
||||
INSERT INTO maps (map_id, image_data, pixel_data)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(map_id) DO UPDATE SET
|
||||
image_data = excluded.image_data,
|
||||
pixel_data = excluded.pixel_data,
|
||||
captured_at = CURRENT_TIMESTAMP
|
||||
`, [mapId, imageData, pixelData]);
|
||||
}
|
||||
|
||||
async getMap(mapId) {
|
||||
return await this.db.get('SELECT * FROM maps WHERE map_id = ?', [mapId]);
|
||||
}
|
||||
|
||||
async getAllMaps() {
|
||||
return await this.db.all('SELECT id, map_id, image_data, captured_at FROM maps ORDER BY map_id');
|
||||
}
|
||||
|
||||
async deleteMap(mapId) {
|
||||
return await this.db.run('DELETE FROM maps WHERE map_id = ?', [mapId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find filled_map items in shulkers that don't have captured images yet.
|
||||
* Returns items with location info needed to withdraw them.
|
||||
*/
|
||||
async getUncapturedMaps() {
|
||||
return await this.db.all(`
|
||||
SELECT si.*, s.slot as shulker_slot, s.chest_id, s.id as shulker_id,
|
||||
c.pos_x, c.pos_y, c.pos_z
|
||||
FROM shulker_items si
|
||||
INNER JOIN shulkers s ON s.id = si.shulker_id
|
||||
INNER JOIN chests c ON c.id = s.chest_id
|
||||
WHERE si.item_name = 'filled_map'
|
||||
AND s.slot_count >= 0
|
||||
ORDER BY si.id
|
||||
`);
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Stats
|
||||
// ========================================
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
'use strict';
|
||||
|
||||
const { PNG } = require('pngjs');
|
||||
|
||||
// Minecraft map color palette (base colors × 4 shades each)
|
||||
// Source: https://minecraft.wiki/w/Map_item_format#Color_table
|
||||
// Index 0-3 = NONE (transparent), 4-7 = GRASS, 8-11 = SAND, etc.
|
||||
// Each base color has 4 multipliers: 0.71, 0.86, 1.0, 0.53
|
||||
const BASE_COLORS = [
|
||||
null, // 0: NONE
|
||||
[127, 178, 56], // 1: GRASS
|
||||
[247, 233, 163], // 2: SAND
|
||||
[199, 199, 199], // 3: WOOL
|
||||
[255, 0, 0], // 4: FIRE
|
||||
[160, 160, 255], // 5: ICE
|
||||
[167, 167, 167], // 6: METAL
|
||||
[0, 124, 0], // 7: PLANT
|
||||
[255, 255, 255], // 8: SNOW
|
||||
[164, 168, 184], // 9: CLAY
|
||||
[151, 109, 77], // 10: DIRT
|
||||
[112, 112, 112], // 11: STONE
|
||||
[64, 64, 255], // 12: WATER
|
||||
[143, 119, 72], // 13: WOOD
|
||||
[255, 252, 245], // 14: QUARTZ
|
||||
[216, 127, 51], // 15: COLOR_ORANGE
|
||||
[178, 76, 216], // 16: COLOR_MAGENTA
|
||||
[102, 153, 216], // 17: COLOR_LIGHT_BLUE
|
||||
[229, 229, 51], // 18: COLOR_YELLOW
|
||||
[127, 204, 25], // 19: COLOR_LIGHT_GREEN
|
||||
[242, 127, 165], // 20: COLOR_PINK
|
||||
[76, 76, 76], // 21: COLOR_GRAY
|
||||
[153, 153, 153], // 22: COLOR_LIGHT_GRAY
|
||||
[76, 127, 153], // 23: COLOR_CYAN
|
||||
[127, 63, 178], // 24: COLOR_PURPLE
|
||||
[51, 76, 178], // 25: COLOR_BLUE
|
||||
[102, 76, 51], // 26: COLOR_BROWN
|
||||
[102, 127, 51], // 27: COLOR_GREEN
|
||||
[153, 51, 51], // 28: COLOR_RED
|
||||
[25, 25, 25], // 29: COLOR_BLACK
|
||||
[250, 238, 77], // 30: GOLD
|
||||
[92, 219, 213], // 31: DIAMOND
|
||||
[74, 128, 255], // 32: LAPIS
|
||||
[0, 217, 58], // 33: EMERALD
|
||||
[129, 86, 49], // 34: PODZOL
|
||||
[112, 2, 0], // 35: NETHER
|
||||
[209, 177, 161], // 36: TERRACOTTA_WHITE
|
||||
[159, 82, 36], // 37: TERRACOTTA_ORANGE
|
||||
[149, 87, 108], // 38: TERRACOTTA_MAGENTA
|
||||
[112, 108, 138], // 39: TERRACOTTA_LIGHT_BLUE
|
||||
[186, 133, 36], // 40: TERRACOTTA_YELLOW
|
||||
[103, 117, 53], // 41: TERRACOTTA_LIGHT_GREEN
|
||||
[160, 77, 78], // 42: TERRACOTTA_PINK
|
||||
[57, 41, 35], // 43: TERRACOTTA_GRAY
|
||||
[135, 107, 98], // 44: TERRACOTTA_LIGHT_GRAY
|
||||
[87, 92, 92], // 45: TERRACOTTA_CYAN
|
||||
[122, 73, 88], // 46: TERRACOTTA_PURPLE
|
||||
[76, 62, 92], // 47: TERRACOTTA_BLUE
|
||||
[76, 50, 35], // 48: TERRACOTTA_BROWN
|
||||
[76, 82, 42], // 49: TERRACOTTA_GREEN
|
||||
[142, 60, 46], // 50: TERRACOTTA_RED
|
||||
[37, 22, 16], // 51: TERRACOTTA_BLACK
|
||||
[189, 48, 49], // 52: CRIMSON_NYLIUM
|
||||
[148, 63, 97], // 53: CRIMSON_STEM
|
||||
[92, 25, 29], // 54: CRIMSON_HYPHAE
|
||||
[22, 126, 134], // 55: WARPED_NYLIUM
|
||||
[58, 142, 140], // 56: WARPED_STEM
|
||||
[86, 44, 62], // 57: WARPED_HYPHAE
|
||||
[20, 180, 133], // 58: WARPED_WART_BLOCK
|
||||
[100, 100, 100], // 59: DEEPSLATE
|
||||
[216, 175, 147], // 60: RAW_IRON
|
||||
[127, 167, 150], // 61: GLOW_LICHEN
|
||||
];
|
||||
|
||||
const SHADE_MULTIPLIERS = [180, 220, 255, 135]; // out of 255
|
||||
|
||||
// Build the full 256-entry lookup: MAP_COLORS[colorIndex] → [r, g, b]
|
||||
const MAP_COLORS = new Array(256).fill(null);
|
||||
for (let base = 0; base < BASE_COLORS.length; base++) {
|
||||
for (let shade = 0; shade < 4; shade++) {
|
||||
const idx = base * 4 + shade;
|
||||
if (!BASE_COLORS[base]) {
|
||||
MAP_COLORS[idx] = [0, 0, 0, 0]; // transparent
|
||||
} else {
|
||||
const [r, g, b] = BASE_COLORS[base];
|
||||
const m = SHADE_MULTIPLIERS[shade];
|
||||
MAP_COLORS[idx] = [
|
||||
Math.floor(r * m / 255),
|
||||
Math.floor(g * m / 255),
|
||||
Math.floor(b * m / 255),
|
||||
255
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a partial map update from a protocol packet to a pixel buffer.
|
||||
* @param {Uint8Array} pixels - 128×128 color index buffer (mutated in place)
|
||||
* @param {Object} packet - Map protocol packet with columns, rows, x, y, data
|
||||
*/
|
||||
function applyMapUpdate(pixels, packet) {
|
||||
const { columns, rows, x, y, data } = packet;
|
||||
if (!columns || columns === 0 || !data) return;
|
||||
|
||||
for (let col = 0; col < columns; col++) {
|
||||
for (let row = 0; row < rows; row++) {
|
||||
const srcIdx = col * rows + row;
|
||||
const dstX = x + col;
|
||||
const dstY = y + row;
|
||||
if (dstX < 128 && dstY < 128) {
|
||||
pixels[dstY * 128 + dstX] = data[srcIdx];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a 128×128 color-index buffer to a base64 PNG string.
|
||||
* @param {Uint8Array} pixels - 128×128 array of Minecraft color indices
|
||||
* @returns {string} base64-encoded PNG
|
||||
*/
|
||||
function renderMapToPNG(pixels) {
|
||||
const png = new PNG({ width: 128, height: 128 });
|
||||
|
||||
for (let i = 0; i < 128 * 128; i++) {
|
||||
const colorIdx = pixels[i];
|
||||
const color = MAP_COLORS[colorIdx] || [0, 0, 0, 0];
|
||||
const offset = i * 4;
|
||||
png.data[offset] = color[0]; // R
|
||||
png.data[offset + 1] = color[1]; // G
|
||||
png.data[offset + 2] = color[2]; // B
|
||||
png.data[offset + 3] = color[3]; // A
|
||||
}
|
||||
|
||||
const buffer = PNG.sync.write(png);
|
||||
return buffer.toString('base64');
|
||||
}
|
||||
|
||||
module.exports = { MAP_COLORS, applyMapUpdate, renderMapToPNG };
|
||||
@@ -345,20 +345,29 @@ class Scanner {
|
||||
|
||||
try {
|
||||
// Navigate the NBT structure to find Items array
|
||||
// Structure is: nbt.value.BlockEntityTag.value.Items.value.value (array)
|
||||
// Structure varies between:
|
||||
// - Placed+opened shulker: nbt.value.BlockEntityTag.value.Items.value.value
|
||||
// - Trade window / freshly-crafted: nbt.value.tag.value.BlockEntityTag.value.Items.value.value
|
||||
// - Simple forms: nbt.Items, nbt.BlockEntityTag.Items, etc.
|
||||
let nbtItems = null;
|
||||
const nbt = shulkerItem.nbt;
|
||||
|
||||
// Try multiple paths to find the items array
|
||||
const paths = [
|
||||
() => nbt.value?.BlockEntityTag?.value?.Items?.value?.value, // Full nested path
|
||||
() => nbt.value?.BlockEntityTag?.value?.Items?.value, // One less nesting
|
||||
() => nbt.BlockEntityTag?.Items?.value?.value, // Without top value
|
||||
() => nbt.BlockEntityTag?.Items?.value, // Simpler
|
||||
() => nbt.BlockEntityTag?.Items, // Direct
|
||||
() => nbt.value?.BlockEntityTag?.value?.Items?.value?.value, // Full nested path (standard placed shulker)
|
||||
() => nbt.value?.BlockEntityTag?.value?.Items?.value, // One less nesting level
|
||||
() => nbt.BlockEntityTag?.Items?.value?.value, // Without top-level value wrapper
|
||||
() => nbt.BlockEntityTag?.Items?.value, // Simpler BlockEntityTag path
|
||||
() => nbt.BlockEntityTag?.Items, // Direct BlockEntityTag
|
||||
() => nbt.value?.tag?.value?.BlockEntityTag?.value?.Items?.value?.value, // Trade window shulker (has extra tag wrapper)
|
||||
() => nbt.value?.tag?.value?.Items?.value?.value, // Trade window with Items directly under tag
|
||||
() => nbt.value?.tag?.value?.Items?.value, // Trade window simpler
|
||||
() => nbt.value?.Items?.value?.value, // No BlockEntityTag
|
||||
() => nbt.Items?.value?.value, // Even simpler
|
||||
() => nbt.Items, // Direct Items
|
||||
() => nbt.tag?.value?.BlockEntityTag?.value?.Items?.value?.value, // Tag wrapper path
|
||||
() => nbt.tag?.BlockEntityTag?.Items?.value?.value, // Tag without value
|
||||
() => nbt.tag?.Items?.value?.value, // Tag with Items direct
|
||||
];
|
||||
|
||||
for (const pathFn of paths) {
|
||||
@@ -483,6 +492,11 @@ class Scanner {
|
||||
result.repairCost = nbt.RepairCost;
|
||||
}
|
||||
|
||||
// Map ID for filled_map items
|
||||
if (nbt.map !== undefined) {
|
||||
result.map = nbt.map;
|
||||
}
|
||||
|
||||
return Object.keys(result).length > 0 ? result : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ class ShulkerHandler {
|
||||
// Wait a few ticks for the item entity to spawn
|
||||
await bot.bot.waitForTicks(3);
|
||||
|
||||
// Jump onto the drop position to collect it (like a human player)
|
||||
// Attempt 1: Jump onto the drop position to collect it (like a human player)
|
||||
console.log(`ShulkerHandler: Jumping onto ${placedPos} to collect drop`);
|
||||
try {
|
||||
await bot.bot.lookAt(placedPos.offset(0.5, 0, 0.5), true);
|
||||
@@ -167,14 +167,43 @@ class ShulkerHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If not collected yet, pathfind directly to the drop
|
||||
// Attempt 2: Pathfind directly to the drop position
|
||||
try {
|
||||
await bot.goTo({ where: placedPos, range: 0 });
|
||||
} catch (e) {
|
||||
try { await bot.goTo({ where: placedPos, range: 1 }); } catch (e2) { /* ignore */ }
|
||||
}
|
||||
|
||||
// Poll for pickup (up to 5 seconds)
|
||||
await bot.bot.waitForTicks(5);
|
||||
if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Attempt 3: Search for the dropped item entity nearby and walk to it
|
||||
// Item may have bounced away from the block position
|
||||
let droppedEntity = null;
|
||||
for (const entity of Object.values(bot.bot.entities)) {
|
||||
if (!entity.name || entity.name !== 'item') continue;
|
||||
const dist = entity.position.distanceTo(placedPos);
|
||||
if (dist < 5 && (!droppedEntity || dist < droppedEntity.position.distanceTo(placedPos))) {
|
||||
droppedEntity = entity;
|
||||
}
|
||||
}
|
||||
|
||||
if (droppedEntity) {
|
||||
console.log(`ShulkerHandler: Found item entity at ${droppedEntity.position}, walking to it`);
|
||||
try {
|
||||
await bot.goTo({ where: droppedEntity.position, range: 0 });
|
||||
} catch (e) {
|
||||
try { await bot.goTo({ where: droppedEntity.position, range: 1 }); } catch (e2) { /* ignore */ }
|
||||
}
|
||||
await bot.bot.waitForTicks(5);
|
||||
if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Final poll — wait up to 5 seconds in case of lag
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await sleep(500);
|
||||
if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
|
||||
@@ -182,101 +211,109 @@ class ShulkerHandler {
|
||||
}
|
||||
}
|
||||
|
||||
console.error('ShulkerHandler: Failed to collect shulker after all attempts');
|
||||
return !!bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort recovery: return a shulker from bot inventory back to its chest slot.
|
||||
* Return a shulker from bot inventory back to its chest slot.
|
||||
* If placedPos is given, dig the placed shulker first.
|
||||
* Never throws — returns true on success, false on failure.
|
||||
* @throws Error if the shulker cannot be returned (caller should handle the failure).
|
||||
*/
|
||||
async returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId) {
|
||||
try {
|
||||
// If shulker is placed on the ground, break it first
|
||||
if (placedPos) {
|
||||
try {
|
||||
await bot.bot.closeWindow(bot.bot.currentWindow);
|
||||
} catch (e) { /* ignore */ }
|
||||
await sleep(300);
|
||||
|
||||
await this.digAndCollectShulker(bot, placedPos);
|
||||
}
|
||||
|
||||
// Find shulker in bot inventory
|
||||
const shulkerInInv = bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (!shulkerInInv) {
|
||||
console.error('ShulkerHandler: Recovery failed — no shulker in inventory');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go to chest and open it
|
||||
await bot.goTo({ where: chestPos, range: 3 });
|
||||
const chestBlock = bot.bot.blockAt(chestPos);
|
||||
if (!chestBlock || !chestBlock.name.includes('chest')) {
|
||||
console.error(`ShulkerHandler: Recovery failed — no chest at ${chestPos}`);
|
||||
return false;
|
||||
}
|
||||
const window = await bot.openContainer(chestBlock);
|
||||
// If shulker is placed on the ground, break it first
|
||||
if (placedPos) {
|
||||
try {
|
||||
await bot.bot.closeWindow(bot.bot.currentWindow);
|
||||
} catch (e) { /* ignore */ }
|
||||
await sleep(300);
|
||||
|
||||
// Find shulker in inventory portion of the window
|
||||
let 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;
|
||||
await this.digAndCollectShulker(bot, placedPos);
|
||||
}
|
||||
|
||||
// Find shulker in bot inventory — throw if not found so caller knows recovery failed
|
||||
const shulkerInInv = bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
|
||||
if (!shulkerInInv) {
|
||||
throw new Error('Recovery failed — no shulker in inventory');
|
||||
}
|
||||
|
||||
// Go to chest and open it
|
||||
await bot.goTo({ where: chestPos, range: 3 });
|
||||
const chestBlock = bot.bot.blockAt(chestPos);
|
||||
if (!chestBlock || !chestBlock.name.includes('chest')) {
|
||||
throw new Error(`Recovery failed — no chest at ${chestPos}`);
|
||||
}
|
||||
const window = await bot.openContainer(chestBlock);
|
||||
await sleep(300);
|
||||
|
||||
// Find shulker in inventory portion of the window
|
||||
let 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 bot.bot.closeWindow(window);
|
||||
throw new Error('Recovery failed — shulker not found in window inventory');
|
||||
}
|
||||
|
||||
// Try original slot first; if occupied, find any empty chest slot
|
||||
let targetSlot = chestSlot;
|
||||
if (window.slots[chestSlot]) {
|
||||
targetSlot = null;
|
||||
for (let i = 0; i < window.inventoryStart; i++) {
|
||||
if (!window.slots[i]) {
|
||||
targetSlot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shulkerWindowSlot === null) {
|
||||
await bot.bot.closeWindow(window);
|
||||
console.error('ShulkerHandler: Recovery failed — shulker not found in window inventory');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try original slot first; if occupied, find any empty chest slot
|
||||
let targetSlot = chestSlot;
|
||||
if (window.slots[chestSlot]) {
|
||||
targetSlot = null;
|
||||
for (let i = 0; i < window.inventoryStart; i++) {
|
||||
if (!window.slots[i]) {
|
||||
targetSlot = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (targetSlot === null) {
|
||||
await bot.bot.closeWindow(window);
|
||||
console.error('ShulkerHandler: Recovery failed — no empty chest slot');
|
||||
return false;
|
||||
}
|
||||
|
||||
await bot.bot.moveSlotItem(shulkerWindowSlot, targetSlot);
|
||||
await sleep(300);
|
||||
|
||||
// Read the NBT and sync DB immediately
|
||||
const updatedSlotItem = window.slots[targetSlot];
|
||||
await bot.bot.closeWindow(window);
|
||||
await sleep(200);
|
||||
|
||||
if (updatedSlotItem && chestId && this.scanner) {
|
||||
try {
|
||||
await this.scanner.scanShulkerFromNBT(bot, Database, chestId, targetSlot, updatedSlotItem);
|
||||
await Database.rebuildItemIndex();
|
||||
console.log(`ShulkerHandler: Recovery DB synced — shulker at chest slot ${targetSlot}`);
|
||||
} catch (e) {
|
||||
console.error(`ShulkerHandler: Recovery DB sync failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`ShulkerHandler: Recovery succeeded — shulker returned to chest slot ${targetSlot}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('ShulkerHandler: Recovery error:', error.message);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (targetSlot === null) {
|
||||
await bot.bot.closeWindow(window);
|
||||
throw new Error('Recovery failed — no empty chest slot');
|
||||
}
|
||||
|
||||
await bot.bot.moveSlotItem(shulkerWindowSlot, targetSlot);
|
||||
await sleep(300);
|
||||
|
||||
// Read the NBT and sync DB immediately
|
||||
const updatedSlotItem = window.slots[targetSlot];
|
||||
await bot.bot.closeWindow(window);
|
||||
await sleep(200);
|
||||
|
||||
if (updatedSlotItem && chestId && this.scanner) {
|
||||
try {
|
||||
await this.scanner.scanShulkerFromNBT(bot, Database, chestId, targetSlot, updatedSlotItem);
|
||||
await Database.rebuildItemIndex();
|
||||
console.log(`ShulkerHandler: Recovery DB synced — shulker at chest slot ${targetSlot}`);
|
||||
} catch (e) {
|
||||
console.error(`ShulkerHandler: Recovery DB sync failed: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the shulker is actually back in the chest
|
||||
await bot.goTo({ where: chestPos, range: 3 });
|
||||
const verifyBlock = bot.bot.blockAt(chestPos);
|
||||
if (!verifyBlock || !verifyBlock.name.includes('chest')) {
|
||||
throw new Error('Recovery verification failed — chest not found');
|
||||
}
|
||||
const verifyWindow = await bot.openContainer(verifyBlock);
|
||||
await sleep(200);
|
||||
const verifyItem = verifyWindow.slots[targetSlot];
|
||||
await bot.bot.closeWindow(verifyWindow);
|
||||
await sleep(200);
|
||||
|
||||
if (!verifyItem || !verifyItem.name.includes('shulker_box')) {
|
||||
throw new Error(`Recovery verification failed — shulker not found at chest slot ${targetSlot} after return`);
|
||||
}
|
||||
|
||||
console.log(`ShulkerHandler: Recovery succeeded — shulker returned to chest slot ${targetSlot}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -573,7 +610,18 @@ class ShulkerHandler {
|
||||
placedPos = result.placedPos;
|
||||
} catch (placeError) {
|
||||
console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
|
||||
let recovered = false;
|
||||
try {
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
|
||||
recovered = true;
|
||||
} catch (recoverError) {
|
||||
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
|
||||
}
|
||||
if (!recovered) {
|
||||
// Recovery failed — shulker state is uncertain, propagate error upward
|
||||
// so the caller knows not to retry this same shulker
|
||||
throw recoverError;
|
||||
}
|
||||
return { deposited: 0, updatedSlotItem: null };
|
||||
}
|
||||
|
||||
@@ -646,13 +694,27 @@ class ShulkerHandler {
|
||||
return { deposited, updatedSlotItem };
|
||||
} catch (returnError) {
|
||||
console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message);
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
return { deposited: 0, updatedSlotItem: null };
|
||||
let recovered = false;
|
||||
try {
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
recovered = true;
|
||||
} catch (recoverError) {
|
||||
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
|
||||
}
|
||||
if (!recovered) throw returnError;
|
||||
return { deposited, updatedSlotItem: null };
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('ShulkerHandler: Deposit failed, attempting recovery:', error.message);
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
let recovered = false;
|
||||
try {
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
recovered = true;
|
||||
} catch (recoverError) {
|
||||
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
|
||||
}
|
||||
if (!recovered) throw error;
|
||||
return { deposited: 0, updatedSlotItem: null };
|
||||
} finally {
|
||||
this.operationInProgress = false;
|
||||
@@ -822,7 +884,14 @@ class ShulkerHandler {
|
||||
placedPos = result.placedPos;
|
||||
} catch (placeError) {
|
||||
console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
|
||||
let recovered = false;
|
||||
try {
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
|
||||
recovered = true;
|
||||
} catch (recoverError) {
|
||||
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
|
||||
}
|
||||
if (!recovered) throw placeError;
|
||||
return { withdrawn: 0, updatedSlotItem: null };
|
||||
}
|
||||
|
||||
@@ -841,6 +910,9 @@ class ShulkerHandler {
|
||||
const shulkerItem = shulkerWindow.slots[s];
|
||||
if (!shulkerItem || shulkerItem.name !== itemName) continue;
|
||||
|
||||
// Skip special/named items — only withdraw via special item handler
|
||||
if (shulkerItem.nbt?.value?.display) continue;
|
||||
|
||||
// Check if inventory has space
|
||||
const hasInvSpace = (() => {
|
||||
for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
|
||||
@@ -906,13 +978,27 @@ class ShulkerHandler {
|
||||
return { withdrawn, updatedSlotItem };
|
||||
} catch (returnError) {
|
||||
console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message);
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
let recovered = false;
|
||||
try {
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
recovered = true;
|
||||
} catch (recoverError) {
|
||||
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
|
||||
}
|
||||
if (!recovered) throw returnError;
|
||||
return { withdrawn: 0, updatedSlotItem: null };
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('ShulkerHandler: Withdraw failed, attempting recovery:', error.message);
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
let recovered = false;
|
||||
try {
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
|
||||
recovered = true;
|
||||
} catch (recoverError) {
|
||||
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
|
||||
}
|
||||
if (!recovered) throw error;
|
||||
return { withdrawn: 0, updatedSlotItem: null };
|
||||
} finally {
|
||||
this.operationInProgress = false;
|
||||
@@ -945,7 +1031,14 @@ class ShulkerHandler {
|
||||
placedPos = result.placedPos;
|
||||
} catch (placeError) {
|
||||
console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
|
||||
let recovered = false;
|
||||
try {
|
||||
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId);
|
||||
recovered = true;
|
||||
} catch (recoverError) {
|
||||
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
|
||||
}
|
||||
if (!recovered) throw placeError;
|
||||
return { withdrawn: 0, updatedSlotItem: null };
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,17 @@ function createRouter(getActiveInstance) {
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/maps', async (req, res) => {
|
||||
if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' });
|
||||
try {
|
||||
const maps = await database.getAllMaps();
|
||||
res.json({ maps });
|
||||
} catch (error) {
|
||||
console.error('API Error /api/maps:', error);
|
||||
res.status(500).json({ error: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/api/special-items', async (req, res) => {
|
||||
if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' });
|
||||
try {
|
||||
@@ -323,6 +334,7 @@ const webUI = {
|
||||
<div class="tab active" onclick="switchStorageSubTab('inventory')">Inventory</div>
|
||||
<div class="tab" onclick="switchStorageSubTab('map')">Storage Map</div>
|
||||
<div class="tab" onclick="switchStorageSubTab('special')">Special Items</div>
|
||||
<div class="tab" onclick="switchStorageSubTab('maps')">Maps</div>
|
||||
<div class="tab" onclick="switchStorageSubTab('withdraw')">Withdraw</div>
|
||||
</div>
|
||||
<div id="stab-inventory" class="stab-content active" style="margin-top:16px">
|
||||
@@ -353,6 +365,12 @@ const webUI = {
|
||||
<div id="specialItems"><div style="padding:20px;color:#6b7280;text-align:center">Click tab to load...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stab-maps" class="stab-content" style="margin-top:16px">
|
||||
<div class="panel">
|
||||
<h3>Filled Maps</h3>
|
||||
<div id="mapsGrid"><div style="padding:20px;color:#6b7280;text-align:center">Click tab to load...</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stab-withdraw" class="stab-content" style="margin-top:16px">
|
||||
<div class="panel">
|
||||
<h3>Request Withdrawal</h3>
|
||||
@@ -455,6 +473,11 @@ const webUI = {
|
||||
.special-card .sp-withdraw button{background:#2563eb;color:#fff;border:none;padding:6px 12px;border-radius:4px;cursor:pointer;font-size:.8em}
|
||||
.special-card .sp-withdraw button:hover{background:#1d4ed8}
|
||||
.special-card .sp-status{font-size:.8em;margin-top:4px;min-height:16px}
|
||||
.maps-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:12px}
|
||||
.map-card{background:#111827;border:1px solid #374151;border-radius:8px;padding:8px;text-align:center;transition:border-color .2s}
|
||||
.map-card:hover{border-color:#60a5fa}
|
||||
.map-card img{width:128px;height:128px;image-rendering:pixelated;border-radius:4px}
|
||||
.map-card .map-label{font-size:.8em;color:#9ca3af;margin-top:6px}
|
||||
.stab-content{display:none}
|
||||
.stab-content.active{display:block}
|
||||
.storage-sub-tabs{display:flex;border-bottom:1px solid #374151}
|
||||
@@ -462,7 +485,7 @@ const webUI = {
|
||||
`,
|
||||
js: `
|
||||
let allItems=[], mapData=[], sortKey='total_count', sortDir=-1;
|
||||
let specialLoaded=false, storageSubTab='inventory';
|
||||
let specialLoaded=false, mapsLoaded=false, storageSubTab='inventory';
|
||||
|
||||
function onStorageTabActive() {
|
||||
if (allItems.length === 0) { loadStats(); loadInventory(); loadPlayers(); }
|
||||
@@ -474,12 +497,13 @@ function switchStorageSubTab(name) {
|
||||
document.querySelectorAll('.stab-content').forEach(t => t.classList.remove('active'));
|
||||
const el = document.getElementById('stab-'+name);
|
||||
if (el) el.classList.add('active');
|
||||
const subNames=['inventory','map','special','withdraw'];
|
||||
const subNames=['inventory','map','special','maps','withdraw'];
|
||||
const tabs = document.querySelectorAll('.storage-sub-tabs .tab');
|
||||
const idx = subNames.indexOf(name);
|
||||
if (idx >= 0 && tabs[idx]) tabs[idx].classList.add('active');
|
||||
if (name === 'map' && mapData.length === 0) loadMap();
|
||||
if (name === 'special' && !specialLoaded) loadSpecialItems();
|
||||
if (name === 'maps' && !mapsLoaded) loadMaps();
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
@@ -875,7 +899,30 @@ async function loadPlayers(){
|
||||
try{const r=await fetch('/api/players');if(!r.ok)return;const d=await r.json();playerNames=(d.players||[]).map(p=>p.player_name)}catch(e){}
|
||||
}
|
||||
|
||||
function storageLoadAll(){loadStats();loadInventory();loadPlayers();if(storageSubTab==='map')loadMap();if(storageSubTab==='special')loadSpecialItems()}
|
||||
async function loadMaps() {
|
||||
const container = document.getElementById('mapsGrid');
|
||||
container.innerHTML = '<div style="padding:20px;color:#6b7280;text-align:center">Loading...</div>';
|
||||
try {
|
||||
const r = await fetch('/api/maps');
|
||||
const d = await r.json();
|
||||
const maps = d.maps || [];
|
||||
mapsLoaded = true;
|
||||
if (maps.length === 0) {
|
||||
container.innerHTML = '<div style="padding:20px;color:#6b7280;text-align:center">No maps captured yet</div>';
|
||||
return;
|
||||
}
|
||||
container.innerHTML = '<div class="maps-grid">' + maps.map(m =>
|
||||
'<div class="map-card">' +
|
||||
'<img src="data:image/png;base64,' + m.image_data + '" alt="Map #' + m.map_id + '">' +
|
||||
'<div class="map-label">Map #' + m.map_id + '</div>' +
|
||||
'</div>'
|
||||
).join('') + '</div>';
|
||||
} catch(e) {
|
||||
container.innerHTML = '<div style="padding:20px;color:#ef4444">Failed to load maps</div>';
|
||||
}
|
||||
}
|
||||
|
||||
function storageLoadAll(){loadStats();loadInventory();loadPlayers();if(storageSubTab==='map')loadMap();if(storageSubTab==='special')loadSpecialItems();if(storageSubTab==='maps')loadMaps()}
|
||||
`,
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user