This commit is contained in:
2026-02-22 20:27:09 -05:00
parent 7b326a112e
commit 92024c8a64
37 changed files with 6785 additions and 3344 deletions
+246 -115
View File
@@ -1,7 +1,5 @@
'use strict';
process.env.DEBUG = 'mineflayer:*'; // Enables all debugging logs
const mineflayer = require('mineflayer');
const minecraftData = require('minecraft-data');
const { pathfinder, Movements, goals: { GoalNear } } = require('mineflayer-pathfinder');
@@ -56,10 +54,16 @@ class CJbot{
this.autoReConnect = 'autoConnect' in args ? args.autoReConnect : true;
this.autoConnect = 'autoConnect' in args ? args.autoConnect : true;
// On-demand mode: bot stays offline until ensureConnected() is called
this.onDemand = args.onDemand || false;
this._idleTimer = null;
this._taskQueue = [];
this._connecting = false;
this._idleTimeout = args.idleTimeout || 30000;
// If we want the be always connected, kick off the function to auto
// reconnect
if(this.autoReConnect) this.__autoReConnect()
if(this.autoReConnect && !this.onDemand) this.__autoReConnect()
}
connect(){
@@ -86,7 +90,7 @@ class CJbot{
// to the caller of the function
this.bot.on('end', (reason, ...args)=>{
console.log(this.name, 'Connection ended:', reason, ...args);
this.pluginUnloadAll();
this.pluginUnloadAll(this.onDemand); // keepDb=true for on-demand
this.isReady = false;
reject(reason);
});
@@ -97,23 +101,10 @@ class CJbot{
await sleep(2000);
this.__onReady();
resolve();
this.pluginLoadAll();
this._pluginsReady = this.pluginLoadAll();
});
// Set a timer to try to connect again in 30 seconds if the bot is
// not connected
/* setTimeout(async ()=>{
try{
if(this.autoReConnect && !this.isReady){
console.log()
await this.connect();
}
}catch(error){
console.error('minecraft.js | connect | setTimeout |', this.name, ' ', error)
}
}, 30000);*/
}catch(error){
}catch(error){
console.log('CJbot.connect Error', error);
reject(error);
}
@@ -133,8 +124,25 @@ class CJbot{
this.bot.loadPlugin(pathfinder);
this.mcData = minecraftData(this.bot.version);
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
if (!this.defaultMove.blocksCost) this.defaultMove.blocksCost = {};
const avoidBlocks = ['chest', 'trapped_chest', 'ender_chest',
'shulker_box', 'white_shulker_box', 'orange_shulker_box',
'magenta_shulker_box', 'light_blue_shulker_box', 'yellow_shulker_box',
'lime_shulker_box', 'pink_shulker_box', 'gray_shulker_box',
'light_gray_shulker_box', 'cyan_shulker_box', 'purple_shulker_box',
'blue_shulker_box', 'brown_shulker_box', 'green_shulker_box',
'red_shulker_box', 'black_shulker_box'];
for (const name of avoidBlocks) {
const block = this.mcData.blocksByName[name];
if (block) this.defaultMove.blocksCost[block.id] = 100;
}
this.bot.pathfinder.setMovements(this.defaultMove);
this._setupAntiStuck();
// Add the listeners to the bot. We do this so if the bot loses
// connection, the mineflayer instance will also be lost.
@@ -154,12 +162,8 @@ class CJbot{
this.__listen();
this.bot.on('title', (...args)=>console.log('on title', args))
// this.bot.on('path_update', (...args)=>{ console.log('EVENT path_update', args) })
// this.bot.on('goal_updated', (...args)=>{ console.log('EVENT goal_updated', args) })
// this.bot.on('path_reset', (...args)=>{ console.log('EVENT path_reset', args) })
// this.bot.on('path_stop', (...args)=>{ console.log('EVENT path_stop', args) })
}catch(error){
console.error('minecraft.js | __onReady | ', this.name, ' ', error);
}}
@@ -252,14 +256,19 @@ class CJbot{
static plungins = {};
static pluginAdd(cls){
this.plungins[cls.name] = cls
this.plungins[cls.name] = cls;
// Queue for web registration if plugin has web support
if (typeof cls.createRouter === 'function' || cls.webUI) {
const webServer = require('../controller/web-server');
webServer.queuePlugin(cls);
}
}
plunginsLoaded = {};
pluginLoadAll(){
async pluginLoadAll(){
for(let pluginName in this.pluginsWanted){
this.pluginLoad(pluginName, this.pluginsWanted[pluginName]);
await this.pluginLoad(pluginName, this.pluginsWanted[pluginName]);
}
}
@@ -280,12 +289,12 @@ class CJbot{
}
}
async pluginUnloadAll(){
console.log('CJbot.pluginUnloadAll');
async pluginUnloadAll(keepDb = false){
console.log('CJbot.pluginUnloadAll', keepDb ? '(keepDb)' : '');
for(let pluginName in this.plunginsLoaded){
console.log('CJbot.pluginUnloadAll loop', pluginName)
try{
await this.plunginsLoaded[pluginName].unload()
await this.plunginsLoaded[pluginName].unload(keepDb);
delete this.plunginsLoaded[pluginName];
}catch(error){
console.log('CJbot.pluginUnload loop error:', error)
@@ -293,6 +302,54 @@ class CJbot{
}
}
/* On-demand lifecycle */
async ensureConnected(taskFn) {
this._resetIdleTimer();
if (this.isReady) return await taskFn();
return new Promise((resolve, reject) => {
this._taskQueue.push({ fn: taskFn, resolve, reject });
if (this._connecting) return;
this._connecting = true;
this.connect().then(async () => {
this._connecting = false;
// Wait for plugins to finish loading (pluginLoadAll runs after spawn)
await this._pluginsReady;
await this._drainTaskQueue();
}).catch((error) => {
this._connecting = false;
const queue = this._taskQueue.splice(0);
for (const task of queue) task.reject(error);
});
});
}
async _drainTaskQueue() {
while (this._taskQueue.length > 0) {
const task = this._taskQueue.shift();
try {
task.resolve(await task.fn());
} catch (error) {
task.reject(error);
}
this._resetIdleTimer();
}
}
_resetIdleTimer() {
if (!this.onDemand) return;
if (this._idleTimer) clearTimeout(this._idleTimer);
this._idleTimer = setTimeout(() => this._idleDisconnect(), this._idleTimeout);
}
_idleDisconnect() {
if (!this.isReady) return;
console.log(`${this.name}: Idle timeout, disconnecting on-demand bot`);
this.quit(true);
}
/* Chat and messaging*/
__listen(){
@@ -363,7 +420,6 @@ class CJbot{
async say(...messages){
for(let message of messages){
// console.log('next chat time:', this.nextChatTime > Date.now(), Date.now()+1, this.nextChatTime-Date.now()+1);
(async (message)=>{
if(this.nextChatTime > Date.now()){
await sleep(this.nextChatTime-Date.now()+1)
@@ -404,26 +460,26 @@ class CJbot{
}
async __doCommand(from, command){try{
if(this.commandLock){
let [cmd, ...parts] = command.split(/\s+/);
if(!this.__reduceCommands(from).includes(cmd)) return;
const cmdDef = this.commands[cmd];
if(this.commandLock && !cmdDef.ignoreLock){
this.whisper(from, `cool down, try again in ${this.commandCollDownTime/1000} seconds...`);
return ;
}
let [cmd, ...parts] = command.split(/\s+/);
if(this.__reduceCommands(from).includes(cmd)){
this.commandLock = true;
try{
await this.commands[cmd].function.call(this, from, ...parts);
}catch(error){
this.whisper(from, `The command encountered an error.`);
this.whisper(from, `ERROR: ${error}`);
console.error(`Chat command error on ${cmd} from ${from}\n`, error);
}
this.__unLockCommand();
}/*else{
this.whisper(from, `I dont know anything about ${cmd}`);
}*/
if(!cmdDef.ignoreLock) this.commandLock = true;
try{
await cmdDef.function.call(this, from, ...parts);
}catch(error){
this.whisper(from, `The command encountered an error.`);
this.whisper(from, `ERROR: ${error}`);
console.error(`Chat command error on ${cmd} from ${from}\n`, error);
}
if(!cmdDef.ignoreLock) this.__unLockCommand();
}catch(error){
console.error('minecraft.js | __doCommand |', this.name, ' ', error)
}}
@@ -494,38 +550,161 @@ class CJbot{
return distance < range;
}
areGoalsWithinRange(goal1, goal2) {
const dx = goal1.x - goal2.x;
const dy = goal1.y - goal2.y;
const dz = goal1.z - goal2.z;
// Global anti-stuck system: monitors every physics tick while pathfinder is
// moving. If the bot hasn't moved for ~600ms (12 ticks), it stops the
// pathfinder and nudges the bot in an alternating direction (back/left/right)
// to free it from corners. This works for ALL pathfinder movement globally.
_antiStuckNudging = false;
const distanceSq = dx * dx + dy * dy + dz * dz;
_setupAntiStuck() {
let lastPos = null;
let stuckTicks = 0;
let nudgeTicks = 0;
let nudgeCount = 0;
let idleTicks = 0;
// Compare with the maximum allowed squared range (rangeSq)
return distanceSq <= goal1.rangeSq && distanceSq <= goal2.rangeSq;
this.bot.on('physicsTick', () => {
// During a nudge, count ticks then clear controls
if (this._antiStuckNudging) {
nudgeTicks++;
if (nudgeTicks >= 8) { // ~400ms of nudge movement
this.bot.clearControlStates();
this._antiStuckNudging = false;
nudgeTicks = 0;
}
return;
}
// Track position regardless of pathfinder state — goTo clears
// the goal between retries which would reset our counter
const pos = this.bot.entity.position;
if (!this.bot.pathfinder.isMoving()) {
// Not pathfinding — only reset if we've been idle a while
// (short gaps between goTo retries shouldn't reset)
idleTicks++;
if (idleTicks > 40) { // ~2 seconds of no pathfinding = truly idle
stuckTicks = 0;
lastPos = null;
nudgeCount = 0;
}
return;
}
idleTicks = 0;
if (!lastPos) {
lastPos = pos.clone();
return;
}
if (lastPos.distanceTo(pos) < 0.05) {
stuckTicks++;
if (stuckTicks >= 12) { // ~600ms with no movement
nudgeCount++;
const directions = ['back', 'left', 'right'];
const dir = directions[nudgeCount % 3];
console.log(`AntiStuck: No movement for ${stuckTicks} ticks, nudging ${dir}`);
// Stop pathfinder so our controls take effect
this.bot.pathfinder.stop();
// Apply nudge direction
this.bot.setControlState(dir, true);
this._antiStuckNudging = true;
nudgeTicks = 0;
stuckTicks = 0;
lastPos = null;
}
} else {
stuckTicks = 0;
lastPos = pos.clone();
}
});
}
async goTo(options) {
let range = options.range || 2;
let block = this.__blockOrVec(options.where);
let retries = 0;
let noPathCount = 0;
const maxRetries = options.maxRetries || 5;
const unjamDirections = ['back', 'left', 'right', 'forward'];
while(!this.isWithinRange(this.__blockOrVec(options.where).position, range)){
if(retries >= maxRetries){
// All pathfinder attempts failed — clear goal, relocate, retry fresh
console.log(`goTo: Pathfinder failed ${maxRetries} times, relocating and retrying`);
this.bot.pathfinder.setGoal(null);
await sleep(200);
// Walk backward and randomly strafe to a new position
const side = Math.random() < 0.5 ? 'left' : 'right';
this.bot.setControlState('back', true);
this.bot.setControlState(side, true);
await sleep(600);
this.bot.clearControlStates();
await sleep(500);
// Reset retries and try again
retries = 0;
continue;
}
try{
console.log('goal', this.bot.pathfinder.goal);
if(this.bot.pathfinder.isMoving()){
// Timeout pathfinder after 30 seconds to prevent infinite hangs
await Promise.race([
this.bot.pathfinder.goto(
new GoalNear(...block.position.toArray(), range)
),
new Promise((_, reject) =>
setTimeout(() => {
this.bot.pathfinder.stop();
reject(new Error('goTo: Pathfinder timed out after 30s'));
}, 30000)
)
]);
}catch(error){
retries++;
const msg = error.message || String(error);
const target = block.position;
console.log(`goTo: Attempt ${retries}/${maxRetries} to ${target} error: ${msg}`);
// "No path" = pathfinder searched full graph, route doesn't exist.
// Walking around won't help — skip this target after 2 attempts.
if(msg.includes('No path')){
noPathCount++;
if(noPathCount >= 2){
console.log(`goTo: No path to ${target} after ${noPathCount} attempts, skipping`);
this.bot.pathfinder.setGoal(null);
return false;
}
// One retry: increase range in case we're just barely blocked
range = Math.min(range + 1, 5);
console.log(`goTo: No path, increasing range to ${range} and retrying`);
this.bot.pathfinder.setGoal(null);
await sleep(500);
console.log('the bot is moving...');
continue;
}
await this.bot.pathfinder.goto(
new GoalNear(...block.position.toArray(), range)
);
}catch(error){
// await sleep(500);
console.log('CJbot.goTo while loop error:', error)
// await this.bot.pathfinder.setGoal(null);
// await this.bot.pathfinder.stop();
await sleep(500);
// Wait for any ongoing anti-stuck nudge to finish
while(this._antiStuckNudging) await sleep(100);
// Clear pathfinder state
this.bot.pathfinder.setGoal(null);
await sleep(200);
// Unjam: cycle through different directions each retry
const dir = unjamDirections[(retries - 1) % unjamDirections.length];
const side = Math.random() < 0.5 ? 'left' : 'right';
console.log(`goTo: Unjamming — walking ${dir} + ${side}`);
this.bot.setControlState(dir, true);
this.bot.setControlState(side, true);
await sleep(800);
this.bot.clearControlStates();
await sleep(300);
}
}
@@ -591,20 +770,6 @@ class CJbot{
this.bot.activateBlock(block);
let window = await this.once('windowOpen');
// while(!this.bot.currentWindow){
// try{
// if(this.bot.currentWindow?.title){
// break;
// }
// this.bot.removeAllListeners('windowOpen');
// if(count++ == 3) throw 'Block wont open';
// }catch(error){
// console.error('ERROR in CJbot.openCraftingTable:', error)
// }
// }
return window;
}
@@ -664,31 +829,6 @@ class CJbot{
await window.close();
/* // Get the inventory of the chest block
const chestInventory = chestBlock.getInventory();
// Iterate through the chest's inventory
chestInventory.forEach((slot, index) => {
// Check if the slot contains a shulker box
if (slot && slot.type === 'shulker_box') {
// Retrieve the shulker's inventory
const shulkerInventory = slot.getInventory();
// Check if the shulker is full of the specified item
const isFull = shulkerInventory.every(shulkerSlot => {
console.log('shulkerSlot', shulkerSlot)
return shulkerSlot && shulkerSlot.id === item.id && shulkerSlot.count === 64; // Assuming max stack size is 64
});
// If full, add the shulker box to the list
if (isFull) {
fullShulkers.push(slot);
}
}
});
return fullShulkers;*/
}
async dumpToChest(block, blockName, amount) {
@@ -706,14 +846,6 @@ class CJbot{
let currentSlot = Number(item.slot);
if(!window.slots[currentSlot]) continue;
// let chestSlot = await this.__nextContainerSlot(window, item);
// console.log('next chest slot', chestSlot)
// if(!chestSlot){
// console.log(`No room for ${item.name}`)
// continue;
// }
try{
await this.bot.transfer({
window,
@@ -729,7 +861,6 @@ class CJbot{
}catch(error){
console.log('error?', item.count, error.message, error);
}
// await this.bot.moveSlotItem(currentSlot, chestSlot);
}
await sleep(1000);