A high-level, general purpose and modular minecraft bot using hot re-loadable (without restarting the bot!) plugins. Batteries included, launch to run!
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

143 lines
5.3 KiB

const env = require("dotenv-packed").parseEnv().parsed
const fs = require('fs');
const cfg = {
admin: process.env.MINECRAFT_PLAYER_ADMIN || env.MINECRAFT_PLAYER_ADMIN || console.warn("main: bot admin user not provided"),
mods: process.env.MINECRAFT_PLAYER_MODS || env.MINECRAFT_PLAYER_MODS || [], // json array,
stateMachines: {}
}
const mineflayer = require("mineflayer");
// const { createGetAccessor } = require('typescript');
const options = !isNaN(parseInt(process.argv[3])) && parseInt(process.argv[3]) > 1e2 ?
{
host: process.argv[2] || process.env.MINECRAFT_HOST || env.MINECRAFT_HOST || 'localhost', // Change this to the ip you want.
port: parseInt(process.argv[3]) || process.env.MINECRAFT_PORT || env.MINECRAFT_PORT // || 58471,
}
:
{
host: process.argv[2] || process.env.MINECRAFT_HOST || env.MINECRAFT_HOST || 'localhost', // Change this to the ip you want.
username: process.argv[3] || process.env.MINECRAFT_USER || env.MINECRAFT_USER,
password: process.argv[4] || process.env.MINECRAFT_PASS || env.MINECRAFT_PASS,
// port: process.argv[5] || process.env.MINECRAFT_PORT || 58471,
}
const bot = mineflayer.createBot(options)
cfg.botOptions = options
let plugins = {}
function loadplugin(pluginname, pluginpath = './plugins/' + pluginname) {
try {
plugins[pluginname] = require(pluginpath)
plugins[pluginname].load(cfg)
} catch (error) {
if (error.code == 'MODULE_NOT_FOUND') {
console.warn('plugin not used:', pluginpath)
} else if (plugins[pluginname] && !plugins[pluginname].load) {
unloadplugin(pluginname, pluginpath)
} else {
console.error(error)
}
}
}
function unloadplugin(pluginname, pluginpath = './plugins/' + pluginname) {
const plugin = require.resolve(pluginpath)
try {
if (plugin && require.cache[plugin]) {
// `unload()` isn't exported sometimes,
// when plugin isn't properly loaded
require.cache[plugin].exports?.unload?.()
delete plugins[pluginname]
delete require.cache[plugin]
}
} catch (error) {
console.error(error)
}
}
function reloadplugin(event, filename, pluginpath = './plugins/') {
if (!/\.js$/.test(filename)) { return }
filename = filename.replace("\\", "/") // windows
if (!cfg.fsTimeout) {
console.info(event, filename)
const fullpluginpath = pluginpath + filename
const pluginname = (filename.split(".js")[0]).replace(/\/.+/, "")
pluginpath = pluginpath + pluginname
const hassubplugin = fullpluginpath.replace(/\.js$/, "") !== pluginpath
const check = Object.keys(cfg.plugins)
console.info(`reload file: ./lib/${fullpluginpath}`)
const plugin = require.resolve(fullpluginpath)
if (plugin && require.cache[plugin]) {
// console.debug(Object.keys(cfg.plugins))
unloadplugin(pluginname)
console.assert(Object.keys(cfg.plugins).length == check.length - 1, "plugin not removed, potential memory leak")
if (hassubplugin) {
console.info("reload: also unloading sub")
unloadplugin(pluginname, fullpluginpath)
}
}
loadplugin(pluginname)
if (Object.keys(cfg.plugins).length != check.length) {
// If left < right :
// - new plugin that's not registered in cfg.plugins, so added
// - rename, so old wasn't removed
// If left > right :
// - error in file (syntax, missing load()), so was not reloaded
console.warn("plugin count mismatch", check.length, Object.keys(cfg.plugins).length, Object.keys(cfg.plugins))
}
cfg.fsTimeout = setTimeout(function () { cfg.fsTimeout = null }, 2000) // give 2 seconds for multiple events
}
// console.log('file.js %s event', event)
}
fs.watch('./lib/plugins', { recursive: true }, reloadplugin)
cfg.bot = bot
// TODO better name, or switch to array
cfg.botAddressPrefix = '!'
cfg.quiet = true
// == actually do stuff
bot.once("spawn", () => {
plugins = {
command: require('./plugins/command'),
informer: require('./plugins/informer'),
inventory: require('./plugins/inventory'),
finder: require('./plugins/finder'),
mover: require('./plugins/mover'),
sleeper: require('./plugins/sleeper'),
eater: require('./plugins/eater'),
armor: require('./plugins/armor'),
guard: require('./plugins/guard'),
// miner: require('./plugins/miner.js'),
statemachine: require('./plugins/statemachine'),
}
cfg.plugins = plugins
// cfg.botAddressPrefix = ${bot.username.substr(-2,2)}
cfg.botAddressRegex = new RegExp(`^${bot.username}:? (/|${cfg.botAddressPrefix}.+)`)
// FIXME leaks every load, so adding here instead of command.js to load only once
bot.addChatPattern("web", /\[WEB\] (\[.+\])?\s*([\w.]+): (.+)/, { parse: true })
for (const plugin of Object.values(plugins)) {
try {
plugin.load(cfg)
} catch (error) {
console.warn(error)
}
}
})
bot.on("death", () => {
bot.pathfinder && bot.pathfinder.setGoal(null)
// plugins.guard.unload()
})
// bot.on("respawn", () => {
// // setTimeout(plugins.guard.load, 2000)
// })