forked from wmantly/mc-bot-town
387 lines
9.5 KiB
JavaScript
387 lines
9.5 KiB
JavaScript
'use strict';
|
|
|
|
const mineflayer = require('mineflayer');
|
|
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.19.2';
|
|
|
|
// 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) =>{
|
|
|
|
// 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.bot.version, 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', (m)=>{
|
|
console.log(this.name, 'Connection ended:', m);
|
|
this.isReady = false;
|
|
reject(m);
|
|
});
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
|
|
// 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{
|
|
|
|
// 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 - 1500) + 1500);
|
|
}
|
|
|
|
async say(...messages){
|
|
for(let message of messages){
|
|
if(this.nextChatTime > Date.now()){
|
|
await sleep(this.nextChatTime-Date.now()+1)
|
|
}
|
|
|
|
this.bot.chat(message);
|
|
this.nextChatTime = Date.now() + this.__chatCoolDown();
|
|
}
|
|
}
|
|
|
|
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.`);
|
|
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 __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 putInChest(block, blockName, amount) {
|
|
block = this.__blockOrVec(block);
|
|
|
|
this.bot.openContainer(block);
|
|
let window = await this.once('windowOpen');
|
|
|
|
for(let item of window.slots.slice(window.inventoryStart).filter(function(item){
|
|
if(!item) return false;
|
|
if(blockName && blockName !== item.name) return false;
|
|
return true;
|
|
})){
|
|
let currentSlot = Number(item.slot);
|
|
if(!window.slots[currentSlot]) continue;
|
|
|
|
if(amount && !amount--) return;
|
|
let chestSlot = await this.__nextContainerSlot(window, item);
|
|
console.log('next slot', chestSlot);
|
|
await this.bot.moveSlotItem(currentSlot, chestSlot)
|
|
|
|
let res = await this.putInChest(...arguments);
|
|
if(res === false) return amount ? amount : false;
|
|
}
|
|
|
|
await this.bot.closeWindow(window);
|
|
|
|
return amount ? amount : true;
|
|
}
|
|
|
|
|
|
}
|
|
|
|
module.exports = {CJbot};
|