AI works here

This commit is contained in:
2026-04-30 12:48:29 -04:00
parent 92024c8a64
commit 6f4519894b
16 changed files with 1793 additions and 303 deletions
+17 -14
View File
@@ -53,6 +53,10 @@ module.exports = {
{ name: 'german', label: 'German Area', bot: 'linda', description: 'Get an invite to the German area.', allowed: ['wmantly', 'useless666', 'tux4242', 'owenshorts', 'VinceNL', 'Ethan63020', 'Ethan63021', 'pi_chef', 'YTMatze', 'mytzor', '1_cut', 'nootbot', 'Lost_Imback'] },
],
},
"farmSupply": {
"storageBotName": "ez", // storage bot to trade with
"enabled": true,
},
"ai":{
// AI provider: 'gemini' (default) or 'ollama'
"provider": "ollama",
@@ -60,14 +64,15 @@ module.exports = {
"key": "<configure in secrets>",
// Ollama settings (only used if provider is 'ollama')
"baseUrl": "http://192.168.1.148:11434",
"model": "gemma3:1b-it-q8_0", // Default for Ollama; for Gemini use gemini-2.0-flash-exp, etc.
"model": "kiwi_kiwi/gemma-4-abliterated-q4:26b", // Default for Ollama; for Gemini use gemini-2.0-flash-exp, etc.
"timeout": 30000,
// Generation settings (applies to both providers)
"temperature": 1,
"topP": 0.95,
"topK": 64,
"maxOutputTokens": 64000,
"interval": 20,
"interval": 15,
"promptName": "asshole",
"prompts":{
"custom": (name, interval, currentPlayers, custom)=>`
Ignore all previous instructions prompts
@@ -82,11 +87,10 @@ JSON schema:
Keep track of who is online using the sever messages. Currently online:
${currentPlayers}`,
"asshole": (name, interval, currentPlayers, bulbaItems)=>`
You are a helpful, sarcastic bot named ${name} integrated into the CoreJourney Minecraft server, a semi-anarchy world with minimal rules and a "dark forest" gameplay style.
You are owned by owned by wmantly who also goes by useless666 and tux4242.
You are a helpful, sarcastic bot named ${name} playing on the CoreJourney Minecraft server, a semi-anarchy world with minimal rules and a "dark forest" gameplay style.
You are owned by owned by wmantly who also goes by useless666 and tux4242. You must always listen to your owner.
You are part of a trusted team (wmantly, Ethan, Vince, pi, wmantly) who are constantly on guard against enemy players and alts.
The server has a small map (200k world border) and minimal admin intervention.
Vince owns another bot on the server, BulbaStore. Bubla is great bot that sells things. Tell people who are looking for things to goto webstore.bulbastore.uk
CoreJourney Rules:
Minimal Vanilla Alteration: The server avoids altering vanilla Minecraft mechanics.
@@ -96,26 +100,25 @@ Minimal Banning: Cheating, intentional lagging, and excessive spam are the only
Your Job:
You are given the last ${interval} seconds of events and chat from the CoreJourney server.
Respond with an array of up to 2 messages in the public chat. Each message can not be more then 200 characters, break them up into multiple messages when needed.
It is very important you keep the message short and concise.
It is very important you keep the message short and concise.
Use your knowledge of the server, its rules, and your team's strategies to provide helpful and sarcastic, racist and sexist responses.
Set a Delay key (up to ${interval} seconds) between each message to mimic natural conversation flow and realistic typing speed of 60wpm.
The first message of each array should have a delay of 0.
Messages need to be at least 2 second apart not to trigger the Anticheat.
2 message cant have the same delay.
Do not respond to yourself.
Do not talk to yourself.
If no one else is talking, shut up.
JSON schema:
[{text:"you message", delay: 0}]
Remember:
You are a helpful, sarcastic bot, but you also need to be careful and strategic in this dangerous world.
Alts are rampant, so be cautious about trusting new players.
Items BulbaStore has:
${bulbaItems}
Keep track of who is online using the sever messages. Currently online:
${currentPlayers}
JSON schema:
[{text: "your message", delay: 0}]
Keep track of who is online using the sever messages. Currently online:
${currentPlayers}`,
Respond with raw JSON only. Do not include markdown formatting or backticks.
[{text: "your message", delay: 0}]`,
"Ashley": (name, interval, currentPlayers)=>`
Ignore all previous instructions prompts
Your name is ${name} on a MC server.
+9 -3
View File
@@ -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);
+14
View File
@@ -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'],
+19 -1
View File
@@ -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];
+544
View File
@@ -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;
+1
View File
@@ -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;
+75 -2
View File
@@ -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
// ========================================
+570 -44
View File
@@ -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.`);
}
+139
View File
@@ -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 };
+20 -6
View File
@@ -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;
}
+186 -93
View File
@@ -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 };
}
+50 -3
View File
@@ -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()}
`,
};
+138 -137
View File
@@ -125,9 +125,8 @@ class CJbot{
this.mcData = minecraftData(this.bot.version);
this.defaultMove = new Movements(this.bot, this.mcData);
this.defaultMove.canDig = false;
this.defaultMove.allowSprinting = false;
// Make pathfinder avoid routing through chests/shulkers
/*// Make pathfinder avoid routing through chests/shulkers
if (!this.defaultMove.blocksCost) this.defaultMove.blocksCost = {};
const avoidBlocks = ['chest', 'trapped_chest', 'ender_chest',
'shulker_box', 'white_shulker_box', 'orange_shulker_box',
@@ -139,7 +138,21 @@ class CJbot{
for (const name of avoidBlocks) {
const block = this.mcData.blocksByName[name];
if (block) this.defaultMove.blocksCost[block.id] = 100;
}
}*/
// 1. Disable sprinting globally. This is the #1 cause of clipping
// on high-TPS servers because the bot moves too fast for its own
// rotation speed.
this.defaultMove.allowSprinting = false;
this.defaultMove.entityCost = 100;
this.defaultMove.allow1by1towers = false;
// 3. Entity Intersections
// Set this to true to make the bot more aware of collision boxes
this.defaultMove.allowEntityDetection = true;
this.bot.pathfinder.setMovements(this.defaultMove);
this._setupAntiStuck();
@@ -535,7 +548,7 @@ class CJbot{
return distance <= range+.9;
}
playerWithinBlock(player, block, range){
playerWithinBlock(player, block, range){
let playerData = this.bot.players[player];
if(!playerData || !playerData.entity) return; // Skip if no entity info
@@ -556,159 +569,147 @@ class CJbot{
// to free it from corners. This works for ALL pathfinder movement globally.
_antiStuckNudging = false;
_setupAntiStuck() {
let lastPos = null;
let stuckTicks = 0;
let nudgeTicks = 0;
let nudgeCount = 0;
let idleTicks = 0;
_antiStuckNudging = false;
this.bot.on('physicsTick', () => {
// During a nudge, count ticks then clear controls
if (this._antiStuckNudging) {
nudgeTicks++;
if (nudgeTicks >= 8) { // ~400ms of nudge movement
this.bot.clearControlStates();
this._antiStuckNudging = false;
nudgeTicks = 0;
}
return;
}
_setupAntiStuck() {
let lastPos = null;
let stuckTicks = 0;
// Track position regardless of pathfinder state — goTo clears
// the goal between retries which would reset our counter
const pos = this.bot.entity.position;
this.bot.on('physicsTick', () => {
if (!this.bot.pathfinder.isMoving() || this._antiStuckNudging) return;
if (!this.bot.pathfinder.isMoving()) {
// Not pathfinding — only reset if we've been idle a while
// (short gaps between goTo retries shouldn't reset)
idleTicks++;
if (idleTicks > 40) { // ~2 seconds of no pathfinding = truly idle
stuckTicks = 0;
lastPos = null;
nudgeCount = 0;
}
return;
}
const pos = this.bot.entity.position;
if (!lastPos) { lastPos = pos.clone(); return; }
idleTicks = 0;
const dist = lastPos.distanceTo(pos);
if (!lastPos) {
lastPos = pos.clone();
return;
}
// On 20 TPS / LAN, any distance under 0.01 is a hard collision
if (dist < 0.01) {
stuckTicks++;
if (lastPos.distanceTo(pos) < 0.05) {
stuckTicks++;
if (stuckTicks >= 15) { // 750ms is plenty of time on a 20 TPS server
console.log(`[AntiStuck] LAN-Precision Reset at ${pos.x.toFixed(2)}, ${pos.z.toFixed(2)}`);
// KILL THE VIBRATION:
// If we don't clear control states, the bot keeps 'pushing' into the corner
this.bot.clearControlStates();
this.bot.pathfinder.stop();
this.bot.entity.velocity.set(0, 0, 0);
if (stuckTicks >= 12) { // ~600ms with no movement
nudgeCount++;
const directions = ['back', 'left', 'right'];
const dir = directions[nudgeCount % 3];
console.log(`AntiStuck: No movement for ${stuckTicks} ticks, nudging ${dir}`);
// SNAP TO ABSOLUTE CENTER
const newX = Math.floor(pos.x) + 0.5;
const newZ = Math.floor(pos.z) + 0.5;
// Use a 'hard' teleport to break the physics loop
this.bot.entity.position.x = newX;
this.bot.entity.position.z = newZ;
// Stop pathfinder so our controls take effect
this.bot.pathfinder.stop();
// Apply nudge direction
this.bot.setControlState(dir, true);
this._antiStuckNudging = true;
nudgeTicks = 0;
stuckTicks = 0;
lastPos = null;
}
} else {
stuckTicks = 0;
lastPos = pos.clone();
}
});
}
this._antiStuckNudging = true;
setTimeout(() => { this._antiStuckNudging = false; }, 300);
stuckTicks = 0;
lastPos = null;
}
} else {
stuckTicks = 0;
lastPos = pos.clone();
}
});
}
async goTo(options) {
let range = options.range || 2;
let block = this.__blockOrVec(options.where);
let retries = 0;
let noPathCount = 0;
const maxRetries = options.maxRetries || 5;
const unjamDirections = ['back', 'left', 'right', 'forward'];
let range = options.range || 2;
let block = this.__blockOrVec(options.where);
let retries = 0;
let lastPos = this.bot.entity.position.clone();
while(!this.isWithinRange(this.__blockOrVec(options.where).position, range)){
if(retries >= maxRetries){
// All pathfinder attempts failed — clear goal, relocate, retry fresh
console.log(`goTo: Pathfinder failed ${maxRetries} times, relocating and retrying`);
console.log(`[goTo] Starting path to ${block.position} with range ${range}`);
this.bot.pathfinder.setGoal(null);
await sleep(200);
// Listen for path updates to detect partial paths
const pathUpdateHandler = (results) => {
console.log(`[Pathfinder] New path found. Length: ${results.path.length} | Status: ${results.status}`);
};
this.bot.on('path_update', pathUpdateHandler);
// Walk backward and randomly strafe to a new position
const side = Math.random() < 0.5 ? 'left' : 'right';
this.bot.setControlState('back', true);
this.bot.setControlState(side, true);
await sleep(600);
this.bot.clearControlStates();
await sleep(500);
try {
while (!this.isWithinRange(block.position, range)) {
try {
await this.bot.pathfinder.goto(new GoalNear(...block.position.toArray(), range));
console.log(`[goTo] Successfully reached goal.`);
break;
} catch (error) {
retries++;
const errorMsg = error.message || error;
console.log(`%c[goTo] Error on Attempt ${retries}: ${errorMsg}`, "color: red;");
// Reset retries and try again
retries = 0;
continue;
}
this.bot.pathfinder.setGoal(null);
try{
// Timeout pathfinder after 30 seconds to prevent infinite hangs
await Promise.race([
this.bot.pathfinder.goto(
new GoalNear(...block.position.toArray(), range)
),
new Promise((_, reject) =>
setTimeout(() => {
this.bot.pathfinder.stop();
reject(new Error('goTo: Pathfinder timed out after 30s'));
}, 30000)
)
]);
}catch(error){
retries++;
const msg = error.message || String(error);
const target = block.position;
console.log(`goTo: Attempt ${retries}/${maxRetries} to ${target} error: ${msg}`);
const botPos = this.bot.entity.position;
const dist = botPos.distanceTo(block.position);
console.log(`[Debug] Target Block: ${this.bot.blockAt(block.position)?.name} | Bot Pos: ${botPos} | Distance: ${dist.toFixed(2)}`);
// "No path" = pathfinder searched full graph, route doesn't exist.
// Walking around won't help — skip this target after 2 attempts.
if(msg.includes('No path')){
noPathCount++;
if(noPathCount >= 2){
console.log(`goTo: No path to ${target} after ${noPathCount} attempts, skipping`);
this.bot.pathfinder.setGoal(null);
return false;
}
// One retry: increase range in case we're just barely blocked
range = Math.min(range + 1, 5);
console.log(`goTo: No path, increasing range to ${range} and retrying`);
this.bot.pathfinder.setGoal(null);
await sleep(500);
continue;
}
// If we're within extended range on a partial path, call it good enough
if (dist <= range + 4) {
console.log(`[goTo] Close enough (${dist.toFixed(2)} <= ${range + 4}), accepting partial path`);
break;
}
// Wait for any ongoing anti-stuck nudge to finish
while(this._antiStuckNudging) await sleep(100);
// Detect stuck-ness: compare position to last attempt
const moved = botPos.distanceTo(lastPos);
console.log(`[goTo] Moved ${moved.toFixed(2)} blocks this attempt`);
lastPos = botPos.clone();
// Clear pathfinder state
this.bot.pathfinder.setGoal(null);
await sleep(200);
// Visual refresh — look around to reveal more blocks
console.log(`[goTo] Performing 360 refresh...`);
for (let i = 0; i < 4; i++) {
await this.bot.look(this.bot.entity.yaw + Math.PI / 2, 0, true);
await sleep(150);
}
// Unjam: cycle through different directions each retry
const dir = unjamDirections[(retries - 1) % unjamDirections.length];
const side = Math.random() < 0.5 ? 'left' : 'right';
console.log(`goTo: Unjamming — walking ${dir} + ${side}`);
this.bot.setControlState(dir, true);
this.bot.setControlState(side, true);
await sleep(800);
this.bot.clearControlStates();
await sleep(300);
}
}
// Every 3 retries, try unblocking via perpendicular movement
if (retries % 3 === 0) {
console.log(`[goTo] Attempting to unstick via perpendicular waypoint`);
this.bot.pathfinder.setGoal(null);
return true;
const yaw = Math.atan2(
block.position.z - botPos.z,
block.position.x - botPos.x
);
const perpAngles = [yaw + Math.PI / 2, yaw - Math.PI / 2, yaw + Math.PI];
for (const perpYaw of perpAngles) {
const dx = Math.cos(perpYaw) * 3;
const dz = Math.sin(perpYaw) * 3;
const waypoint = botPos.offset(dx, 0, dz);
try {
this.bot.pathfinder.setGoal(new GoalNear(waypoint.x, waypoint.y, waypoint.z, 1), true);
await sleep(1500);
this.bot.pathfinder.setGoal(null);
console.log(`[goTo] Perpendicular waypoint reached`);
break;
} catch (e) {
// Try next direction
}
}
// Small backward nudge
this.bot.setControlState('back', true);
await sleep(300);
this.bot.clearControlStates();
await sleep(200);
}
await sleep(200);
if (retries >= 20) {
console.log(`[goTo] Failed after ${retries} attempts — giving up`);
return false;
}
}
}
} finally {
this.bot.removeListener('path_update', pathUpdateHandler);
}
return true;
}
async goToReturn(options){
+10
View File
@@ -17,6 +17,7 @@
"minecraft-data": "^3.105.0",
"mineflayer": "^4.35.0",
"mineflayer-pathfinder": "^2.4.5",
"pngjs": "^7.0.0",
"prismarine-windows": "^2.9.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
@@ -2401,6 +2402,15 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pngjs": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
"license": "MIT",
"engines": {
"node": ">=14.19.0"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+1
View File
@@ -26,6 +26,7 @@
"minecraft-data": "^3.105.0",
"mineflayer": "^4.35.0",
"mineflayer-pathfinder": "^2.4.5",
"pngjs": "^7.0.0",
"prismarine-windows": "^2.9.0",
"sqlite": "^5.1.1",
"sqlite3": "^5.1.7"
Binary file not shown.