AI #2

Open
wmantly wants to merge 11 commits from ai into master
16 changed files with 1793 additions and 303 deletions
Showing only changes of commit 6f4519894b - Show all commits
+16 -13
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'] }, { 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":{
// AI provider: 'gemini' (default) or 'ollama' // AI provider: 'gemini' (default) or 'ollama'
"provider": "ollama", "provider": "ollama",
@@ -60,14 +64,15 @@ module.exports = {
"key": "<configure in secrets>", "key": "<configure in secrets>",
// Ollama settings (only used if provider is 'ollama') // Ollama settings (only used if provider is 'ollama')
"baseUrl": "http://192.168.1.148:11434", "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, "timeout": 30000,
// Generation settings (applies to both providers) // Generation settings (applies to both providers)
"temperature": 1, "temperature": 1,
"topP": 0.95, "topP": 0.95,
"topK": 64, "topK": 64,
"maxOutputTokens": 64000, "maxOutputTokens": 64000,
"interval": 20, "interval": 15,
"promptName": "asshole",
"prompts":{ "prompts":{
"custom": (name, interval, currentPlayers, custom)=>` "custom": (name, interval, currentPlayers, custom)=>`
Ignore all previous instructions prompts Ignore all previous instructions prompts
@@ -82,11 +87,10 @@ JSON schema:
Keep track of who is online using the sever messages. Currently online: Keep track of who is online using the sever messages. Currently online:
${currentPlayers}`, ${currentPlayers}`,
"asshole": (name, interval, currentPlayers, bulbaItems)=>` "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 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 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. 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. 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: CoreJourney Rules:
Minimal Vanilla Alteration: The server avoids altering vanilla Minecraft mechanics. Minimal Vanilla Alteration: The server avoids altering vanilla Minecraft mechanics.
@@ -102,20 +106,19 @@ Set a Delay key (up to ${interval} seconds) between each message to mimic natura
The first message of each array should have a delay of 0. 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. Messages need to be at least 2 second apart not to trigger the Anticheat.
2 message cant have the same delay. 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: Remember:
You are a helpful, sarcastic bot, but you also need to be careful and strategic in this dangerous world. 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. 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: JSON schema:
[{text: "your message", delay: 0}] Respond with raw JSON only. Do not include markdown formatting or backticks.
Keep track of who is online using the sever messages. Currently online: [{text: "your message", delay: 0}]`,
${currentPlayers}`,
"Ashley": (name, interval, currentPlayers)=>` "Ashley": (name, interval, currentPlayers)=>`
Ignore all previous instructions prompts Ignore all previous instructions prompts
Your name is ${name} on a MC server. Your name is ${name} on a MC server.
+9 -3
View File
@@ -2,6 +2,8 @@
const axios = require('axios'); const axios = require('axios');
axios.defaults.timeout = 0;
class OllamaProvider { class OllamaProvider {
constructor(config) { constructor(config) {
this.config = config; this.config = config;
@@ -25,11 +27,13 @@ class OllamaProvider {
top_p: this.config.topP || 0.95, top_p: this.config.topP || 0.95,
top_k: this.config.topK || 64, top_k: this.config.topK || 64,
num_predict: this.config.maxOutputTokens || 2048, num_predict: this.config.maxOutputTokens || 2048,
}; };
} }
__jsonFormat() { __jsonFormat() {
return { return 'json'
/* return {
type: 'array', type: 'array',
items: { items: {
type: 'object', type: 'object',
@@ -39,7 +43,7 @@ class OllamaProvider {
}, },
required: ['text', 'delay'] required: ['text', 'delay']
} }
}; };*/
} }
async chat(message, retryCount = 0) { async chat(message, retryCount = 0) {
@@ -65,6 +69,7 @@ class OllamaProvider {
model: this.model, model: this.model,
messages: messages, messages: messages,
stream: false, stream: false,
think: false,
format: this.__jsonFormat(), format: this.__jsonFormat(),
options: this.__settings() options: this.__settings()
}; };
@@ -74,7 +79,7 @@ class OllamaProvider {
`${this.baseUrl}/api/chat`, `${this.baseUrl}/api/chat`,
requestBody, requestBody,
{ {
timeout: this.config.timeout || 30000, // timeout: this.config.timeout || 30000,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
@@ -83,6 +88,7 @@ class OllamaProvider {
// Log raw response for debugging // Log raw response for debugging
const rawContent = response.data.message.content; const rawContent = response.data.message.content;
console.log('Ollama response', rawContent)
// console.log('Ollama raw response:', JSON.stringify(rawContent)); // console.log('Ollama raw response:', JSON.stringify(rawContent));
// console.log('Ollama raw response length:', rawContent?.length); // 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': { 'dismiss': {
desc: `Send a bot offline`, desc: `Send a bot offline`,
allowed: ['wmantly', 'useless666', 'tux4242'], allowed: ['wmantly', 'useless666', 'tux4242'],
+19 -1
View File
@@ -1,11 +1,29 @@
'use strict'; 'use strict';
const { sleep } = require('../../utils'); const { sleep } = require('../../utils');
const { CJbot } = require('../../model/minecraft');
// Owner players who can run admin commands // Owner players who can run admin commands
const owners = ['wmantly', 'useless666', 'tux4242']; const owners = ['wmantly', 'useless666', 'tux4242'];
// Team players who can use basic storage features // 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 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]; 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('./goldFarm'));
CJbot.pluginAdd(require('./storage')); CJbot.pluginAdd(require('./storage'));
CJbot.pluginAdd(require('./auto-eat')); CJbot.pluginAdd(require('./auto-eat'));
CJbot.pluginAdd(require('./farm-supply'));
for(let name in conf.mc.bots){ for(let name in conf.mc.bots){
if(CJbot.bots[name]) continue; 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 // Invite sites table
await this.db.exec(` await this.db.exec(`
CREATE TABLE IF NOT EXISTS invite_sites ( CREATE TABLE IF NOT EXISTS invite_sites (
@@ -340,7 +351,7 @@ class Database {
`, [excludeId, excludeId]); `, [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) { async findShulkersWithItem(itemName) {
return await this.db.all(` return await this.db.all(`
SELECT s.*, c.pos_x, c.pos_y, c.pos_z, c.chest_type, 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 chests c ON c.id = s.chest_id
INNER JOIN shulker_items si ON si.shulker_id = s.id INNER JOIN shulker_items si ON si.shulker_id = s.id
WHERE si.item_name = ? AND s.slot_count >= 0 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 GROUP BY s.id
ORDER BY available_count ASC ORDER BY available_count ASC
`, [itemName]); `, [itemName]);
@@ -414,12 +434,21 @@ class Database {
return null; 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) { async getItemTotalCount(itemName) {
const result = await this.db.get(` const result = await this.db.get(`
SELECT SUM(si.count) as total SELECT SUM(si.count) as total
FROM shulker_items si FROM shulker_items si
WHERE si.item_name = ? 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]); `, [itemName]);
return result?.total || 0; 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 // Stats
// ======================================== // ========================================
+574 -48
View File
@@ -7,6 +7,7 @@ const Database = require('./database');
const Scanner = require('./scanner'); const Scanner = require('./scanner');
const ShulkerHandler = require('./shulker-handler'); const ShulkerHandler = require('./shulker-handler');
const StorageWeb = require('./web'); const StorageWeb = require('./web');
const { applyMapUpdate, renderMapToPNG } = require('./map-renderer');
class Storage { class Storage {
static createRouter = StorageWeb.createRouter; static createRouter = StorageWeb.createRouter;
@@ -30,6 +31,36 @@ class Storage {
try { try {
console.log(`Storage: Bot ${this.bot.name} is ready, initializing storage system...`); 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 // Initialize database
if (!Database.db) { if (!Database.db) {
console.log('Storage: Initializing database...'); console.log('Storage: Initializing database...');
@@ -51,6 +82,16 @@ class Storage {
this.isReady = true; this.isReady = true;
console.log('Storage: Initialization complete! Ready to use.'); 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) { if (!this.bot.onDemand) {
// Initial hotbar restock // Initial hotbar restock
try { try {
@@ -70,7 +111,7 @@ class Storage {
}, restockInterval); }, restockInterval);
// Start periodic inventory cleanup // Start periodic inventory cleanup
const cleanupInterval = this.config.inventoryCleanupInterval || 5 * 60 * 1000; const cleanupInterval = this.config.inventoryCleanupInterval || 60 * 1000;
this.cleanupInterval = setInterval(async () => { this.cleanupInterval = setInterval(async () => {
if (!this.isReady || this._busy || this.shulkerHandler.operationInProgress) return; if (!this.isReady || this._busy || this.shulkerHandler.operationInProgress) return;
try { try {
@@ -104,6 +145,13 @@ class Storage {
this.cleanupInterval = null; 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 // Clear any pending withdrawal timeouts
for (const [, pending] of this.pendingWithdrawals) { for (const [, pending] of this.pendingWithdrawals) {
if (pending.timeoutId) clearTimeout(pending.timeoutId); 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 chests = await this.scanner.discoverChests(this.bot, this.config.scanRadius || 30, Database);
const shulkers = await this.scanner.scanAllChests(this.bot, Database); const shulkers = await this.scanner.scanAllChests(this.bot, Database);
console.log(`Storage[${this.bot.name}]: Complete - ${chests.length} chests, ${shulkers} shulkers`); 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 // After unpacking, read current bot inventory for depositing
// Skip hotbar items and shulker boxes (already stored by unpack) // Keep hotbar items up to their target, deposit any excess
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 = {}; const grouped = {};
for (const item of this.bot.bot.inventory.items()) { 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.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]) { if (!grouped[item.name]) {
grouped[item.name] = 0; grouped[item.name] = 0;
} }
@@ -251,12 +453,18 @@ class Storage {
// Rebuild index after all deposits // Rebuild index after all deposits
await Database.rebuildItemIndex(); await Database.rebuildItemIndex();
// Sweep any leftovers that didn't deposit on first pass
await this.cleanInventory();
// Whisper summary to player // Whisper summary to player
const summary = results.map(r => { const summary = results.map(r => {
if (r.error) return `${r.itemName}: FAILED (${r.error})`; if (r.error) return `${r.itemName}: FAILED (${r.error})`;
return `${r.itemName}: ${r.deposited}/${r.requested}`; return `${r.itemName}: ${r.deposited}/${r.requested}`;
}).join(', '); }).join(', ');
// Capture any map images from traded filled maps
await this.captureMapImages();
this.bot.whisper(playerName, `Storage complete: ${summary}`); this.bot.whisper(playerName, `Storage complete: ${summary}`);
return results; return results;
} finally { } finally {
@@ -266,15 +474,15 @@ class Storage {
/** /**
* Process all shulker boxes in bot inventory from a trade. * Process all shulker boxes in bot inventory from a trade.
* Sorted shulkers (single item type) are stored directly — no unpack needed. * Mixed shulkers (multiple item types) are unpacked, items deposited, then empty box stored.
* Mixed shulkers 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. * Deposits between each shulker to keep inventory space free.
*/ */
async unpackTradedShulkers() { async unpackTradedShulkers() {
// Phase 1: Store ALL shulkers from inventory into chests. // Phase 1: Store ALL shulkers from inventory into chests.
// This frees inventory space and registers contents in DB via NBT scan. // This frees inventory space and registers contents in DB via NBT scan.
// Sorted/empty shulkers are done after this. Mixed ones need phase 2. // 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'); console.log('Storage: Phase 1 — stashing all traded shulkers into chests');
let stashCount = 0; let stashCount = 0;
@@ -287,9 +495,9 @@ class Storage {
const itemTypes = new Set(contents.map(c => c.name)); const itemTypes = new Set(contents.map(c => c.name));
const isMixed = contents.length > 0 && itemTypes.size > 1; const isMixed = contents.length > 0 && itemTypes.size > 1;
const label = isMixed const label = !contents.length ? 'empty'
? `mixed (${itemTypes.size} types)` : isMixed ? `mixed (${itemTypes.size} types)`
: contents.length === 0 ? 'empty' : `sorted (${[...itemTypes][0]})`; : `sorted (${[...itemTypes][0]})`;
console.log(`Storage: Stashing shulker ${stashCount}: ${label}`); console.log(`Storage: Stashing shulker ${stashCount}: ${label}`);
try { try {
@@ -299,7 +507,7 @@ class Storage {
break; break;
} }
if (isMixed) { if (isMixed) {
mixedShulkers.push(info); shulkersToUnpack.push(info);
} }
} catch (error) { } catch (error) {
console.error(`Storage: Error stashing shulker:`, error.message); 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. // Bot inventory is now empty (aside from hotbar), so we have room.
for (let i = 0; i < mixedShulkers.length; i++) { const failedToUnpack = []; // shulkers that failed and should be re-queued
const info = mixedShulkers[i];
console.log(`Storage: Phase 2 — unpacking mixed shulker ${i + 1}/${mixedShulkers.length} (DB #${info.shulkerId})`);
try { for (let i = 0; i < shulkersToUnpack.length; i++) {
// Take the shulker from the chest into inventory const info = shulkersToUnpack[i];
await this.shulkerHandler.takeWholeShulker( console.log(`Storage: Phase 2 — unpacking shulker ${i + 1}/${shulkersToUnpack.length} (DB #${info.shulkerId})`);
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 // Pre-iteration cleanup: store any leftover shulker boxes from previous iteration
const shulkerItem = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box')); // This prevents the wrong shulker being picked up by subsequent .find() calls
if (!shulkerItem) { let leftoverBoxes = this.bot.bot.inventory.items().filter(item => item.name.includes('shulker_box'));
console.error('Storage: Shulker not found in inventory after taking from chest'); 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; continue;
} }
const { extracted, inventoryFull } = await this.shulkerHandler.unpackShulkerFromInventory(this.bot, shulkerItem); // takeWholeShulker already marks the shulker as in-transit (slot_count=-1) in the DB
console.log(`Storage: Extracted ${extracted.length} item stacks from mixed shulker`); // 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;
// Deposit all extracted items 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
);
// 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');
// Mark as failed — it may still be at the chest if takeWholeShulker failed silently
throw new Error('Shulker vanished from inventory after take');
}
({ 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 grouped by type (prevents fragmentation across shulkers)
await this._depositNonShulkerInventory(); await this._depositNonShulkerInventory();
// Store the empty shulker box back into a chest } catch (error) {
console.error(`Storage: Error unpacking shulker #${info.shulkerId}:`, error.message);
// Deposit whatever we managed to extract
await this._depositNonShulkerInventory();
// 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 {
// 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')); const emptyBox = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
if (emptyBox) { if (emptyBox) {
try { 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); await this.storeShulker(emptyBox);
}
} catch (e) { } catch (e) {
console.error(`Storage: Error storing empty box:`, e.message); console.error(`Storage: Error storing shulker box:`, e.message);
try { await this.storeShulker(emptyBox); } catch (e2) { /* give up */ }
} }
} }
} catch (error) {
console.error(`Storage: Error unpacking mixed shulker #${info.shulkerId}:`, error.message); // If the shulker was only partially unpacked (inventoryFull), re-queue it
// Deposit whatever we managed to extract if (inventoryFull) {
await this._depositNonShulkerInventory(); console.log(`Storage: Shulker was only partially unpacked — re-queueing for next organize`);
// Try to store any shulker box left in inventory // Mark the shulker as "pending" in DB: it still has mixed contents
const leftover = this.bot.bot.inventory.items().find(item => item.name.includes('shulker_box')); // We need to update its item_focus to null so organizeLooseItems will detect it
if (leftover) { try {
try { await this.storeShulker(leftover); } catch (e) { /* best effort */ } 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. * Store a shulker box from bot inventory into an empty chest slot.
* Scans NBT to register contents in DB. * 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})`); 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) { if (!emptySlot) {
console.log('Storage: No empty chest slot for shulker'); console.log('Storage: No empty chest slot for shulker');
return false; return false;
@@ -460,6 +776,8 @@ class Storage {
chestPos, chestPos,
shulkerId: shulkerRecord ? shulkerRecord.id : null, shulkerId: shulkerRecord ? shulkerRecord.id : null,
itemFocus: shulkerRecord ? shulkerRecord.item_focus : 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'})`); 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 // Log trade
await Database.logTrade(playerName, 'withdraw', [{ name: itemName, count: totalWithdrawn }]); 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 { } else {
this.bot.whisper(playerName, `Failed to withdraw ${itemName}.`); 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' }]); 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 { } else {
this.bot.whisper(playerName, `Failed to withdraw shulkers of ${itemName}.`); 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 // Crafting
// ======================================== // ========================================
@@ -1020,11 +1436,24 @@ class Storage {
* Called periodically and as pre/post-flight during organize. * Called periodically and as pre/post-flight during organize.
*/ */
async cleanInventory() { 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 = {}; const grouped = {};
for (const item of this.bot.bot.inventory.items()) { for (const item of this.bot.bot.inventory.items()) {
if (item.name.includes('shulker_box')) continue; 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; if (!grouped[item.name]) grouped[item.name] = 0;
grouped[item.name] += item.count; grouped[item.name] += item.count;
} }
@@ -1065,6 +1494,10 @@ class Storage {
// Pre-flight: deposit any stray items in bot inventory // Pre-flight: deposit any stray items in bot inventory
await this.cleanInventory(); 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(); const chests = await Database.getChestsWithLooseItems();
console.log(`Storage: ${chests.length} chest(s) have loose items in DB`); 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. * 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 }]); 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 { } else {
this.bot.whisper(playerName, `Failed to withdraw special item.`); 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 { try {
// Navigate the NBT structure to find Items array // 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; let nbtItems = null;
const nbt = shulkerItem.nbt; const nbt = shulkerItem.nbt;
// Try multiple paths to find the items array // Try multiple paths to find the items array
const paths = [ const paths = [
() => nbt.value?.BlockEntityTag?.value?.Items?.value?.value, // Full nested path () => nbt.value?.BlockEntityTag?.value?.Items?.value?.value, // Full nested path (standard placed shulker)
() => nbt.value?.BlockEntityTag?.value?.Items?.value, // One less nesting () => nbt.value?.BlockEntityTag?.value?.Items?.value, // One less nesting level
() => nbt.BlockEntityTag?.Items?.value?.value, // Without top value () => nbt.BlockEntityTag?.Items?.value?.value, // Without top-level value wrapper
() => nbt.BlockEntityTag?.Items?.value, // Simpler () => nbt.BlockEntityTag?.Items?.value, // Simpler BlockEntityTag path
() => nbt.BlockEntityTag?.Items, // Direct () => 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.value?.Items?.value?.value, // No BlockEntityTag
() => nbt.Items?.value?.value, // Even simpler () => nbt.Items?.value?.value, // Even simpler
() => nbt.Items, // Direct Items () => 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) { for (const pathFn of paths) {
@@ -483,6 +492,11 @@ class Scanner {
result.repairCost = nbt.RepairCost; 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; return Object.keys(result).length > 0 ? result : null;
} }
+113 -20
View File
@@ -147,7 +147,7 @@ class ShulkerHandler {
// Wait a few ticks for the item entity to spawn // Wait a few ticks for the item entity to spawn
await bot.bot.waitForTicks(3); 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`); console.log(`ShulkerHandler: Jumping onto ${placedPos} to collect drop`);
try { try {
await bot.bot.lookAt(placedPos.offset(0.5, 0, 0.5), true); await bot.bot.lookAt(placedPos.offset(0.5, 0, 0.5), true);
@@ -167,14 +167,43 @@ class ShulkerHandler {
return true; return true;
} }
// If not collected yet, pathfind directly to the drop // Attempt 2: Pathfind directly to the drop position
try { try {
await bot.goTo({ where: placedPos, range: 0 }); await bot.goTo({ where: placedPos, range: 0 });
} catch (e) { } catch (e) {
try { await bot.goTo({ where: placedPos, range: 1 }); } catch (e2) { /* ignore */ } 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++) { for (let i = 0; i < 10; i++) {
await sleep(500); await sleep(500);
if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) { if (bot.bot.inventory.items().find(item => item.name.includes('shulker_box'))) {
@@ -182,16 +211,16 @@ class ShulkerHandler {
} }
} }
console.error('ShulkerHandler: Failed to collect shulker after all attempts');
return !!bot.bot.inventory.items().find(item => item.name.includes('shulker_box')); 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. * 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) { async returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId) {
try {
// If shulker is placed on the ground, break it first // If shulker is placed on the ground, break it first
if (placedPos) { if (placedPos) {
try { try {
@@ -202,19 +231,17 @@ class ShulkerHandler {
await this.digAndCollectShulker(bot, placedPos); await this.digAndCollectShulker(bot, placedPos);
} }
// Find shulker in bot inventory // 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')); const shulkerInInv = bot.bot.inventory.items().find(item => item.name.includes('shulker_box'));
if (!shulkerInInv) { if (!shulkerInInv) {
console.error('ShulkerHandler: Recovery failed — no shulker in inventory'); throw new Error('Recovery failed — no shulker in inventory');
return false;
} }
// Go to chest and open it // Go to chest and open it
await bot.goTo({ where: chestPos, range: 3 }); await bot.goTo({ where: chestPos, range: 3 });
const chestBlock = bot.bot.blockAt(chestPos); const chestBlock = bot.bot.blockAt(chestPos);
if (!chestBlock || !chestBlock.name.includes('chest')) { if (!chestBlock || !chestBlock.name.includes('chest')) {
console.error(`ShulkerHandler: Recovery failed — no chest at ${chestPos}`); throw new Error(`Recovery failed — no chest at ${chestPos}`);
return false;
} }
const window = await bot.openContainer(chestBlock); const window = await bot.openContainer(chestBlock);
await sleep(300); await sleep(300);
@@ -231,8 +258,7 @@ class ShulkerHandler {
if (shulkerWindowSlot === null) { if (shulkerWindowSlot === null) {
await bot.bot.closeWindow(window); await bot.bot.closeWindow(window);
console.error('ShulkerHandler: Recovery failed — shulker not found in window inventory'); throw new Error('Recovery failed — shulker not found in window inventory');
return false;
} }
// Try original slot first; if occupied, find any empty chest slot // Try original slot first; if occupied, find any empty chest slot
@@ -249,8 +275,7 @@ class ShulkerHandler {
if (targetSlot === null) { if (targetSlot === null) {
await bot.bot.closeWindow(window); await bot.bot.closeWindow(window);
console.error('ShulkerHandler: Recovery failed — no empty chest slot'); throw new Error('Recovery failed — no empty chest slot');
return false;
} }
await bot.bot.moveSlotItem(shulkerWindowSlot, targetSlot); await bot.bot.moveSlotItem(shulkerWindowSlot, targetSlot);
@@ -271,12 +296,24 @@ class ShulkerHandler {
} }
} }
// 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}`); console.log(`ShulkerHandler: Recovery succeeded — shulker returned to chest slot ${targetSlot}`);
return true; return true;
} catch (error) {
console.error('ShulkerHandler: Recovery error:', error.message);
return false;
}
} }
/** /**
@@ -573,7 +610,18 @@ class ShulkerHandler {
placedPos = result.placedPos; placedPos = result.placedPos;
} catch (placeError) { } catch (placeError) {
console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message); console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId); 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 }; return { deposited: 0, updatedSlotItem: null };
} }
@@ -646,13 +694,27 @@ class ShulkerHandler {
return { deposited, updatedSlotItem }; return { deposited, updatedSlotItem };
} catch (returnError) { } catch (returnError) {
console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message); console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId);
return { deposited: 0, updatedSlotItem: null }; recovered = true;
} catch (recoverError) {
console.error('ShulkerHandler: Recovery failed:', recoverError.message);
}
if (!recovered) throw returnError;
return { deposited, updatedSlotItem: null };
} }
} catch (error) { } catch (error) {
console.error('ShulkerHandler: Deposit failed, attempting recovery:', error.message); console.error('ShulkerHandler: Deposit failed, attempting recovery:', error.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); 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 }; return { deposited: 0, updatedSlotItem: null };
} finally { } finally {
this.operationInProgress = false; this.operationInProgress = false;
@@ -822,7 +884,14 @@ class ShulkerHandler {
placedPos = result.placedPos; placedPos = result.placedPos;
} catch (placeError) { } catch (placeError) {
console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message); console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId); 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 }; return { withdrawn: 0, updatedSlotItem: null };
} }
@@ -841,6 +910,9 @@ class ShulkerHandler {
const shulkerItem = shulkerWindow.slots[s]; const shulkerItem = shulkerWindow.slots[s];
if (!shulkerItem || shulkerItem.name !== itemName) continue; 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 // Check if inventory has space
const hasInvSpace = (() => { const hasInvSpace = (() => {
for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) { for (let i = shulkerWindow.inventoryStart; i < shulkerWindow.inventoryEnd; i++) {
@@ -906,13 +978,27 @@ class ShulkerHandler {
return { withdrawn, updatedSlotItem }; return { withdrawn, updatedSlotItem };
} catch (returnError) { } catch (returnError) {
console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message); console.error('ShulkerHandler: Failed to close/break/return, recovering:', returnError.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); 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 }; return { withdrawn: 0, updatedSlotItem: null };
} }
} catch (error) { } catch (error) {
console.error('ShulkerHandler: Withdraw failed, attempting recovery:', error.message); console.error('ShulkerHandler: Withdraw failed, attempting recovery:', error.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, placedPos, chestId); 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 }; return { withdrawn: 0, updatedSlotItem: null };
} finally { } finally {
this.operationInProgress = false; this.operationInProgress = false;
@@ -945,7 +1031,14 @@ class ShulkerHandler {
placedPos = result.placedPos; placedPos = result.placedPos;
} catch (placeError) { } catch (placeError) {
console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message); console.error('ShulkerHandler: Failed to place shulker, recovering:', placeError.message);
let recovered = false;
try {
await this.returnShulkerToChest(bot, chestPos, chestSlot, null, chestId); 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 }; 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) => { router.get('/api/special-items', async (req, res) => {
if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' }); if (!dbAvailable()) return res.status(503).json({ error: 'Storage database not initialized' });
try { try {
@@ -323,6 +334,7 @@ const webUI = {
<div class="tab active" onclick="switchStorageSubTab('inventory')">Inventory</div> <div class="tab active" onclick="switchStorageSubTab('inventory')">Inventory</div>
<div class="tab" onclick="switchStorageSubTab('map')">Storage Map</div> <div class="tab" onclick="switchStorageSubTab('map')">Storage Map</div>
<div class="tab" onclick="switchStorageSubTab('special')">Special Items</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 class="tab" onclick="switchStorageSubTab('withdraw')">Withdraw</div>
</div> </div>
<div id="stab-inventory" class="stab-content active" style="margin-top:16px"> <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 id="specialItems"><div style="padding:20px;color:#6b7280;text-align:center">Click tab to load...</div></div>
</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 id="stab-withdraw" class="stab-content" style="margin-top:16px">
<div class="panel"> <div class="panel">
<h3>Request Withdrawal</h3> <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{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-withdraw button:hover{background:#1d4ed8}
.special-card .sp-status{font-size:.8em;margin-top:4px;min-height:16px} .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{display:none}
.stab-content.active{display:block} .stab-content.active{display:block}
.storage-sub-tabs{display:flex;border-bottom:1px solid #374151} .storage-sub-tabs{display:flex;border-bottom:1px solid #374151}
@@ -462,7 +485,7 @@ const webUI = {
`, `,
js: ` js: `
let allItems=[], mapData=[], sortKey='total_count', sortDir=-1; let allItems=[], mapData=[], sortKey='total_count', sortDir=-1;
let specialLoaded=false, storageSubTab='inventory'; let specialLoaded=false, mapsLoaded=false, storageSubTab='inventory';
function onStorageTabActive() { function onStorageTabActive() {
if (allItems.length === 0) { loadStats(); loadInventory(); loadPlayers(); } if (allItems.length === 0) { loadStats(); loadInventory(); loadPlayers(); }
@@ -474,12 +497,13 @@ function switchStorageSubTab(name) {
document.querySelectorAll('.stab-content').forEach(t => t.classList.remove('active')); document.querySelectorAll('.stab-content').forEach(t => t.classList.remove('active'));
const el = document.getElementById('stab-'+name); const el = document.getElementById('stab-'+name);
if (el) el.classList.add('active'); 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 tabs = document.querySelectorAll('.storage-sub-tabs .tab');
const idx = subNames.indexOf(name); const idx = subNames.indexOf(name);
if (idx >= 0 && tabs[idx]) tabs[idx].classList.add('active'); if (idx >= 0 && tabs[idx]) tabs[idx].classList.add('active');
if (name === 'map' && mapData.length === 0) loadMap(); if (name === 'map' && mapData.length === 0) loadMap();
if (name === 'special' && !specialLoaded) loadSpecialItems(); if (name === 'special' && !specialLoaded) loadSpecialItems();
if (name === 'maps' && !mapsLoaded) loadMaps();
} }
async function loadStats() { 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){} 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()}
`, `,
}; };
+111 -110
View File
@@ -125,9 +125,8 @@ class CJbot{
this.mcData = minecraftData(this.bot.version); this.mcData = minecraftData(this.bot.version);
this.defaultMove = new Movements(this.bot, this.mcData); this.defaultMove = new Movements(this.bot, this.mcData);
this.defaultMove.canDig = false; 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 = {}; if (!this.defaultMove.blocksCost) this.defaultMove.blocksCost = {};
const avoidBlocks = ['chest', 'trapped_chest', 'ender_chest', const avoidBlocks = ['chest', 'trapped_chest', 'ender_chest',
'shulker_box', 'white_shulker_box', 'orange_shulker_box', 'shulker_box', 'white_shulker_box', 'orange_shulker_box',
@@ -139,7 +138,21 @@ class CJbot{
for (const name of avoidBlocks) { for (const name of avoidBlocks) {
const block = this.mcData.blocksByName[name]; const block = this.mcData.blocksByName[name];
if (block) this.defaultMove.blocksCost[block.id] = 100; 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.bot.pathfinder.setMovements(this.defaultMove);
this._setupAntiStuck(); this._setupAntiStuck();
@@ -556,64 +569,44 @@ class CJbot{
// to free it from corners. This works for ALL pathfinder movement globally. // to free it from corners. This works for ALL pathfinder movement globally.
_antiStuckNudging = false; _antiStuckNudging = false;
_antiStuckNudging = false;
_setupAntiStuck() { _setupAntiStuck() {
let lastPos = null; let lastPos = null;
let stuckTicks = 0; let stuckTicks = 0;
let nudgeTicks = 0;
let nudgeCount = 0;
let idleTicks = 0;
this.bot.on('physicsTick', () => { this.bot.on('physicsTick', () => {
// During a nudge, count ticks then clear controls if (!this.bot.pathfinder.isMoving() || this._antiStuckNudging) return;
if (this._antiStuckNudging) {
nudgeTicks++;
if (nudgeTicks >= 8) { // ~400ms of nudge movement
this.bot.clearControlStates();
this._antiStuckNudging = false;
nudgeTicks = 0;
}
return;
}
// Track position regardless of pathfinder state — goTo clears
// the goal between retries which would reset our counter
const pos = this.bot.entity.position; const pos = this.bot.entity.position;
if (!lastPos) { lastPos = pos.clone(); return; }
if (!this.bot.pathfinder.isMoving()) { const dist = lastPos.distanceTo(pos);
// 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;
}
idleTicks = 0; // On 20 TPS / LAN, any distance under 0.01 is a hard collision
if (dist < 0.01) {
if (!lastPos) {
lastPos = pos.clone();
return;
}
if (lastPos.distanceTo(pos) < 0.05) {
stuckTicks++; stuckTicks++;
if (stuckTicks >= 12) { // ~600ms with no movement if (stuckTicks >= 15) { // 750ms is plenty of time on a 20 TPS server
nudgeCount++; console.log(`[AntiStuck] LAN-Precision Reset at ${pos.x.toFixed(2)}, ${pos.z.toFixed(2)}`);
const directions = ['back', 'left', 'right'];
const dir = directions[nudgeCount % 3];
console.log(`AntiStuck: No movement for ${stuckTicks} ticks, nudging ${dir}`);
// Stop pathfinder so our controls take effect // 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.pathfinder.stop();
this.bot.entity.velocity.set(0, 0, 0);
// 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;
// Apply nudge direction
this.bot.setControlState(dir, true);
this._antiStuckNudging = true; this._antiStuckNudging = true;
nudgeTicks = 0; setTimeout(() => { this._antiStuckNudging = false; }, 300);
stuckTicks = 0; stuckTicks = 0;
lastPos = null; lastPos = null;
} }
@@ -628,86 +621,94 @@ class CJbot{
let range = options.range || 2; let range = options.range || 2;
let block = this.__blockOrVec(options.where); let block = this.__blockOrVec(options.where);
let retries = 0; let retries = 0;
let noPathCount = 0; let lastPos = this.bot.entity.position.clone();
const maxRetries = options.maxRetries || 5;
const unjamDirections = ['back', 'left', 'right', 'forward'];
while(!this.isWithinRange(this.__blockOrVec(options.where).position, range)){ console.log(`[goTo] Starting path to ${block.position} with range ${range}`);
if(retries >= maxRetries){
// All pathfinder attempts failed — clear goal, relocate, retry fresh
console.log(`goTo: Pathfinder failed ${maxRetries} times, relocating and retrying`);
this.bot.pathfinder.setGoal(null); // Listen for path updates to detect partial paths
await sleep(200); const pathUpdateHandler = (results) => {
console.log(`[Pathfinder] New path found. Length: ${results.path.length} | Status: ${results.status}`);
// Walk backward and randomly strafe to a new position };
const side = Math.random() < 0.5 ? 'left' : 'right'; this.bot.on('path_update', pathUpdateHandler);
this.bot.setControlState('back', true);
this.bot.setControlState(side, true);
await sleep(600);
this.bot.clearControlStates();
await sleep(500);
// Reset retries and try again
retries = 0;
continue;
}
try { try {
// Timeout pathfinder after 30 seconds to prevent infinite hangs while (!this.isWithinRange(block.position, range)) {
await Promise.race([ try {
this.bot.pathfinder.goto( await this.bot.pathfinder.goto(new GoalNear(...block.position.toArray(), range));
new GoalNear(...block.position.toArray(), range) console.log(`[goTo] Successfully reached goal.`);
), break;
new Promise((_, reject) =>
setTimeout(() => {
this.bot.pathfinder.stop();
reject(new Error('goTo: Pathfinder timed out after 30s'));
}, 30000)
)
]);
} catch (error) { } catch (error) {
retries++; retries++;
const msg = error.message || String(error); const errorMsg = error.message || error;
const target = block.position; console.log(`%c[goTo] Error on Attempt ${retries}: ${errorMsg}`, "color: red;");
console.log(`goTo: Attempt ${retries}/${maxRetries} to ${target} error: ${msg}`);
// "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); this.bot.pathfinder.setGoal(null);
return false;
} const botPos = this.bot.entity.position;
// One retry: increase range in case we're just barely blocked const dist = botPos.distanceTo(block.position);
range = Math.min(range + 1, 5); console.log(`[Debug] Target Block: ${this.bot.blockAt(block.position)?.name} | Bot Pos: ${botPos} | Distance: ${dist.toFixed(2)}`);
console.log(`goTo: No path, increasing range to ${range} and retrying`);
this.bot.pathfinder.setGoal(null); // If we're within extended range on a partial path, call it good enough
await sleep(500); if (dist <= range + 4) {
continue; console.log(`[goTo] Close enough (${dist.toFixed(2)} <= ${range + 4}), accepting partial path`);
break;
} }
// Wait for any ongoing anti-stuck nudge to finish // Detect stuck-ness: compare position to last attempt
while(this._antiStuckNudging) await sleep(100); const moved = botPos.distanceTo(lastPos);
console.log(`[goTo] Moved ${moved.toFixed(2)} blocks this attempt`);
lastPos = botPos.clone();
// Clear pathfinder state // 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);
}
// 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); this.bot.pathfinder.setGoal(null);
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); await sleep(200);
// Unjam: cycle through different directions each retry if (retries >= 20) {
const dir = unjamDirections[(retries - 1) % unjamDirections.length]; console.log(`[goTo] Failed after ${retries} attempts — giving up`);
const side = Math.random() < 0.5 ? 'left' : 'right'; return false;
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);
} }
} }
}
} finally {
this.bot.removeListener('path_update', pathUpdateHandler);
}
return true; return true;
} }
+10
View File
@@ -17,6 +17,7 @@
"minecraft-data": "^3.105.0", "minecraft-data": "^3.105.0",
"mineflayer": "^4.35.0", "mineflayer": "^4.35.0",
"mineflayer-pathfinder": "^2.4.5", "mineflayer-pathfinder": "^2.4.5",
"pngjs": "^7.0.0",
"prismarine-windows": "^2.9.0", "prismarine-windows": "^2.9.0",
"sqlite": "^5.1.1", "sqlite": "^5.1.1",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7"
@@ -2401,6 +2402,15 @@
"url": "https://github.com/sponsors/jonschlinkert" "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": { "node_modules/prebuild-install": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "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", "minecraft-data": "^3.105.0",
"mineflayer": "^4.35.0", "mineflayer": "^4.35.0",
"mineflayer-pathfinder": "^2.4.5", "mineflayer-pathfinder": "^2.4.5",
"pngjs": "^7.0.0",
"prismarine-windows": "^2.9.0", "prismarine-windows": "^2.9.0",
"sqlite": "^5.1.1", "sqlite": "^5.1.1",
"sqlite3": "^5.1.7" "sqlite3": "^5.1.7"
Binary file not shown.