'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(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('login', ()=>{ 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.__startListeners(); // Call the internal listeners when the bot is ready for(let callback of this.listeners.onReady || []){ console.log('calling listener', callback); await callback.call(this); } this.isReady = true; this.__error(); console.log('Bot is ready', this.bot.entity.username, this.username); // Start chat listeners this.__listen(); }catch(error){ console.error('minecraft.js | __onReady | ', this.name, ' ', error); }} __startListeners(){ for(let event in this.listeners){ 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 event === 'onReady' ? true : ()=> this.bot.off(listener, callback); } // todo; add .off wrapper // 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; } } /* 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()){ console.log('am sleeping'); 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; } __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'); } async _goTo(block, range=2){ block = this.__blockOrVec(block); return await this.bot.pathfinder.goto(new GoalNear(...block.position.toArray(), range)); } goTo(options){ return new Promise(async(resolve, reject)=>{ let range = options.range || 2; try{ await this._goTo(options.where, range) return resolve(); }catch(error){ if(options.reTry) return reject('Action can not move to where') await this._goTo(options, 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); while(!this.bot.currentWindow){ let window = this.bot.openContainer(block); await sleep(1500); if(this.bot.currentWindow?.title){ break; } this.bot.removeAllListeners('windowOpen'); if(count++ == 3) throw 'Block wont open'; } return this.bot.currentWindow; } 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};