commit 4f1e510386762e076cdd1401e2b92dd2f4e7853b Author: jay Date: Mon Dec 21 21:08:38 2020 +0500 feat: :tada: init new repo Dump of current working bot. Warning: somewhat messy code! Lints haven't been run, no tests, etc. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3a75822 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local +/lib/**/*.old +/lib/**/*.bak + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Editor +*.swp +*.swo \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..f031871 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "protospace", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/lib/index.js", + "args": ["games.protospace.ca"] + }, + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/lib/index.js", + // port may need to be changed for each session + "args": ["localhost", "56901"] + } + ] +} \ No newline at end of file diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..0ebc880 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,123 @@ +// TODO reload +const fs = require('fs'); +let cfg = { + admin: "Applezaus", + mods: ["Applezaus", "tanner6", "Angram42", "[WEB] Angram42", "[WEB] Applezaus"], + stateMachines: {} +} + +const mineflayer = require("mineflayer"); +// const { createGetAccessor } = require('typescript'); + +const bot = + !isNaN(parseInt(process.argv[3])) && parseInt(process.argv[3]) > 1e2 ? + mineflayer.createBot({ + host: process.argv[2] || process.env.MINECRAFT_HOST || 'localhost', // Change this to the ip you want. + port: parseInt(process.argv[3]) || process.env.MINECRAFT_PORT // || 58471, + }) + : + mineflayer.createBot({ + host: process.argv[2] || process.env.MINECRAFT_HOST || 'localhost', // Change this to the ip you want. + username: process.argv[3] || process.env.MINECRAFT_USER, + password: process.argv[4] || process.env.MINECRAFT_PASS, + // port: process.argv[5] || process.env.MINECRAFT_PORT || 58471, + }) + + +let plugins = {} + +function loadplugin(pluginname, pluginpath) { + try { + plugins[pluginname] = require(pluginpath) + plugins[pluginname]?.load(cfg) + } catch (error) { + if (error.code == 'MODULE_NOT_FOUND') { + console.warn('plugin not used:', pluginpath) + } else { + console.error(error) + } + } +} + +function unloadplugin(pluginname, pluginpath) { + pluginpath = pluginpath ? pluginpath : './plugins/' + pluginname + const plugin = require.resolve(pluginpath) + try { + if (plugin && require.cache[plugin]) { + require.cache[plugin].exports?.unload() + delete plugins[pluginname] + delete require.cache[plugin] + } + } catch (error) { + console.error(error) + } +} + +reloadplugin = (event, filename, pluginpath) => { + if (!/\.js$/.test(filename)) { return } + if (!cfg.fsTimeout) { + console.info(event, filename) + pluginpath = (pluginpath ? pluginpath : './plugins/') + filename + const check = Object.keys(cfg.plugins) + console.info(`reload file:`, pluginpath) + const plugin = require.resolve(pluginpath) + if (plugin && require.cache[plugin]) { + // console.debug(Object.keys(cfg.plugins)) + unloadplugin(filename.split(".js")[0], pluginpath) + console.assert(Object.keys(cfg.plugins).length == check.length - 1, "plugin not removed, potential memory leak") + } + loadplugin(filename.split(".js")[0], pluginpath) + 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', reloadplugin) + +cfg.bot = bot +cfg.botAddress = new RegExp(`^${bot.username} (!.+)`) +cfg.quiet = true + + +// == actually do stuff + +bot.once("spawn", () => { + plugins = { + command: require('./plugins/command'), + sleeper: require('./plugins/sleeper'), + armor: require('./plugins/armor'), + // mover: require('./plugins/mover'), + guard: require('./plugins/guard'), + inventory: require('./plugins/inventory'), + // eater: require('./plugins/eater'), + finder: require('./plugins/finder'), + miner: require('./plugins/miner'), + // statemachine: require('./plugins/statemachine'), + } + + cfg.plugins = plugins + + 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) +// }) \ No newline at end of file diff --git a/lib/plugins/armor.js b/lib/plugins/armor.js new file mode 100644 index 0000000..c55fda7 --- /dev/null +++ b/lib/plugins/armor.js @@ -0,0 +1,16 @@ +const armorManager = require('mineflayer-armor-manager') +const mineflayer = require('mineflayer'); + +let cfg = {} + + +const load = (config) => { + cfg = config + bot = cfg.bot + + bot.loadPlugin(armorManager); +} + +const unload = () => { console.warn("armour: may not properly unload") } + +module.exports = { load, unload } \ No newline at end of file diff --git a/lib/plugins/command.js b/lib/plugins/command.js new file mode 100644 index 0000000..32cc364 --- /dev/null +++ b/lib/plugins/command.js @@ -0,0 +1,323 @@ +const v = require('vec3') +let mcData = require('minecraft-data') + +let cfg = {} +let bot = {} +// bot.chatAddPattern(new RegExp(`^hi|hello ${bot.username}`, "i"), 'greet', "General greeting") + +function todo() { + bot.chat("not implemented yet") +} + +function checkBlockExists(name) { + if (mcData.blocksByName[name] === undefined) { + bot.chat(`${name} is not a block name`) + return false + } else { + return true + } +} +function checkItemExists(name) { + if (mcData.itemsByName[name] === undefined) { + bot.chat(`${name} is not an item name`) + return false + } else { + return true + } +} + +const events = { + whisper: function command_whisper(username, message) { + if ([bot.username, "me"].includes(username)) return + if (/^gossip/.test(message)) return + // if (/^!/.test(message) && username === cfg.admin){ + if (username === cfg.admin) { + message = message.replace("\\", "@") + console.info("whispered command", message) + bot.chat("/" + message) + } else { + bot.whisper(cfg.admin, `gossip ${username}: ${message}`) + console.info(username, "whispered", message) + } + } + , chat: command + , kicked: (reason, loggedIn) => console.warn(reason, loggedIn) +} + +const events_registered = [] + +function command(username, message) { + + if (username === bot.username && !message.startsWith("!")) return + + const player = bot.players[username] ? bot.players[username].entity : null + + if (message.startsWith("zzz")) { + bot.chat("/afk") + cfg.plugins.sleeper.sleep() + // if(cfg) + return + } + + switch (message) { + case "hi": + case "hello": + case "howdy": + case "yo": + bot.swingArm() + return + case "69": + Math.random() > 0.5 && bot.chat("nice!") + return + case "awesome": + case "cool": + case "superb": + case "nice": + Math.random() > 0.1 && setTimeout(() => bot.chat("very nice!"), 1000) + return + default: + break; + } + + if (message.startsWith("!") || cfg.botAddress.test(message)) { + message = cfg.botAddress.test(message) ? cfg.botAddress.exec(message)[1] : message + + console.log(message) + message = message.slice(1) + message_parts = message.split(/\s+/) + switch (message_parts[0]) { + // case "follow": + // // if(username === cfg.admin) + // cfg.stateMachines.follow.transitions[4].trigger() + // // cfg.stateMachines.follow.transitions[1].trigger() + // break; + // case "stay": + // // if(username === cfg.admin) + // cfg.stateMachines.follow.transitions[3].trigger() + // break; + case "echo": + // bot.chat(message_parts[1]) + // bot.chat(message.slice(1)) + bot.chat(message) + break; + case "autosleep": + case "sleep": + message_parts[1] = message_parts[0] == "autosleep" ? "auto" : message_parts[1] + switch (message_parts[1]) { + case "auto": + cfg.sleep.auto = !cfg.sleep.auto + bot.chat(`ok, ${cfg.sleep.auto ? "" : "not "}auto sleeping `) + bot.chat("/afk") + break; + case "silent": + case "quiet": + cfg.sleep.quiet = !cfg.sleep.quiet + bot.chat(`ok, ${cfg.sleep.quiet ? "" : "not "}sleeping quietly`) + break; + case "timeout": + if (!message_parts[2]) { + bot.chat(`sleeping every ${Math.round(cfg.sleep.timeout / 60 / 1000)}mins`) + return + } + const timeout = parseFloat(message_parts[2], 10) + if (timeout) { + cfg.sleep.timeout = timeout * 60 * 1000 + cfg.sleep.timeoutFn = null + cfg.plugins.sleeper.sleep(true) //reset timer + bot.chat(`ok, next sleep attempt in ${timeout}mins`) + break; + } + default: + bot.chat(`usage: !sleep [auto | quiet | timeout ], or zzz for manual sleep`) + break; + } + break; //todo; needed? + case "zzz": + cfg.plugins.sleeper.sleep() + case "afk": + bot.chat("/afk") + break; + case "awaken": + case "wake": + case "wakeup": + cfg.plugins.sleeper.wake() + break; + case "attack": + case "rage": + case "ragemode": + case "guard": + switch (message_parts.length) { + case 1: + if (!player) { + bot.chat("can't see you.") + return + } + bot.chat('for glory!') + cfg.plugins.guard.guardArea(player.position) + break + case 2: + switch (message_parts[1]) { + case "self": + cfg.guard.self = !cfg.guard.self + bot.chat(`ok, ${cfg.guard.self?"no longer being altruistic":"self sacrifice!"}`) + return; + case "stop": + cfg.plugins.guard.stopGuarding() + bot.chat('no longer guarding this area.') + return; + + default: + break; + } + default: + bot.chat("don't know wym") + } + // entity = new BehaviorGetClosestEntity(bot, bot.nearestEntity(), a => EntityFilters().MobsOnly(a)) + // if (entity) { + // bot.attack(entity, true) + // } else { + // bot.chat('no nearby entities') + // } + // // bot.attack(new BehaviorGetClosestEntity(bot, {}, EntityFilters().MobsOnly())) + break; + case "find": + switch (message_parts.length) { + case 2: + cfg.plugins.finder.findBlocks(message_parts[1]) + break + + default: + break; + } + break + case "lookat": + // const coords = v(message_parts.splice(1)) + switch (message_parts.length) { + case 2: + case 3: + todo() + // bot.lookAt({}) + break + case 1: + bot.lookAt(bot.nearestEntity().position) + break + case 4: + bot.lookAt(v(message_parts.splice(1))) + break + default: + break + } + break + case "ride": + case "mount": + bot.mount(bot.nearestEntity()) + break + case "getoff": + case "unmount": + case "dismount": + bot.dismount() + break + case "go": + bot.moveVehicle(0, 10) + break + // case "use": + // bot.useOn(bot.nearestEntity()) + // break; + + // // TODO move all inventory related tasks into own module + // case "give": + // // switch (message_parts[1]) { + // // case "hand": + // // case "right": + // // break; + // // case "left": + // // break; + // // // case "slot": + // // default: + // // let slot = parseInt(message_parts[1]) + // // if (!isNaN(slot)) { + + // // } else { + + // // } + // // break; + // // } + // // break; + // case "take": + // // TODO take only what's requested, then throw all the rest + // // TODO take all + // case "toss": + // case "drop": + // if (!message_parts[1]) { return false } // FIXME, works but ugly + // if (!checkItemExists(message_parts[1])) { return false } + // switch (message_parts.length) { + // case 2: + // bot.toss(mcData.blocksByName[message_parts[1]].id) + // break + // case 3: + // bot.tossStack( + // mcData.itemsByName[message_parts[1]].id, + // (err) => { + // if (err) { + // console.log(err) + // bot.chat(err) + // } + // } + // ) + // break + // default: + // break + // } + // break; + case "location": + switch (message_parts[1]) { + case "add": + case "record": + + break; + + default: + break; + } + case "where": + case "where?": + // TODO put in /lib/location + console.log(bot.entity.position) + bot.chat(bot.entity.position.floored().toString()) + break; + case "warp": + // if(message_parts[1] == "spawn") + bot.chat("/" + message) + break; + + default: + if (cfg.mods.includes(username)) + bot.chat("/" + message) + break; + } + } +} + +const load = (config) => { + cfg = config + bot = cfg.bot + + mcData = mcData(bot.version) + for (const [key, fn] of Object.entries(events)) { + events_registered.push( + bot.on(key, fn) + ) + } + mcData = require('minecraft-data')(bot.version) +} + +const unload = () => { + console.log("events_registered:", events_registered.length, "of", bot._eventsCount) + for (const [key, fn] of Object.entries(events)) { + bot.off(key, fn) + events_registered.shift() + } + console.log("events_registered:", bot._eventsCount) +} + +module.exports = { load, unload, events_registered, command } \ No newline at end of file diff --git a/lib/plugins/finder.js b/lib/plugins/finder.js new file mode 100644 index 0000000..bb5e4d3 --- /dev/null +++ b/lib/plugins/finder.js @@ -0,0 +1,53 @@ +// const performance = require('performance-now') +const {Vec3} = require('vec3') +let mcData = require('minecraft-data') + +let cfg = {} +let bot = {} + +function findBlocks(name){ + // const name = message.split(' ')[1] + if (mcData.blocksByName[name] === undefined) { + bot.chat(`${name} is not a block name`) + return + } else if (["ancient_debris", "diamond_ore", "emerald_ore"].includes(name)) { + return + } + + const ids = [mcData.blocksByName[name].id] + + // const startTime = performance.now() + const blocks = bot.findBlocks({ matching: ids, maxDistance: 128, count: 10 }) + // const time = (performance.now() - startTime).toFixed(2) + + // TODO check if toString() is really needed + if (blocks.length === 0){ + bot.chat("not here (around 128 blocks)") + return + } + const pos = minmax(blocks).map(x => x.toString()) + + + // bot.chat(`I found ${blocks.length} ${name} blocks in ${time} ms`) + bot.chat(`I found ${blocks.length} ${name} blocks at ${pos}`) +} + +function minmax(coordsArray){ + const min = coordsArray.reduce((x,y) => x.min(y)) + const max = coordsArray.reduce((x,y) => x.max(y)) + + return [min, max] +} + +const load = (config) => { + cfg = config + bot = cfg.bot + + mcData = mcData(bot.version) +} + +const unload = () => { + // bot.off("time", autoSleep) +} + +module.exports = { load, unload, findBlocks} \ No newline at end of file diff --git a/lib/plugins/guard.js b/lib/plugins/guard.js new file mode 100644 index 0000000..c0b8576 --- /dev/null +++ b/lib/plugins/guard.js @@ -0,0 +1,147 @@ +const mineflayer = require('mineflayer') +const { pathfinder, Movements, goals } = require('mineflayer-pathfinder') +const pvp = require('mineflayer-pvp').plugin + +let cfg = {} +let bot = {} +let guardPos + +const filterallMobs = e => e.type === 'mob' +// const filterPlayers = e => e.type === 'player' && e.username !== 'Applezaus' +// const filterPassiveMobs = e => e.kind === 'Passive mobs' +const filterCreeper = e => e.mobType === 'Creeper' +const filterHostileMobs = e => e.kind === 'Hostile mobs' +// // not needed if detecting by kind +// && e.mobType !== 'Armor Stand' // Mojang classifies armor stands as mobs for some reason? + +// Assign the given location to be guarded +function guardArea(pos) { + if (pos) { + cfg.guard.pos = guardPos = pos + // Check for new enemies to attack + // bot.off('physicTick', lookForMobs) + bot.off('time', lookForMobs) + bot.on('time', lookForMobs) + } + + // We are not currently in combat, move to the guard pos + if (!bot.pvp.target && guardPos) { + moveToGuardPos() + } +} + +// Cancel all pathfinder and combat +function stopGuarding() { + guardPos = cfg.guard.pos = null + bot.pvp.stop() + bot.pathfinder.setGoal(null) + bot.off('time', lookForMobs) + // bot.off('physicTick', lookForMobs) +} + +// Pathfinder to the guard position +function moveToGuardPos() { + const mcData = require('minecraft-data')(bot.version) + bot.pathfinder.setMovements(new Movements(bot, mcData)) + bot.pathfinder.setGoal(new goals.GoalBlock(guardPos.x, guardPos.y, guardPos.z)) +} + +// Called when the bot has killed it's target. +// () => { +// if (guardPos) { +// moveToGuardPos() +// } +// }) + +function lookForMobs() { + if (!guardPos) return // Do nothing if bot is not guarding anything + + // Only look for mobs within 16 blocks + const filter = e => e.position.distanceTo(bot.entity.position) < 16 && filterHostileMobs(e) + + const entityEnemy = bot.nearestEntity(filter) + if (entityEnemy) { + if(filterCreeper(entityEnemy)) + // Start attacking + // bot.off('time', lookForMobs) + // bot.on('physicTick', lookForMobs) + bot.pvp.attack(entityEnemy) + } +} + +function guardSelf(entity) { + if (!cfg.guard.self || entity !== bot.entity || guardPos) return // Do nothing + // bot.chat("") + bot.chat(( + () => { const a = ["ouch!", "oww!", "help", "", "", ""]; return a[Math.floor(Math.random() * a.length)] } + )()) + + console.info(bot.nearestEntity(filterallMobs)) + + // Only look for mobs within 10 blocks + const filter = e => e.position.distanceTo(bot.entity.position) < 10 && filterHostileMobs(e) + + const entityEnemy = bot.nearestEntity(filter) + if (entityEnemy && !filterCreeper(entityEnemy)) { + // Start attacking + // bot.off('time', lookForMobs) + // bot.on('physicTick', lookForMobs) + bot.pvp.attack(entityEnemy) + } +} + +// bot.on('physicTick', lookForMobs) + +// Listen for player commands +// bot.on('chat', (username, message) => { +// // Guard the location the player is standing +// if (message === 'guard') { +// const player = bot.players[username] + +// if (!player) { +// bot.chat("I can't see you.") +// return +// } + +// bot.chat('I will guard that location.') +// guardArea(player.entity.position) +// } + +// // Stop guarding +// if (message === 'stop') { +// bot.chat('I will no longer guard this area.') +// stopGuarding() +// } +// }) + +const load = (config) => { + cfg = config + bot = cfg.bot + cfg.guard = { + pos: null, + auto: true, + self: true, + } + + guardPos = cfg.guard.pos + + + bot.loadPlugin(pathfinder) + bot.loadPlugin(pvp) + + bot.on('stoppedAttacking', guardArea) + bot.on('entityHurt', guardSelf) + // bot.on("time", guardArea) + +} + +const unload = () => { + stopGuarding() + bot.off('time', lookForMobs) + // bot.off('physicTick', lookForMobs) + bot.off('stoppedAttacking', guardArea) + bot.off('entityHurt', guardSelf) + // bot.off("time", guardArea) +} + +module.exports = { load, unload, guardArea, guardSelf, stopGuarding } \ No newline at end of file diff --git a/lib/plugins/inventory.js b/lib/plugins/inventory.js new file mode 100644 index 0000000..da97f37 --- /dev/null +++ b/lib/plugins/inventory.js @@ -0,0 +1,190 @@ +/* + * Using the inventory is one of the first things you learn in Minecraft, + * now it's time to teach your bot the same skill. + * + * Command your bot with chat messages and make him toss, equip, use items + * and even craft new items using the built-in recipe book. + * + * To learn more about the recipe system and how crafting works + * remember to read the API documentation! + */ +const mineflayer = require('mineflayer') + +// if (process.argv.length < 4 || process.argv.length > 6) { +// console.log('Usage : node inventory.js [] []') +// process.exit(1) +// } + +// const bot = mineflayer.createBot({ +// host: process.argv[2], +// port: parseInt(process.argv[3]), +// username: process.argv[4] ? process.argv[4] : 'inventory', +// password: process.argv[5] +// }) + +let cfg = {} +let bot = {} +// let mcd + +function inventory(username, message) { + if (username === bot.username) return + const command = message.split(' ') + switch (true) { + // case message === 'loaded': + // bot.waitForChunksToLoad(() => { + // bot.chat('Ready!') + // }) + // break + case /^list$/.test(message): + sayItems() + break + case /^toss \d+ \w+$/.test(message): + // toss amount name + // ex: toss 64 diamond + tossItem(command[2], command[1]) + break + case /^toss \w+$/.test(message): + // toss name + // ex: toss diamond + tossItem(command[1]) + break + case /^equip \w+ \w+$/.test(message): + // equip destination name + // ex: equip hand diamond + equipItem(command[2], command[1], quiet = cfg.quiet) + break + case /^unequip \w+$/.test(message): + // unequip testination + // ex: unequip hand + unequipItem(command[1]) + break + case /^use$/.test(message): + useEquippedItem() + break + case /^craft \d+ \w+$/.test(message): + // craft amount item + // ex: craft 64 stick + craftItem(command[2], command[1]) + break + } +} + + +function sayItems(items = bot.inventory.items()) { + const output = items.map(itemToString).join(', ') + if (output) { + console.info("inventory:", output) + !cfg.quiet && bot.chat(output) + } else { + !cfg.quiet && bot.chat('empty') + } +} + +function tossItem(name, amount) { + amount = parseInt(amount, 10) + const item = itemByName(name) + if (!item) { + bot.chat(`I have no ${name}`) + } else if (amount) { + bot.toss(item.type, null, amount, checkIfTossed) + } else { + bot.tossStack(item, checkIfTossed) + } + + function checkIfTossed(err) { + if (err) { + bot.chat(`unable to toss: ${err.message}`) + } else if (amount) { + bot.chat(`tossed ${amount} x ${name}`) + } else { + bot.chat(`tossed ${name}`) + } + } +} + +function equipItem(name, destination, quiet = false) { + const item = itemByName(name) + if (item) { + bot.equip(item, destination, checkIfEquipped) + } else { + !quiet && bot.chat(`I have no ${name}`) + } + + function checkIfEquipped(err) { + if (err) { + bot.chat(`cannot equip ${name}: ${err.message}`) + } else { + !quiet && bot.chat(`equipped ${name}`) + } + } +} + +function unequipItem(destination) { + bot.unequip(destination, (err) => { + if (err) { + bot.chat(`cannot unequip: ${err.message}`) + } else { + bot.chat('unequipped') + } + }) +} + +function useEquippedItem() { + bot.chat('activating item') + bot.activateItem() +} + +function craftItem(name, amount) { + amount = parseInt(amount, 10) + const item = require('minecraft-data')(bot.version).findItemOrBlockByName(name) + const craftingTable = bot.findBlock({ + matching: 58 + }) + + if (item) { + const recipe = bot.recipesFor(item.id, null, 1, craftingTable)[0] + if (recipe) { + bot.chat(`I can make ${name}`) + bot.craft(recipe, amount, craftingTable, (err) => { + if (err) { + bot.chat(`error making ${name}`) + } else { + bot.chat(`did the recipe for ${name} ${amount} times`) + } + }) + } else { + bot.chat(`I cannot make ${name}`) + } + } else { + bot.chat(`unknown item: ${name}`) + } +} + +function itemToString(item) { + if (item) { + return `${item.name} x ${item.count}` + } else { + return '(nothing)' + } +} + +function itemByName(name) { + return bot.inventory.items().filter(item => item.name === name)[0] +} + + +const load = (config) => { + cfg = config + bot = cfg.bot + // cfg.inventory = { + // auto: true, + // quiet: false + // } + bot.on('chat', inventory) +} + +const unload = () => { + bot.off('chat', inventory) +} + +module.exports = { load, unload, equipItem } \ No newline at end of file diff --git a/lib/plugins/miner.js b/lib/plugins/miner.js new file mode 100644 index 0000000..b57409e --- /dev/null +++ b/lib/plugins/miner.js @@ -0,0 +1,190 @@ +const mineflayer = require('mineflayer') +const pathfinder = require('mineflayer-pathfinder').pathfinder +const { + gameplay, + MoveTo, + BreakBlock, + ObtainItems, + ObtainItem, + GiveTo, + Craft +} = require('prismarine-gameplay') +// const { Gameplay } = require('prismarine-gameplay/lib/gameplay') +// const { Vec3 } = require('vec3') +let mcData = require('minecraft-data') + +let cfg = {} +let bot = {} + +// if (process.argv.length < 4 || process.argv.length > 6) { +// console.log('Usage : node miner.js [] []') +// process.exit(1) +// } + +// const bot = mineflayer.createBot({ +// host: process.argv[2], +// port: parseInt(process.argv[3]), +// username: process.argv[4] ? process.argv[4] : 'collect_items', +// // password: process.argv[5] +// }) + + +// bot.on('spawn', () => bot.gameplay.debugText = true) + +// bot.on('chat', (username, message) => mine(username, message)) + +function checkBlockExists(name){ + if (mcData.blocksByName[name] === undefined) { + bot.chat(`${name} is not a block name`) + return false + } else { + return true + } +} + +function miner(username, message) { + const player = bot.players[username] ? bot.players[username].entity : null + + const command = message.split(' ') + switch (true) { + // case /^echo .*/.test(message): + // bot.chat(command.slice(1).join(" ")) + // break + + // case /^zz+/.test(message): + // bot.chat("/afk") + // break + case /^debug$/.test(message): + bot.gameplay.debugText = !!!bot.gameplay.debugText + break + + case /^moveto -?[0-9]+ -?[0-9]+$/.test(message): + bot.gameplay.solveFor( + new MoveTo({ + x: parseInt(command[1]), + z: parseInt(command[2]) + }), logError) + break + + case /^moveto -?[0-9]+ -?[0-9]+ -?[0-9]+$/.test(message): + bot.gameplay.solveFor( + new MoveTo({ + x: parseInt(command[1]), + y: parseInt(command[2]), + z: parseInt(command[3]) + }), logError) + break + + case /^moveto \w+$/.test(message): + const player2 = bot.players[command[1]] ? bot.players[command[1]].entity : null + if (!player2) { + bot.chat(`can't see ${command[1]}..`) + } else { + bot.gameplay.solveFor( + new MoveTo({ + x: player2.position.x, + y: player2.position.y, + z: player2.position.z + }) + ) + } + break + + case /^comehere$/.test(message): + if (!player) { + bot.chat("can't see you..") + } else { + bot.gameplay.solveFor( + new MoveTo({ + x: player.position.x, + y: player.position.y, + z: player.position.z + }), logError) + } + break + + case /^break -?[0-9]+ -?[0-9]+ -?[0-9]+$/.test(message): + bot.gameplay.solveFor( + new BreakBlock({ + position: new Vec3( + parseInt(command[1]), + parseInt(command[2]), + parseInt(command[3]) + ) + }), logError) + break + + case /^collect [0-9]+ [a-zA-Z_]+$/.test(message): + if(!checkBlockExists(command[2])) {return false} + bot.gameplay.solveFor( + new ObtainItems({ + itemType: command[2], + count: parseInt(command[1]) + }), logError) + break + + case /^collect [a-zA-Z_]+$/.test(message): + if(!checkBlockExists(command[2])) {return false} + bot.gameplay.solveFor( + new ObtainItem({ + itemType: command[1] + }), logError) + break + + case /^bringme [0-9]+ [a-zA-Z_]+$/.test(message): + if(!checkBlockExists(command[2])) {return false} + bot.gameplay.solveFor( + new GiveTo({ + itemType: command[2], + count: parseInt(command[1]), + entity: player + }), logError) + break + + case /^craft [0-9]+ [a-zA-Z_]+$/.test(message): + if(!checkBlockExists(command[2])) {return false} + bot.gameplay.solveFor( + new Craft({ + itemType: command[2], + count: parseInt(command[1]), + entity: player + }), logError) + break + + case /^stop$/.test(message): + bot.chat("♪♪ can't stop me now!! ♪♪") + // player = bot.player.entity + if (player) { + bot.gameplay.solveFor( + new MoveTo({ + x: player.position.x, + y: player.position.y, + z: player.position.z + }), logError) + } + break + } +} + +function logError(err) { + if (err) + bot.chat(`Failed task: ${err.message}`) +} + + +const load = (config) => { + cfg = config + bot = cfg.bot + + mcData = mcData(bot.version) + bot.loadPlugin(pathfinder) + bot.loadPlugin(gameplay) + bot.on("chat", miner) +} + +const unload = () => { + // bot.gameplay + bot.off("chat", miner) +} + +module.exports = { load, unload } \ No newline at end of file diff --git a/lib/plugins/sleeper.js b/lib/plugins/sleeper.js new file mode 100644 index 0000000..05efcfd --- /dev/null +++ b/lib/plugins/sleeper.js @@ -0,0 +1,115 @@ + +const pathfinder = require('mineflayer-pathfinder').pathfinder +const { Bot } = require('mineflayer') +const { + gameplay, + MoveTo, + MoveToInteract, + ObtainItem, + // Craft +} = require('prismarine-gameplay') + +let cfg = {} +let bot = {} +let inv +// cfg.autosleep = false + +function sleep(quiet) { + quiet = quiet !== undefined ? quiet : cfg.sleep.quiet + if(bot.game.dimension !== "minecraft:overworld" || cfg.sleep.force){ + !quiet && bot.chat("can't sleep, not in overworld now") + return + } + let bed = bot.findBlock({ + matching: block => bot.isABed(block) + }) + let bedstatus = bed && bot.parseBedMetadata(bed).occupied ? "n unoccupied" : "" + if(bed && bedstatus == "n unoccupied"){ + bot.lookAt(bed.position) + bed = bot.findBlock({ + matching: block => bot.isABed(block) && !bot.parseBedMetadata(block).occupied + }) || bed + bedstatus = bot.parseBedMetadata(bed).occupied ? "n unoccupied" : "" + } + if (bed && bedstatus == "") { + bot.lookAt(bed.position) + // const nearbed = + bot.gameplay.solveFor( + new MoveTo((bed.position.range = 2) && bed.position), (err) => { + // new MoveTo(bed.position), (err) => { + // new MoveToInteract(bed.position), (err) => { + if (err) { + !quiet && bot.chat(`can't reach bed: ${err.message}`) + } else { + bot.waitForChunksToLoad(() => { + bot.sleep(bed, (err) => { + if (err) { + !quiet && bot.chat(`can't sleep: ${err.message}`) + } else { + !quiet && bot.chat("zzz") + console.log("sleeping? ", bot.isSleeping) + // hack until this is fixed + // bot.isSleeping = bot.isSleeping ? bot.isSleeping : true + bot.isSleeping = true + } + }) + }) + } + }) + // } else if (bed){ + } else if (inv && inv.equipItem("red_bed", "hand", true)) { + // doesn't work fortunately + // FIXME: DONT IMPLEMENT until it is detected as NOT NETHER + bot.placeBlock() + } else { + bot.gameplay.solveFor( + new ObtainItem("bed"), (err) => { + if (err) { + !quiet && bot.chat(`need a${bedstatus} bed: may not see if just placed`) + } + } + ) + // bot.chat('/afk') + } +} + +function wake() { + bot.wake((err) => { + if (err) { + bot.chat(`can't wake up: ${err.message}`) + } else { + bot.chat('woke up') + } + }) +} + +function autoSleep() { + if (!bot.time.isDay && !cfg.sleep.timeoutFn && cfg.sleep.auto && !bot.isSleeping) { + sleep() + cfg.sleep.timeoutFn = setTimeout(() => { cfg.sleep.timeoutFn = null }, cfg.sleep.timeout) // give 2 seconds for multiple events + console.log("sleeping?", bot.isSleeping, bot.time) + } +} + +const load = (config) => { + cfg = config + bot = cfg.bot + cfg.sleep = { + auto: true, + // timeout: 30 * 1000, + timeout: 2 * 60 * 1000, + quiet: false + } + + bot.loadPlugin(pathfinder) + bot.loadPlugin(gameplay) + inv = cfg.plugins["inventory"] + bot.on("time", autoSleep) + +} + +const unload = () => { + bot.off("time", autoSleep) +} + +module.exports = { load, unload, sleep, wake } \ No newline at end of file diff --git a/lib/plugins/statemachine.js b/lib/plugins/statemachine.js new file mode 100644 index 0000000..073f98c --- /dev/null +++ b/lib/plugins/statemachine.js @@ -0,0 +1,115 @@ +// Load your dependency plugins. + +const {pathfinder} = require('mineflayer-pathfinder') +// bot.loadPlugin(require('prismarine-viewer').mineflayer) +// const mineflayerViewer = require('prismarine-viewer').mineflayer + +// Import required behaviors. +const { + StateTransition, + BotStateMachine, + EntityFilters, + BehaviorFollowEntity, + BehaviorLookAtEntity, + BehaviorGetClosestEntity, + NestedStateMachine, + BehaviorIdle, + StateMachineWebserver, +} = require("mineflayer-statemachine"); + +// TODO chat + + + +// wait for our bot to login. +function statemachineInit() { + cfg.botAddress = new RegExp(`^${bot.username} (!.+)`) + // This targets object is used to pass data between different states. It can be left empty. + const targets = new Object(); + + // Create our states + const getClosestPlayer = new BehaviorGetClosestEntity( + bot, targets, entity => EntityFilters().PlayersOnly(entity) && bot.entity.position.distanceTo(entity.position) <= 50 ); // && a.username !== bot.username); + const followPlayer = new BehaviorFollowEntity(bot, targets); + const lookAtPlayer = new BehaviorLookAtEntity(bot, targets); + const stay = new BehaviorIdle(); + + // Create our transitions + const transitions = [ + + // We want to start following the player immediately after finding them. + // Since getClosestPlayer finishes instantly, shouldTransition() should always return true. + new StateTransition({ + parent: getClosestPlayer, + child: followPlayer, + onTransition: (quiet) => quiet || bot.chat(`Hi ${targets.entity.username}!`), + shouldTransition: () => bot.entity.position.distanceTo(targets.entity.position) <= 50, + // shouldTransition: () => getClosestPlayer.distanceToTarget() < 100 || console.info("player too far!") && false, + }), + + // If the distance to the player is less than two blocks, switch from the followPlayer + // state to the lookAtPlayer state. + new StateTransition({ + parent: followPlayer, + child: lookAtPlayer, + // onTransition: () => console.log(targets), + shouldTransition: () => followPlayer.distanceToTarget() < 2, + }), + + // If the distance to the player is more than two blocks, switch from the lookAtPlayer + // state to the followPlayer state. + new StateTransition({ + parent: lookAtPlayer, + child: followPlayer, + shouldTransition: () => lookAtPlayer.distanceToTarget() >= 5, + }), + new StateTransition({ + parent: lookAtPlayer, + child: stay, + onTransition: () => bot.chat("ok, staying"), + // shouldTransition: () => true, + }), + new StateTransition({ + parent: stay, + child: getClosestPlayer, + // shouldTransition: () => Math.random() > 0.01, + // shouldTransition: () => Math.random() > 0.1 && getClosestPlayer.distanceToTarget() < 2, + }), + ]; + + // Now we just wrap our transition list in a nested state machine layer. We want the bot + // to start on the getClosestPlayer state, so we'll specify that here. + const rootLayer = new NestedStateMachine(transitions, getClosestPlayer, stay); + + // We can start our state machine simply by creating a new instance. + cfg.stateMachines.follow = new BotStateMachine(bot, rootLayer); + const webserver = new StateMachineWebserver(bot, cfg.stateMachines.follow); + webserver.startServer(); + + // mineflayerViewer(bot, { port: 3000 }) + // const path = [bot.entity.position.clone()] + // bot.on('move', () => { + // if (path[path.length - 1].distanceTo(bot.entity.position) > 1) { + // path.push(bot.entity.position.clone()) + // bot.viewer.drawLine('path', path) + // } + // }) +} + +const load = (config) => { + cfg = config + bot = cfg.bot + // cfg.inventory = { + // auto: true, + // quiet: false + // } + // bot.on('chat', inventory) + bot.loadPlugin(pathfinder) + // statemachineInit() +} + +const unload = () => { + // bot.off('chat', inventory) +} + +module.exports = { load, unload } \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..f8cd885 --- /dev/null +++ b/package.json @@ -0,0 +1,56 @@ +{ + "name": "angram-bot", + "version": "0.1.0", + "description": "A high-level, general purpose and modular minecraft bot using hot re-loadable (without restarting the bot!) plugins. Batteries included, launch to run!", + "main": "index.js", + "scripts": { + "test": "jest --verbose", + "pretest": "pnpm run lint && require-self", + "prepare": "pnpm install require-self && require-self", + "lint": "standard", + "fix": "standard --fix", + "dev": "node ./lib/index.js localhost", + "prod": "node ./lib/index.js games.protospace.ca" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/PrismarineJS/prismarine-template.git" + }, + "keywords": [ + "prismarine", + "template", + "minecraft", + "mineflayer", + "ai" + ], + "author": "Jay", + "license": "MIT", + "bugs": { + "url": "https://github.com/PrismarineJS/prismarine-template/issues" + }, + "homepage": "https://github.com/PrismarineJS/prismarine-template#readme", + "devDependencies": { + "jest": "^26.6.3", + "require-self": "^0.2.3" + }, + "dependencies": { + "minecraft-data": "^2.70.2", + "mineflayer": "^2.34.0", + "mineflayer-armor-manager": "^1.3", + "mineflayer-pathfinder": "^1.1", + "mineflayer-pvp": "^1", + "prismarine-block": "^1", + "prismarine-chat": "^1", + "prismarine-entity": "^1.1.0", + "prismarine-gameplay": "github:TheDudeFromCI/prismarine-gameplay#crafting", + "prismarine-item": "^1.5.0", + "prismarine-nbt": "^1.3", + "prismarine-recipe": "^1", + "typescript": "^4", + "vec3": "^0.1", + "dotenv-packed": "^1.2" + }, + "files": [ + "lib/**/*" + ] +}