'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'); const Vec3 = require('vec3'); const {sleep} = require('../utils'); class CJbot{ isReady = false; listeners = {}; // Holds the minimum cool down time for chat messages nextChatTime = 1500; // Prevents executing commands while one is running commandLock = false; commandCollDownTime = 1500; // Holds the list of commands this bot can execute commands = {}; combatTag = false; doLogOut = false; // Map of all the bots currently in use static bots = {}; // Forces the code to wait while until the bot is loaded before moving on to // the next static __addLock = false; // Holds a list of bots that still needs to be connect static __toConnect = []; constructor(args){ // Friendly name to access the bot in `CJbot.bots` this.name = args.name || args.username; this.username = args.username; this.password = args.password; this.host = args.host; this.auth = args.auth || 'microsoft'; this.version = args.version || '1.20.1'; this.hasAi = args.hasAi; // States if the bot should connect when its loaded this.autoReConnect = 'autoConnect' in args ? args.autoReConnect : true; this.autoConnect = 'autoConnect' in args ? args.autoConnect : true; // If we want the be always connected, kick off the function to auto // reconnect if(this.autoReConnect) this.__autoReConnect() } connect(){ return new Promise((resolve, reject) =>{ try{ // Create the mineflayer instance this.bot = mineflayer.createBot({ host: this.host, username: this.username, password: this.password, version: this.version, auth: this.auth, }); // If an error happens before the login event, toss an error back to // the caller of the function let onError = this.bot.on('error', (m)=>{ console.log('ERROR CJbot.connect on error:', this.name, m.toString()); reject(m); }) // If the connection ends before the login event, toss an error back // to the caller of the function this.bot.on('end', (reason, ...args)=>{ console.log(this.name, 'Connection ended:', reason, ...args); this.isReady = false; reject(reason); }); // When the bot is ready, return to the caller success this.bot.on('spawn', async()=>{ console.log('CJbot.connect on spawn') this.__onReady(); resolve(); }); // 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) await this.connect(); }catch(error){ console.error('minecraft.js | connect | setTimeout |', this.name, ' ', error) }}, 30000); }catch(error){ reject(error); } }); } // Wrap the once method so it works correctly with await once(event){ return new Promise((resolve, reject)=> this.bot.once(event, resolve)); } // Internal method to kick off the functionality after its loaded in the // server async __onReady(){try{ this.bot.loadPlugin(pathfinder); this.mcData = minecraftData(this.bot.version); this.defaultMove = new Movements(this.bot, this.mcData); this.defaultMove.canDig = false this.bot.pathfinder.setMovements(this.defaultMove); // Add the listeners to the bot. We do this so if the bot loses // connection, the mineflayer instance will also be lost. this.isReady = true; this.__error(); this.__startListeners(); // Call the internal listeners when the bot is ready for(let callback of this.listeners.onReady || []){ callback.call(this); } console.log('Bot is ready', this.bot.entity.username, this.username); // Start chat listeners this.__listen(); // 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); }} __startListeners(){ for(let event in this.listeners){ console.log('__adding listeners', event) for(let callback of this.listeners[event]){ this.bot.on(event, callback); } } } // Wrap the .on method so we can hold the listeners internally on(event, callback){ if(!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); // If the bot is already loaded, add the passed listener to the // mineflayer instance if(this.isReady){ if(event === 'onReady') callback(this); else this.bot.on(event, callback); } return ()=> this.off(event, callback); } // Remove listener for events off(event, callback) { console.log('off', event, callback) if (!this.listeners[event]) return false; const index = this.listeners[event].indexOf(callback); if (index === -1) return false; this.listeners[event].splice(index, 1); // If bot is ready, also remove from the Mineflayer bot if (this.isReady) { this.bot.off(event, callback); } return true; } // Listen for ending events and call connect again __autoReConnect(){ try{ console.log('auto connect function') this.on('end', async (reason)=>{ console.error('_autorestart MC on end', reason) await sleep(30000) this.connect() }); this.on('kick', console.error) this.on('error', (error)=>{ console.error('MC on error', error); this.connect(); }); }catch(error){ console.error('error in __autoReConnect', error); } } __error(message){ this.bot.on('error', (error)=>{ console.error(`ERROR!!! MC bot ${this.username} on ${this.host} had an error:\n`, error) }); } quit(force){ if(!this.combatTag || force){ this.bot.quit(); }else{ console.log('Logout prevented due to combatTag'); this.doLogOut = true; } } /* Plugins */ static plungins = {}; static pluginAdd(cls){ this.plungins[cls.name] = cls } plunginsLoaded = {}; async pluginLoad(pluginName, opts){ console.log('CJbot.pluginLoad', pluginName) let plugin = new this.constructor.plungins[pluginName]({...opts, bot:this}) await plugin.init(); this.plunginsLoaded[pluginName] = plugin; } async pluginUnload(name){ console.log('CJbot.pluginUnload', name) if(this.plunginsLoaded[name]){ await this.plunginsLoaded[name].unload(); delete this.plunginsLoaded[name]; console.log('CJbot.pluginUnload', name, 'done'); return true; } } /* Chat and messaging*/ __listen(){ this.bot.on('chat', (from, message)=>{ // Ignore messages from this bot if(from === this.bot.entity.username) return; // Filter messages to this bot if(message.startsWith(this.bot.entity.username)){ this.__doCommand( from, message.replace(`${this.bot.entity.username} ` , ' ').trim() ) } }); this.bot.on('whisper', (from, message)=>{ this.__doCommand( from, message.replace(`${this.bot.entity.username} ` , ' ').trim() ) }); this.bot.on('message', (message, type)=>{ if(message.toString().includes(' invited you to teleport to him.')){ // teleport invite console.log('found teleport', message.toString().split(' ')[0]) this.__doCommand(message.toString().split(' ')[0], '.invite'); } if(message.toString().includes(' wants to trade with you!')){ // teleport invite console.log('found Trade', message.toString().split(' ')[0]) this.__doCommand(message.toString().split(' ')[0], '.trade'); } if(message.toString().includes('You are combat tagged by')){ try{ this.combatTag = true; console.log('was attacked by') let attacker = message.toString().split('. ')[0].replace('You are combat tagged by ', '') console.log('was attacked by', attacker) // teleport invite this.whisper(attacker, 'Please do not attack me, I am a bot.') }catch(error){ console.log('error!!!!!!', error) } } if(message.toString().includes('You are no longer in combat. You may now logout.')){ this.combatTag = false if(this.doLogOut){ this.quit() } } }); } __chatCoolDown(){ return Math.floor(Math.random() * (3000 - 2000) + 2000); } 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) } this.bot.chat(message); })(message); this.nextChatTime = Date.now() + this.__chatCoolDown(); } } async sayAiSafe(...messages){ for(let message of messages){ if(message.startsWith('/') && !(message.startsWith('/msg') || message.startsWith('/help'))){ console.log('bot tried to execute bad command', message); message = '.'+message; } await this.say(message); } } async whisper(to, ...messages){ await this.say(...messages.map(message=>`/msg ${to} ${message}`)); } /* Commands */ async __unLockCommand(time){ await sleep(this.commandCollDownTime); this.commandLock = false; } __reduceCommands(from){ return Object.keys(this.commands).filter(command =>{ if (this.commands[command].allowed && !this.commands[command].allowed.includes(from)) return false; return true; }); } async __doCommand(from, command){try{ if(this.commandLock){ 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}`); }*/ }catch(error){ console.error('minecraft.js | __doCommand |', this.name, ' ', error) }} addCommand(name, obj){ if(this.commands[name]) return false; this.commands[name] = obj; } getPlayers(){ for (let [username, value] of Object.entries(this.bot.players)){ value.lvl = Number(value.displayName.extra[0].text) } return this.bot.players; } /* Actions */ __blockOrVec(thing){ if(thing instanceof Vec3.Vec3) return this.bot.blockAt(thing); if(thing.constructor && thing.constructor.name === 'Block') return thing; throw new Error('Not supported block identifier'); } findBlockBySign(text){ return this.bot.findBlock({ useExtraInfo: true, maxDistance: 64, matching: (block)=> { if(block.name.includes('sign') && block.signText.includes(text)){ return true; } } }); } findChestBySign(text){ return this.bot.findBlock({ point: this.findBlockBySign(text).position, // maxDistance: 1, useExtraInfo: true, matching: block => block.name === 'chest' }); } isWithinRange(target, range=2) { const botPos = this.bot.entity.position; const distance = botPos.distanceTo(target); return distance <= range+.9; } async _goTo(block, range=2){ try{ }catch(error){ if(error.message === 'Not supported block identifier'){ console.log('found block error') await sleep(1000); block = this.__blockOrVec(block); } console.log('other', error) } try{ }catch(error){ if(error.name === 'GoalChanged') return await this._goTo(block, range); console.log('error in _goTo', error.name, '|', error.message); } } async goTo(options) { let range = options.range || 2; let block = this.__blockOrVec(options.where); while(!this.isWithinRange(this.__blockOrVec(options.where).position, range)){ try{ if(this.bot.pathfinder.isMoving()){ 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); } } return true; } async goToReturn(options){ let here = this.bot.entity.position; let hereYaw = this.bot.entity.yaw await this.goTo(options); return async () =>{ await this.goTo({where: here, range: 0}, true); await sleep(500); await this.bot.look(Math.floor(hereYaw), 0); } } async __nextContainerSlot(window, item) { let firstEmptySlot = false; await window.containerItems(); for(let slot in window.slots.slice(0, window.inventoryStart)){ if(window.slots[slot] === null ){ if(!Number.isInteger(firstEmptySlot)) firstEmptySlot = Number(slot); continue; } if(item.type === window.slots[slot].type && window.slots[slot].count < window.slots[slot].stackSize){ return slot; } } return firstEmptySlot; } async openContainer(block){ let count = 0; block = this.__blockOrVec(block); let window; while(!this.bot.currentWindow){ try{ window = await this.bot.openContainer(block); }catch(error){ if(!error.message.includes('Event windowOpen did not fire within timeout')) throw error; } if(this.bot.currentWindow?.title){ break; } this.bot.removeAllListeners('windowOpen'); await sleep(1500); if(count++ == 3) throw 'Block wont open'; } return this.bot.currentWindow; } async openCraftingTable(block){ let count = 0; block = this.__blockOrVec(block); 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; } async checkItemsFromContainer(containerBlock, itemName, count){ let currentSlot = 0; let foundCount = 0; let window = await this.openContainer(containerBlock); for(let slot of window.slots){ if(currentSlot++ === window.inventoryStart) break;currentSlot if(!slot) continue; if(slot.name === itemName) foundCount += slot.count; } await this.bot.closeWindow(window); if(foundCount >= count) return true; } async getItemsFromChest(containerBlock, itemName, count){ let window = await this.openContainer(containerBlock); await sleep(500); // console.log('item id', this.mcData.itemsByName[itemName], this.mcData) await window.withdraw(this.mcData.itemsByName[itemName].id, null, count); await this.bot.closeWindow(window); } async getFullShulkersFromChest(chestBlock, item) { const fullShulkers = []; let window = await this.openContainer(chestBlock); let currentSlot = 0; for(let slot of window.slots){ if(currentSlot++ === window.inventoryStart) break; if(!slot) continue; // if(!slot || slot.name !== 'shulker_box') continue; console.log('slot:', slot) if(slot.nbt){ // console.log(slot.nbt) console.log('BlockEntityTag:', slot.nbt.value.BlockEntityTag.value.Items.value.value) } } /* // 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) { let window = await this.openContainer(block); let items = window.slots.slice(window.inventoryStart).filter(function(item){ if(!item) return false; if(blockName && blockName !== item.name) return false; return true; }); for(let item of items){ await sleep(500); 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, itemType: this.mcData.itemsByName[item.name].id, sourceStart: currentSlot, sourceEnd: currentSlot+1, destStart: 0, destEnd: window.inventoryStart-1, count: amount || item.count, }) if(amount) amount -= item.count }catch(error){ console.log('error?', item.count, error.message, error); } // await this.bot.moveSlotItem(currentSlot, chestSlot); } await sleep(1000); await this.bot.closeWindow(window); return amount ? amount : true; } } module.exports = {CJbot};