Compare commits
	
		
			21 Commits
		
	
	
		
			cover
			...
			1b21fcb096
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					1b21fcb096 | ||
| 
						 | 
					4b3e58ceb3 | ||
| 
						 | 
					8e854a0a2f | ||
| 
						 | 
					c42a3a2304 | ||
| 
						 | 
					b453b7d6bd | ||
| 
						 | 
					6b79f1fc60 | ||
| 
						 | 
					6e1ef5aada | ||
| 
						 | 
					e2ae7e5ad2 | ||
| 
						 | 
					42138a421b | ||
| 
						 | 
					aded1e4193 | ||
| 
						 | 
					60394e38eb | ||
| 
						 | 
					22490f7ec1 | ||
| 
						 | 
					72c4622091 | ||
| 
						 | 
					0757776d8b | ||
| 
						 | 
					a0ffaf1654 | ||
| 
						 | 
					1597acca72 | ||
| 
						 | 
					1e82045221 | ||
| 
						 | 
					33c4fc0c77 | ||
| 
						 | 
					33c4233223 | ||
| 
						 | 
					3f3ebbae10 | ||
| 
						 | 
					fd0e1e1347 | 
							
								
								
									
										16
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
# See this article for reference: https://help.github.com/articles/dealing-with-line-endings/
 | 
			
		||||
# Refreshing repo after line ending change:
 | 
			
		||||
# https://help.github.com/articles/dealing-with-line-endings/#refreshing-a-repository-after-changing-line-endings
 | 
			
		||||
 | 
			
		||||
# Handle line endings automatically for files detected as text
 | 
			
		||||
# and leave all files detected as binary untouched.
 | 
			
		||||
* text=auto
 | 
			
		||||
 | 
			
		||||
#
 | 
			
		||||
# The above will handle all files NOT found below
 | 
			
		||||
#
 | 
			
		||||
# These files are text and should be normalized (Convert crlf => lf)
 | 
			
		||||
# Use lf as eol for these files
 | 
			
		||||
*.js    text eol=lf
 | 
			
		||||
*.ts    text eol=lf
 | 
			
		||||
*.json  text eol=lf
 | 
			
		||||
							
								
								
									
										59
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										59
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -1,30 +1,31 @@
 | 
			
		||||
# 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
 | 
			
		||||
.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
 | 
			
		||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
/.pnp
 | 
			
		||||
.pnp.js
 | 
			
		||||
 | 
			
		||||
# testing
 | 
			
		||||
/coverage
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
/data
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
.env
 | 
			
		||||
.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
 | 
			
		||||
							
								
								
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
								
							@@ -3,6 +3,8 @@
 | 
			
		||||
        "command",
 | 
			
		||||
        "mover",
 | 
			
		||||
        "sleeper",
 | 
			
		||||
        "informer"
 | 
			
		||||
        "informer",
 | 
			
		||||
        "statemachine",
 | 
			
		||||
        "builder"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								lib/index.js
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								lib/index.js
									
									
									
									
									
								
							@@ -86,7 +86,8 @@ reloadplugin = (event, filename, pluginpath) => {
 | 
			
		||||
fs.watch('./lib/plugins', reloadplugin)
 | 
			
		||||
 | 
			
		||||
cfg.bot = bot
 | 
			
		||||
cfg.botAddress = new RegExp(`^${bot.username} (!.+)`)
 | 
			
		||||
// TODO better name, or switch to array
 | 
			
		||||
cfg.botAddressPrefix = '!'
 | 
			
		||||
cfg.quiet = true
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -95,19 +96,20 @@ cfg.quiet = true
 | 
			
		||||
bot.once("spawn", () => {
 | 
			
		||||
    plugins = {
 | 
			
		||||
        command: require('./plugins/command'),
 | 
			
		||||
        eater: require('./plugins/eater'),
 | 
			
		||||
        inventory: require('./plugins/inventory'),
 | 
			
		||||
        informer: require('./plugins/informer'),
 | 
			
		||||
        inventory: require('./plugins/inventory'),
 | 
			
		||||
        finder: require('./plugins/finder'),
 | 
			
		||||
        sleeper: require('./plugins/sleeper'),
 | 
			
		||||
        armor: require('./plugins/armor'),
 | 
			
		||||
        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'),
 | 
			
		||||
        statemachine: require('./plugins/statemachine'),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    cfg.plugins = plugins
 | 
			
		||||
    cfg.botAddressRegex = new RegExp(`^${bot.username} (${cfg.botAddressPrefix}.+)`)
 | 
			
		||||
 | 
			
		||||
    for (const plugin of Object.values(plugins)) {
 | 
			
		||||
        try {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,11 +30,10 @@ 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)
 | 
			
		||||
            if (/^!/.test(message)) {
 | 
			
		||||
            if (message.startsWith(cfg.botAddressPrefix)) {
 | 
			
		||||
                command(username, message)
 | 
			
		||||
            } else {
 | 
			
		||||
                bot.chat(message)
 | 
			
		||||
@@ -62,6 +61,9 @@ const events = {
 | 
			
		||||
const events_registered = []
 | 
			
		||||
 | 
			
		||||
function command(username, message) {
 | 
			
		||||
    // TODO better name, maybe an array?
 | 
			
		||||
    cfg.botAddressPrefix = cfg.botAddressPrefix || "!"
 | 
			
		||||
 | 
			
		||||
    function fuzzyRespond(responses, probability = 1, timeout = 1) {
 | 
			
		||||
        if (Math.random() < probability) {
 | 
			
		||||
            const response = responses[Math.floor(Math.random() * responses.length)]
 | 
			
		||||
@@ -76,7 +78,7 @@ function command(username, message) {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (username === bot.username && !message.startsWith("!")) return
 | 
			
		||||
    if (username === bot.username && !message.startsWith(cfg.botAddressPrefix)) return
 | 
			
		||||
 | 
			
		||||
    const player = bot.players[username] ? bot.players[username].entity : null
 | 
			
		||||
 | 
			
		||||
@@ -88,11 +90,12 @@ function command(username, message) {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    if (message.startsWith("!") || cfg.botAddress.test(message)) {
 | 
			
		||||
        message = cfg.botAddress.test(message) ? cfg.botAddress.exec(message)[1] : message
 | 
			
		||||
 | 
			
		||||
    if (message.startsWith(cfg.botAddressPrefix) || cfg.botAddressRegex.test(message)) {
 | 
			
		||||
        message = cfg.botAddressRegex.test(message) ? cfg.botAddressRegex.exec(message)[1] : message
 | 
			
		||||
        console.log(message)
 | 
			
		||||
        message = message.slice(1) // remove `!`
 | 
			
		||||
        // remove `!`
 | 
			
		||||
        message = message.startsWith(cfg.botAddressPrefix) ? message.slice(cfg.botAddressPrefix.length) : message
 | 
			
		||||
 | 
			
		||||
        // TODO command dispatchEvent, for aliases
 | 
			
		||||
        function subcommand(message) {
 | 
			
		||||
            const message_parts = message.split(/\s+/)
 | 
			
		||||
@@ -293,7 +296,7 @@ function command(username, message) {
 | 
			
		||||
                            switch (message_parts[1]) {
 | 
			
		||||
                                case "me":
 | 
			
		||||
                                    if (player) {
 | 
			
		||||
                                        bot.lookAt((new v.Vec3(0, 1, 0)).add(player.position))
 | 
			
		||||
                                        bot.lookAt(player.position.offset(0, 1, 0))
 | 
			
		||||
                                    } else {
 | 
			
		||||
                                        cfg.quiet || bot.chat("can't see you")
 | 
			
		||||
                                    }
 | 
			
		||||
@@ -310,7 +313,7 @@ function command(username, message) {
 | 
			
		||||
                                    break
 | 
			
		||||
                                default:
 | 
			
		||||
                                    const aPlayer = bot.players[message_parts[2]] ? bot.players[message_parts[2]].entity : null
 | 
			
		||||
                                    if (aPlayer) bot.lookAt((new v.Vec3(0, 1, 0)).add(aPlayer.position))
 | 
			
		||||
                                    if (aPlayer) bot.lookAt(aPlayer.position.offset(0, 1, 0))
 | 
			
		||||
                                    break;
 | 
			
		||||
                            }
 | 
			
		||||
                            break
 | 
			
		||||
@@ -327,7 +330,7 @@ function command(username, message) {
 | 
			
		||||
                    }
 | 
			
		||||
                    break
 | 
			
		||||
                case "info":
 | 
			
		||||
                    cfg.plugins.informer.command(message_parts.splice(1))
 | 
			
		||||
                    cfg.plugins.informer.command(message_parts.splice(1), player)
 | 
			
		||||
                    break
 | 
			
		||||
                // case "use":
 | 
			
		||||
                //     bot.useOn(bot.nearestEntity())
 | 
			
		||||
@@ -404,6 +407,16 @@ function command(username, message) {
 | 
			
		||||
                            break
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
                case "sm":
 | 
			
		||||
                case "step":
 | 
			
		||||
                    cfg.plugins.statemachine?.command?.(
 | 
			
		||||
                        message_parts[0] == "sm" ? message_parts.slice(1) : message_parts, player
 | 
			
		||||
                    )
 | 
			
		||||
                        // TODO refactor into plugin detection and command exec function
 | 
			
		||||
                        // safecommand(plugin_name, message_parts, player)
 | 
			
		||||
                        // message_parts includes command?
 | 
			
		||||
                        || bot.chat("statemachine plugin not loaded")
 | 
			
		||||
                    break
 | 
			
		||||
                case "location":
 | 
			
		||||
                    // TODO put in /lib/location
 | 
			
		||||
                    switch (message_parts[1]) {
 | 
			
		||||
@@ -461,7 +474,7 @@ function command(username, message) {
 | 
			
		||||
            case "howdy":
 | 
			
		||||
            case "heyo":
 | 
			
		||||
            case "yo":
 | 
			
		||||
                if (player) bot.lookAt((new v.Vec3(0, 1, 0)).add(player.position))
 | 
			
		||||
                if (player) bot.lookAt(player.position.offset(0, 1, 0))
 | 
			
		||||
 | 
			
		||||
                // TODO sneak
 | 
			
		||||
                // function swingArm() {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,7 @@ let cfg = {}
 | 
			
		||||
let bot = {}
 | 
			
		||||
let isEating = false
 | 
			
		||||
 | 
			
		||||
function callbackHandle(err) {
 | 
			
		||||
    if (err) console.error(err)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function eat(callback) {
 | 
			
		||||
function eat(callback = e => e && console.error(e)) {
 | 
			
		||||
    isEating = true
 | 
			
		||||
 | 
			
		||||
    const foodNames = require('minecraft-data')(bot.version).foodsArray.map((item) => item.name)
 | 
			
		||||
@@ -37,23 +33,23 @@ function eat(callback) {
 | 
			
		||||
 | 
			
		||||
    bot.equip(best_food, 'hand', function (error) {
 | 
			
		||||
        if (error) {
 | 
			
		||||
            console.error(error)
 | 
			
		||||
            console.warn(error, best_food)
 | 
			
		||||
            isEating = false
 | 
			
		||||
            bot.emit('eat_stop')
 | 
			
		||||
        } else {
 | 
			
		||||
            bot.consume(function (err) {
 | 
			
		||||
                if (err) {
 | 
			
		||||
                    console.error(err)
 | 
			
		||||
            try {
 | 
			
		||||
                bot.consume().catch(error => {
 | 
			
		||||
                    if (error.message === "Food is full") {
 | 
			
		||||
                        console.warn(error, best_food)
 | 
			
		||||
                    } else {
 | 
			
		||||
                        return callback({ error, best_food })
 | 
			
		||||
                    }
 | 
			
		||||
                }).finally(() => {
 | 
			
		||||
                    isEating = false
 | 
			
		||||
                    bot.emit('eat_stop')
 | 
			
		||||
                    return callback(err)
 | 
			
		||||
                } else {
 | 
			
		||||
                    isEating = false
 | 
			
		||||
                    bot.emit('eat_stop')
 | 
			
		||||
                    callback(null)
 | 
			
		||||
                    if (!bot.food === 20) eat(callbackHandle)
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
                })
 | 
			
		||||
            } catch { }
 | 
			
		||||
            if (bot.food !== 20) eat(callback)
 | 
			
		||||
        }
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
@@ -76,7 +72,7 @@ function checkFood() {
 | 
			
		||||
                // TODO implement better idle state
 | 
			
		||||
            ) || true // idle most likely
 | 
			
		||||
        ) {
 | 
			
		||||
            eat(callbackHandle)
 | 
			
		||||
            eat()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -71,9 +71,7 @@ function lookForMobs() {
 | 
			
		||||
        bot.pvp.attack(entityEnemy)
 | 
			
		||||
    } else if (entityEnemy) {
 | 
			
		||||
        bot.lookAt(
 | 
			
		||||
            // (new v.Vec3(0, 1, 0)).add(
 | 
			
		||||
            entityEnemy.position
 | 
			
		||||
            // )
 | 
			
		||||
        )
 | 
			
		||||
        cfg.quiet || bot.chat("AH! A creeper! They creep me out!")
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
let cfg
 | 
			
		||||
let bot
 | 
			
		||||
let mcData
 | 
			
		||||
// import v from 'vec3'
 | 
			
		||||
const v = require('vec3')
 | 
			
		||||
 | 
			
		||||
function block(pos) {
 | 
			
		||||
@@ -51,34 +52,47 @@ function item(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function entity(name) {
 | 
			
		||||
    const entity = bot.nearestEntity((entity) => {
 | 
			
		||||
    const entity = typeof name === "string" ? bot.nearestEntity((entity) => {
 | 
			
		||||
        const ename = entity.name || entity.username
 | 
			
		||||
        return name && ename ? ename == name : true
 | 
			
		||||
    })
 | 
			
		||||
    }) : entity
 | 
			
		||||
    console.log(entity)
 | 
			
		||||
    if (!entity) {
 | 
			
		||||
        cfg.quiet || bot.chat("no entity")
 | 
			
		||||
        return entity
 | 
			
		||||
    }
 | 
			
		||||
    let info = [entity.type, entity.name || entity.username]
 | 
			
		||||
    let info = [entity.type, entity.username || entity.name]
 | 
			
		||||
    // TODO various info depending on the type of entity; player, villager, etc
 | 
			
		||||
    if (entity.metadata) info.push("len: " + entity.metadata.length)
 | 
			
		||||
    cfg.quiet || bot.chat(info.join("; "))
 | 
			
		||||
    return entity
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function command(message_parts) {
 | 
			
		||||
function command(message_parts, player) {
 | 
			
		||||
    if (message_parts.length > 0) {
 | 
			
		||||
        cfg.info.recentCommand = message_parts
 | 
			
		||||
    }
 | 
			
		||||
    switch (message_parts.length) {
 | 
			
		||||
        case 0:
 | 
			
		||||
            // TODO most recent command?
 | 
			
		||||
            block()
 | 
			
		||||
            if (cfg.info.recentCommand) {
 | 
			
		||||
                command(cfg.info.recentCommand, player)
 | 
			
		||||
            } else {
 | 
			
		||||
                // TODO dispatch on instance of entity, block, etc..
 | 
			
		||||
                // TODO have the logic inside the function or with a utility function
 | 
			
		||||
                block(player.position || player?.entity.position || null)
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case 1:
 | 
			
		||||
            switch (message_parts[0]) {
 | 
			
		||||
                case "i":
 | 
			
		||||
                case "item":
 | 
			
		||||
                    item()
 | 
			
		||||
                    break
 | 
			
		||||
                case "e":
 | 
			
		||||
                case "entity":
 | 
			
		||||
                    entity()
 | 
			
		||||
                    break
 | 
			
		||||
                case "b":
 | 
			
		||||
                case "block":
 | 
			
		||||
                default:
 | 
			
		||||
                    block()
 | 
			
		||||
@@ -89,9 +103,11 @@ function command(message_parts) {
 | 
			
		||||
 | 
			
		||||
        case 2:
 | 
			
		||||
            switch (message_parts[0]) {
 | 
			
		||||
                case "i":
 | 
			
		||||
                case "item":
 | 
			
		||||
                    item(message_parts[1])
 | 
			
		||||
                    break
 | 
			
		||||
                case "e":
 | 
			
		||||
                case "entity":
 | 
			
		||||
                default:
 | 
			
		||||
                    entity(message_parts[1])
 | 
			
		||||
@@ -101,6 +117,7 @@ function command(message_parts) {
 | 
			
		||||
 | 
			
		||||
        case 4:
 | 
			
		||||
            switch (message_parts[0]) {
 | 
			
		||||
                case "b":
 | 
			
		||||
                case "block":
 | 
			
		||||
                default:
 | 
			
		||||
                    block(message_parts.slice(1))
 | 
			
		||||
@@ -110,6 +127,7 @@ function command(message_parts) {
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
        default:
 | 
			
		||||
            cfg.quiet || bot.chat("info: unknown command")
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -119,6 +137,7 @@ const load = (config) => {
 | 
			
		||||
    bot = cfg.bot
 | 
			
		||||
    cfg.info = {
 | 
			
		||||
        quiet: cfg.quiet,
 | 
			
		||||
        recentCommand: null,
 | 
			
		||||
    }
 | 
			
		||||
    mcData = bot.mcData || (bot.mcData = require('minecraft-data')(bot.version))
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,17 +11,16 @@ let movements = []
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function initMoves(bot = bot, mcData = bot.mcData) {
 | 
			
		||||
    console.info(movements)
 | 
			
		||||
    if (movements.length > 0) {
 | 
			
		||||
        bot.pathfinder.setMovements(movements.defaultMove)
 | 
			
		||||
        return console.warn("movements already initialized!")
 | 
			
		||||
        return console.warn("go init: movements already initialized!", movements)
 | 
			
		||||
    }
 | 
			
		||||
    let defaultMove = new Movements(bot, mcData)
 | 
			
		||||
    defaultMove.canDig = false
 | 
			
		||||
    defaultMove.scafoldingBlocks.push(mcData.blocksByName.slime_block.id)
 | 
			
		||||
    defaultMove.blocksCantBreak.add(mcData.blocksByName.glass.id)
 | 
			
		||||
    defaultMove.blocksToAvoid.add(mcData.blocksByName.magma_block.id)
 | 
			
		||||
    movements.push(defaultMove)
 | 
			
		||||
    const normalMove = new Movements(bot, mcData)
 | 
			
		||||
    normalMove.canDig = false
 | 
			
		||||
    normalMove.scafoldingBlocks.push(mcData.blocksByName.slime_block.id)
 | 
			
		||||
    normalMove.blocksCantBreak.add(mcData.blocksByName.glass.id)
 | 
			
		||||
    normalMove.blocksToAvoid.add(mcData.blocksByName.magma_block.id)
 | 
			
		||||
    movements.push(normalMove)
 | 
			
		||||
    movements.defaultMove = movements[0]
 | 
			
		||||
 | 
			
		||||
    bot.pathfinder.setMovements(defaultMove)
 | 
			
		||||
@@ -82,6 +81,66 @@ function follow(entity, dynamic = true, distance = 3) {
 | 
			
		||||
    bot.pathfinder.setGoal(new GoalFollow(entity, distance), dynamic)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function away(entity = bot.nearestEntity(), invertInvert = true, dynamic = true, distance = 10) {
 | 
			
		||||
    const currentGoal = bot.pathfinder.goal
 | 
			
		||||
    console.assert(currentGoal || entity)
 | 
			
		||||
    const { GoalInvert } = require('mineflayer-pathfinder').goals
 | 
			
		||||
 | 
			
		||||
    bot.pathfinder.setMovements(movements.defaultMove)
 | 
			
		||||
 | 
			
		||||
    if (!currentGoal) {
 | 
			
		||||
        const { GoalFollow } = require('mineflayer-pathfinder').goals
 | 
			
		||||
 | 
			
		||||
        if (entity.entity) {
 | 
			
		||||
            console.log("go away entity:", entity, entity.entity)
 | 
			
		||||
            entity = entity.entity
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        cfg.quiet || bot.chat(
 | 
			
		||||
            `going away from ${entity?.type
 | 
			
		||||
            }: ${entity?.username || entity?.displayName
 | 
			
		||||
            }${dynamic ? "" : " once"}`
 | 
			
		||||
        )
 | 
			
		||||
        // alternative implementation
 | 
			
		||||
        // follow(entity, dynamic, distance)
 | 
			
		||||
        // bot.pathfinder.setGoal(new GoalInvert(bot.pathfinder.goal), dynamic)
 | 
			
		||||
        return bot.pathfinder.setGoal(new GoalInvert(
 | 
			
		||||
            new GoalFollow(entity, distance)
 | 
			
		||||
        ), dynamic)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (currentGoal instanceof GoalInvert) {
 | 
			
		||||
        const currEntity = currentGoal.goal.entity
 | 
			
		||||
        console.log("go away inverse goal:", currentGoal.goal)
 | 
			
		||||
        if (invertInvert) {
 | 
			
		||||
            cfg.quiet || bot.chat(
 | 
			
		||||
                `switching towards ${currentGoal.goal?.constructor.name
 | 
			
		||||
                }: ${currEntity?.type
 | 
			
		||||
                }: ${currEntity?.username || currEntity?.displayName
 | 
			
		||||
                }${dynamic ? "" : " once"}`
 | 
			
		||||
            )
 | 
			
		||||
            bot.pathfinder.setGoal(currentGoal.goal, dynamic)
 | 
			
		||||
        } else {
 | 
			
		||||
            cfg.quiet || bot.chat(
 | 
			
		||||
                `already going away from ${currentGoal.goal?.constructor.name
 | 
			
		||||
                }; not switching`
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        const currEntity = currentGoal.entity
 | 
			
		||||
        console.log("go away goal:", currentGoal)
 | 
			
		||||
        cfg.quiet || bot.chat(
 | 
			
		||||
            `going away from ${currentGoal?.constructor.name
 | 
			
		||||
            }: ${currEntity?.type
 | 
			
		||||
            }: ${currEntity?.username || currEntity?.displayName
 | 
			
		||||
            }${dynamic ? "" : " once"}`
 | 
			
		||||
        )
 | 
			
		||||
        bot.pathfinder.setGoal(new GoalInvert(currentGoal), dynamic)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ride(entity) {
 | 
			
		||||
    entity = entity?.entity || entity
 | 
			
		||||
    const ridableMobs = ["Horse", "Donkey", "Pig", "Strider"]
 | 
			
		||||
@@ -107,7 +166,8 @@ function ride(entity) {
 | 
			
		||||
function moveOrRide(turn = false, reverse = -1, directionLabel, message_parts2) {
 | 
			
		||||
    // bot.once("attach", state = "vehiccel")
 | 
			
		||||
    if (bot.vehicle) {
 | 
			
		||||
        const amount = parseInt(message_parts2[0]) || 10 * -reverse
 | 
			
		||||
        // FIXME moveVehicle should be +-1 or 0?
 | 
			
		||||
        const amount = Math.sign(parseInt(message_parts2[0]) || -reverse)
 | 
			
		||||
        bot.moveVehicle(turn && amount || 0, !turn && amount || 0)
 | 
			
		||||
    } else {
 | 
			
		||||
        command([directionLabel].concat(message_parts2))
 | 
			
		||||
@@ -230,6 +290,11 @@ function command(message_parts, player) {
 | 
			
		||||
        case "mount":
 | 
			
		||||
            ride(message_parts2[0])
 | 
			
		||||
            break
 | 
			
		||||
        case "away":
 | 
			
		||||
        case "run":
 | 
			
		||||
        case "runaway":
 | 
			
		||||
            away()
 | 
			
		||||
            break
 | 
			
		||||
        case "w":
 | 
			
		||||
        case "f":
 | 
			
		||||
            moveOrRide(0, -1, "forward", message_parts2)
 | 
			
		||||
@@ -246,6 +311,11 @@ function command(message_parts, player) {
 | 
			
		||||
        case "r":
 | 
			
		||||
            moveOrRide(1, 1, "left", message_parts2)
 | 
			
		||||
            break
 | 
			
		||||
        case "up":
 | 
			
		||||
        case "u":
 | 
			
		||||
        case "j":
 | 
			
		||||
            moveOrRide(1, 1, "left", message_parts2)
 | 
			
		||||
            break
 | 
			
		||||
        case "back":
 | 
			
		||||
        case "forward":
 | 
			
		||||
        case "jump":
 | 
			
		||||
 
 | 
			
		||||
@@ -1,115 +1,479 @@
 | 
			
		||||
// 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)
 | 
			
		||||
    //     }
 | 
			
		||||
    // })
 | 
			
		||||
// import { createMachine, interpret, InterpreterStatus } from "xstate";
 | 
			
		||||
const { createMachine, interpret, InterpreterStatus } = require('xstate');
 | 
			
		||||
// import { access, mkdir, writeFile, readFile } from "fs";
 | 
			
		||||
const { access, mkdir, writeFile, readFile } = require('fs');
 | 
			
		||||
// ANGRAM_PREFIX='MINECRAFT'
 | 
			
		||||
const { MINECRAFT_DATA_FOLDER } = process.env || require("dotenv-packed").parseEnv().parsed;
 | 
			
		||||
const storage_dir = MINECRAFT_DATA_FOLDER || './data/' + "/sm/";
 | 
			
		||||
// import { createBot } from "mineflayer"
 | 
			
		||||
// let { pathfinder, Movements, goals } = require('mineflayer-pathfinder')
 | 
			
		||||
// let cfg
 | 
			
		||||
// let bot = createBot({ username: 'statebot' })
 | 
			
		||||
let bot;
 | 
			
		||||
let quiet;
 | 
			
		||||
let updateRate = 20;
 | 
			
		||||
const machines = {
 | 
			
		||||
    list: {},
 | 
			
		||||
    running: {},
 | 
			
		||||
};
 | 
			
		||||
let webserver;
 | 
			
		||||
let cfg = {
 | 
			
		||||
    statemachine: {
 | 
			
		||||
        webserver: null,
 | 
			
		||||
        // quiet: true,
 | 
			
		||||
        quiet: quiet,
 | 
			
		||||
        list: {},
 | 
			
		||||
        running: {},
 | 
			
		||||
        draft: null,
 | 
			
		||||
        recent: null,
 | 
			
		||||
        // debug: null,
 | 
			
		||||
        debug: true,
 | 
			
		||||
        updateRate: updateRate
 | 
			
		||||
    }
 | 
			
		||||
    // FIXME temp variables to satisfy typescript autocomplete
 | 
			
		||||
    // , quiet: null
 | 
			
		||||
    ,
 | 
			
		||||
    bot: bot,
 | 
			
		||||
    plugins: { statemachine: null }
 | 
			
		||||
};
 | 
			
		||||
// Edit your machine(s) here
 | 
			
		||||
function init(smName = "dummy", webserver) {
 | 
			
		||||
    access(storage_dir, err => {
 | 
			
		||||
        if (err?.code === 'ENOENT') {
 | 
			
		||||
            mkdir(storage_dir, e => e && console.warn("sm init: create dir", e));
 | 
			
		||||
        }
 | 
			
		||||
        else if (err) {
 | 
			
		||||
            console.warn("sm init: create dir", err);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    // const machine = newSM(smName)
 | 
			
		||||
    // machine.states.idle.on.TOGGLE = "start"
 | 
			
		||||
    // machine.states.start.on.TOGGLE = "idle"
 | 
			
		||||
    const machine = createMachine({
 | 
			
		||||
        id: smName,
 | 
			
		||||
        initial: "idle",
 | 
			
		||||
        states: {
 | 
			
		||||
            idle: {
 | 
			
		||||
                on: { TOGGLE: "start", NEXT: "start", PREV: "look", STOP: "finish" }
 | 
			
		||||
            },
 | 
			
		||||
            start: {
 | 
			
		||||
                on: { TOGGLE: "look", NEXT: "look", PREV: "idle" },
 | 
			
		||||
                entry: 'lookAtPlayerOnce',
 | 
			
		||||
            },
 | 
			
		||||
            look: {
 | 
			
		||||
                on: { TOGGLE: "idle", NEXT: "idle", PREV: "start" },
 | 
			
		||||
                // entry: ['look', 'blah']
 | 
			
		||||
                // entry: 'lookAtPlayerOnce',
 | 
			
		||||
                activities: 'lookAtPlayer',
 | 
			
		||||
                meta: { debug: true }
 | 
			
		||||
            },
 | 
			
		||||
            finish: {
 | 
			
		||||
                type: 'final'
 | 
			
		||||
            },
 | 
			
		||||
        },
 | 
			
		||||
        on: { START: '.start', STOP: '.idle' },
 | 
			
		||||
        meta: { debug: true },
 | 
			
		||||
        context: { player: null, rate: updateRate },
 | 
			
		||||
    }, {
 | 
			
		||||
        actions: {
 | 
			
		||||
            // action implementation
 | 
			
		||||
            lookAtPlayerOnce: (context, event) => {
 | 
			
		||||
                const player = context?.player && (context?.player?.isValid || context?.player?.entity?.isValid)
 | 
			
		||||
                    || bot.nearestEntity(entity => entity.type === 'player');
 | 
			
		||||
                if (player?.position || player?.entity) {
 | 
			
		||||
                    context.player = player;
 | 
			
		||||
                    bot.lookAt((player.entity || player).position.offset(0, 1, 0));
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        activities: {
 | 
			
		||||
            lookAtPlayer: (context, event) => {
 | 
			
		||||
                const player = (context?.player?.isValid || context?.player?.entity?.isValid) && context?.player
 | 
			
		||||
                    || bot.nearestEntity(entity => entity.type === 'player' && entity.isValid);
 | 
			
		||||
                // TODO check pos every event?
 | 
			
		||||
                if (player?.position || player?.entity) {
 | 
			
		||||
                    context.player = player;
 | 
			
		||||
                    function looks() {
 | 
			
		||||
                        bot.lookAt((player.entity || player).position.offset(0, 1, 0));
 | 
			
		||||
                    }
 | 
			
		||||
                    bot.on("time", looks);
 | 
			
		||||
                    return () => bot.off("time", looks);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    quiet || bot.chat("look: no valid players");
 | 
			
		||||
                    // TODO use xstate logger
 | 
			
		||||
                    context.debug && console.log("sm: no valid player", this, context, player);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        delays: {
 | 
			
		||||
            /* ... */
 | 
			
		||||
        },
 | 
			
		||||
        guards: {
 | 
			
		||||
            /* ... */
 | 
			
		||||
        },
 | 
			
		||||
        services: {
 | 
			
		||||
            /* ... */
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    console.log("sm init: machine", machine);
 | 
			
		||||
    const service = runSM(saveSM(machine));
 | 
			
		||||
    if (service?.send) {
 | 
			
		||||
        setTimeout(service.send, 200, "TOGGLE");
 | 
			
		||||
        // setTimeout(service.send, 400, "TOGGLE")
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        console.warn("sm init: service", service);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const load = (config) => {
 | 
			
		||||
    cfg = config
 | 
			
		||||
    bot = cfg.bot
 | 
			
		||||
    // cfg.inventory = {
 | 
			
		||||
    //     auto: true,
 | 
			
		||||
    //     quiet: false
 | 
			
		||||
    // }
 | 
			
		||||
    // bot.on('chat', inventory)
 | 
			
		||||
    bot.loadPlugin(pathfinder)
 | 
			
		||||
    // statemachineInit()
 | 
			
		||||
function newSM(smName = "sm_" + Object.keys(cfg.statemachine.list).length) {
 | 
			
		||||
    smName = smName.replace(/\s+/, '_');
 | 
			
		||||
    if (cfg.statemachine.list[smName]) {
 | 
			
		||||
        console.warn("sm exists", smName);
 | 
			
		||||
        quiet || bot.chat(`sm ${smName} already exists, edit or use another name instead`);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    const machine = createMachine({
 | 
			
		||||
        id: smName,
 | 
			
		||||
        initial: "start",
 | 
			
		||||
        // TODO use history states for PAUSE and RESUME
 | 
			
		||||
        states: { idle: { on: { START: "start" } }, start: { on: { STOP: "idle" } } }
 | 
			
		||||
    });
 | 
			
		||||
    cfg.statemachine.draft = machine;
 | 
			
		||||
    return machine;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const unload = () => {
 | 
			
		||||
    // bot.off('chat', inventory)
 | 
			
		||||
function saveSM(machine = cfg.statemachine.draft) {
 | 
			
		||||
    if (!machine?.id) {
 | 
			
		||||
        console.warn("sm save: invalid", machine);
 | 
			
		||||
        quiet || bot.chat("sm: couldn't save, invalid");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO do tests and validation
 | 
			
		||||
    // 1. ensure default states [start, idle]
 | 
			
		||||
    // 2. ensure closed cycle from start to idle
 | 
			
		||||
    cfg.statemachine.list[machine.id] = machine;
 | 
			
		||||
    if (machine.id === cfg.statemachine.draft?.id) {
 | 
			
		||||
        cfg.statemachine.draft = null;
 | 
			
		||||
    }
 | 
			
		||||
    writeFile(
 | 
			
		||||
        // TODO
 | 
			
		||||
        // `${storage_dir}/${player}/${machine.id}.json`
 | 
			
		||||
        `${storage_dir}/${machine.id}.json`,
 | 
			
		||||
        // TODO decide which data to store
 | 
			
		||||
        // https://xstate.js.org/docs/guides/states.html#persisting-state
 | 
			
		||||
        // JSON.stringify(machine.toJSON),
 | 
			
		||||
        // JSON.stringify(machine.states.toJSON), // + activities, delays, etc
 | 
			
		||||
        JSON.stringify({ config: machine.config, context: machine.context }), e => e && console.log("sm load sm: write file", e));
 | 
			
		||||
    // return run ? runSM(machine) : machine
 | 
			
		||||
    return machine;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports = { load, unload }
 | 
			
		||||
function loadSM(name, run = true) {
 | 
			
		||||
    //: StateMachine<any, any, any> {
 | 
			
		||||
    readFile(
 | 
			
		||||
        // readFileSync(
 | 
			
		||||
        // TODO
 | 
			
		||||
        // `${storage_dir}/${player}/${machine.id}.json`
 | 
			
		||||
        `${storage_dir}/${name}.json`, afterRead
 | 
			
		||||
        // JSON.stringify(machine.toJSON),
 | 
			
		||||
    );
 | 
			
		||||
    function afterRead(err, jsonString) {
 | 
			
		||||
        if (err) {
 | 
			
		||||
            console.warn("sm load sm: read file", err);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            const machine = createMachine(JSON.parse(jsonString).config);
 | 
			
		||||
            // TODO do tests and validation
 | 
			
		||||
            // 1. ensure default states [start, idle]
 | 
			
		||||
            // 2. ensure closed cycle from start to idle
 | 
			
		||||
            cfg.statemachine.list[machine.id] = machine;
 | 
			
		||||
            if (machine.id === cfg.statemachine.draft?.id) {
 | 
			
		||||
                cfg.statemachine.draft = machine;
 | 
			
		||||
            }
 | 
			
		||||
            if (run) {
 | 
			
		||||
                runSM(machine);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    // return run ? runSM(machine) : machine
 | 
			
		||||
    // return machine
 | 
			
		||||
}
 | 
			
		||||
// function isInterpreter(SMorService: StateMachine<any, any, any> | Interpreter<any>): SMorService is Interpreter<any> {
 | 
			
		||||
//   return (SMorService as Interpreter<any>).start !== undefined;
 | 
			
		||||
// }
 | 
			
		||||
function getSM(name = cfg.statemachine.draft || cfg.statemachine.recent, asService = false, quiet = false) {
 | 
			
		||||
    const machine = typeof name === "string" ? cfg.statemachine.list[name]
 | 
			
		||||
        : name;
 | 
			
		||||
    if (!machine) {
 | 
			
		||||
        console.warn("sm get: doesn't exist", name);
 | 
			
		||||
        cfg.statemachine.quiet || bot.chat(`sm ${name} doesn't exist`);
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (asService) {
 | 
			
		||||
        const service = cfg.statemachine.running[machine?.id];
 | 
			
		||||
        if (!service) {
 | 
			
		||||
            quiet || console.warn("sm get: already stopped", machine);
 | 
			
		||||
            quiet || cfg.statemachine.quiet || bot.chat(`sm ${machine?.id} isn't running`);
 | 
			
		||||
            return interpret(machine);
 | 
			
		||||
        }
 | 
			
		||||
        return service;
 | 
			
		||||
    }
 | 
			
		||||
    else {
 | 
			
		||||
        // return machine.machine ? machine.machine : machine
 | 
			
		||||
        return machine;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
function runSM(name = getSM(undefined, undefined, true), player // or supervisor?
 | 
			
		||||
    , restart = false) {
 | 
			
		||||
    if (!name)
 | 
			
		||||
        return;
 | 
			
		||||
    const service = getSM(name, true, true);
 | 
			
		||||
    if (!service)
 | 
			
		||||
        return;
 | 
			
		||||
    const machine = service.machine;
 | 
			
		||||
    if (!machine)
 | 
			
		||||
        return;
 | 
			
		||||
    switch (service.status) {
 | 
			
		||||
        case InterpreterStatus.Running:
 | 
			
		||||
            if (!restart) {
 | 
			
		||||
                console.warn("sm run: already running", service.id);
 | 
			
		||||
                quiet || bot.chat(`sm ${service.id} already running`);
 | 
			
		||||
                return service;
 | 
			
		||||
            }
 | 
			
		||||
            stopSM(machine);
 | 
			
		||||
        case InterpreterStatus.NotStarted:
 | 
			
		||||
        case InterpreterStatus.Stopped:
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            console.warn("sm run: unknown status:", service.status, service);
 | 
			
		||||
            return;
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
    if (cfg.statemachine.debug || machine.meta?.debug) {
 | 
			
		||||
        service.onTransition((state) => {
 | 
			
		||||
            quiet || bot.chat(`sm trans: ${machine.id}, ${state.value}`);
 | 
			
		||||
            quiet || state.meta?.debug && bot.chat(`sm next events: ${machine.id}, ${state.nextEvents}`);
 | 
			
		||||
            console.log("sm debug: trans", state.value, state);
 | 
			
		||||
        }).onDone((done) => {
 | 
			
		||||
            quiet || bot.chat(`sm done: ${machine.id}, ${done}`);
 | 
			
		||||
            console.log("sm debug: done", done.data, done);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
    cfg.statemachine.running[machine.id] = service;
 | 
			
		||||
    cfg.statemachine.recent = machine;
 | 
			
		||||
    service.start();
 | 
			
		||||
    // return machine
 | 
			
		||||
    return service;
 | 
			
		||||
}
 | 
			
		||||
function stopSM(name = getSM(), quiet = false) {
 | 
			
		||||
    let service = getSM(name, true, quiet);
 | 
			
		||||
    if (!service)
 | 
			
		||||
        return;
 | 
			
		||||
    const machine = service.machine;
 | 
			
		||||
    switch (service.status) {
 | 
			
		||||
        case InterpreterStatus.NotStarted:
 | 
			
		||||
        case InterpreterStatus.Stopped:
 | 
			
		||||
            console.log("sm stop status", service.status, service.id, cfg.statemachine.running[service.id]);
 | 
			
		||||
        // TODO check if any bugs
 | 
			
		||||
        case InterpreterStatus.Running:
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            console.warn("sm stop: unknown status:", service.status);
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
    service?.stop?.();
 | 
			
		||||
    cfg.statemachine.running[machine.id] = null;
 | 
			
		||||
    delete cfg.statemachine.running[machine.id];
 | 
			
		||||
    // return machine
 | 
			
		||||
    return service;
 | 
			
		||||
}
 | 
			
		||||
function actionSM(action, name = getSM()) {
 | 
			
		||||
    if (!action) {
 | 
			
		||||
        return console.warn("sm action", action);
 | 
			
		||||
    }
 | 
			
		||||
    let service = getSM(name, true, true);
 | 
			
		||||
    if (service.status !== InterpreterStatus.Running)
 | 
			
		||||
        return;
 | 
			
		||||
    // const machine = service.machine
 | 
			
		||||
    service.send(action.toUpperCase());
 | 
			
		||||
}
 | 
			
		||||
function stepSM(command = "", ...message_parts) {
 | 
			
		||||
    let service = getSM(undefined, true);
 | 
			
		||||
    if (!service)
 | 
			
		||||
        return;
 | 
			
		||||
    if (!service.send) {
 | 
			
		||||
        console.warn("sm step: can't send", service.machine);
 | 
			
		||||
        // TODO start a temporary service to interpret
 | 
			
		||||
        quiet || bot.chat("sm: step doesn't support machines that aren't running yet");
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    if (service?.status !== InterpreterStatus.Running) {
 | 
			
		||||
        console.warn("sm step: machine not running, attempting start", service);
 | 
			
		||||
        runSM;
 | 
			
		||||
    }
 | 
			
		||||
    // const machine = service.machine
 | 
			
		||||
    switch (command) {
 | 
			
		||||
        case "edit":
 | 
			
		||||
            // maybe `edit <type>`, like `add`?
 | 
			
		||||
            // where type:
 | 
			
		||||
            // context
 | 
			
		||||
            // action
 | 
			
		||||
            // timeout | all timeout
 | 
			
		||||
            break;
 | 
			
		||||
        case "undo":
 | 
			
		||||
            break;
 | 
			
		||||
        case "del":
 | 
			
		||||
            break;
 | 
			
		||||
        case "p":
 | 
			
		||||
        case "prev":
 | 
			
		||||
            service?.send("PREV");
 | 
			
		||||
            break;
 | 
			
		||||
        case "add":
 | 
			
		||||
        // maybe `add <type>`?
 | 
			
		||||
        // where type:
 | 
			
		||||
        // context
 | 
			
		||||
        // action
 | 
			
		||||
        // timeout
 | 
			
		||||
        case "new":
 | 
			
		||||
            break;
 | 
			
		||||
        // case "with":
 | 
			
		||||
        //   console.log(this)
 | 
			
		||||
        //   stepSM(getSM(message_parts[0], true, true), ...message_parts.slice(1))
 | 
			
		||||
        //   break;
 | 
			
		||||
        case "help":
 | 
			
		||||
            quiet || bot.chat("![sm ]step [ p(rev) | n(ext) ]");
 | 
			
		||||
            break;
 | 
			
		||||
        case "n":
 | 
			
		||||
        case "next":
 | 
			
		||||
        default:
 | 
			
		||||
            service?.send("NEXT");
 | 
			
		||||
            quiet || bot.chat("stepped");
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
function tickSM(time = bot.time.timeOfDay, rate = 20) {
 | 
			
		||||
    if (time % rate !== 0) {
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
    console.log("sm tick", rate, time);
 | 
			
		||||
}
 | 
			
		||||
function debugSM(name = getSM()) {
 | 
			
		||||
    if (!name)
 | 
			
		||||
        return;
 | 
			
		||||
    let service = getSM(name, true);
 | 
			
		||||
    if (!service)
 | 
			
		||||
        return;
 | 
			
		||||
    const machine = service.machine;
 | 
			
		||||
    machine.meta.debug = !!!machine.meta.debug;
 | 
			
		||||
    console.info("sm debug", machine.meta, service, machine);
 | 
			
		||||
    cfg.statemachine.quiet || machine.meta.debug && bot.chat("sm debug: " + machine.id);
 | 
			
		||||
}
 | 
			
		||||
function command(message_parts) {
 | 
			
		||||
    const message_parts2 = message_parts.slice(1);
 | 
			
		||||
    switch (message_parts[0]) {
 | 
			
		||||
        case "add":
 | 
			
		||||
            command(["new"].concat(message_parts2));
 | 
			
		||||
            break;
 | 
			
		||||
        case "finish":
 | 
			
		||||
        case "done":
 | 
			
		||||
        case "end":
 | 
			
		||||
            command(["save"].concat(message_parts2));
 | 
			
		||||
            break;
 | 
			
		||||
        case "do":
 | 
			
		||||
        case "load":
 | 
			
		||||
        case "start":
 | 
			
		||||
            command(["run"].concat(message_parts2));
 | 
			
		||||
            break;
 | 
			
		||||
        case "debug":
 | 
			
		||||
            switch (message_parts[1]) {
 | 
			
		||||
                case "sm":
 | 
			
		||||
                case "global":
 | 
			
		||||
                case "meta":
 | 
			
		||||
                    cfg.statemachine.debug = !!!cfg.statemachine.debug;
 | 
			
		||||
                    quiet || bot.chat(`sm debug: ${cfg.statemachine.debug}`);
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
        case "new":
 | 
			
		||||
        case "save":
 | 
			
		||||
        case "run":
 | 
			
		||||
        case "step":
 | 
			
		||||
        case "stop":
 | 
			
		||||
        // temp
 | 
			
		||||
        case "action":
 | 
			
		||||
            switch (message_parts2.length) {
 | 
			
		||||
                case 0:
 | 
			
		||||
                    console.warn(`sm ${message_parts[0]}: no name, using defaults`);
 | 
			
		||||
                case 1:
 | 
			
		||||
                    // FIXME `this` doesn't work always
 | 
			
		||||
                    (this.runSM && this || cfg.plugins.statemachine)[message_parts[0] + "SM"](...message_parts2);
 | 
			
		||||
                    break;
 | 
			
		||||
                default:
 | 
			
		||||
                    if (["action"].includes(message_parts[0])) {
 | 
			
		||||
                        (this.runSM && this || cfg.plugins.statemachine)[message_parts[0] + "SM"](...message_parts2);
 | 
			
		||||
                    }
 | 
			
		||||
                    else {
 | 
			
		||||
                        console.warn(`sm ${message_parts[0]}: more than 1 arg passed`, message_parts2);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case "undo":
 | 
			
		||||
            break;
 | 
			
		||||
        case "list":
 | 
			
		||||
        case "status":
 | 
			
		||||
            // TODO current/recent, updateRate
 | 
			
		||||
            const { list, running } = cfg.statemachine;
 | 
			
		||||
            console.log("sm list", running, list);
 | 
			
		||||
            quiet || bot.chat(`${Object.keys(running).length} of ${Object.keys(list).length} active: ${Object.keys(running)}`
 | 
			
		||||
                + (message_parts[1] === "all" ? `${Object.keys(list)}` : ''));
 | 
			
		||||
            break;
 | 
			
		||||
        case "quiet":
 | 
			
		||||
            quiet = cfg.statemachine.quiet = !!!cfg.statemachine.quiet;
 | 
			
		||||
            quiet || bot.chat(`sm: ${cfg.statemachine.quiet ? "" : "not "}being quiet`);
 | 
			
		||||
            break;
 | 
			
		||||
        default:
 | 
			
		||||
            // TODO general helper from declarative commands object
 | 
			
		||||
            quiet || bot.chat(`sm help: !sm [new | step| save | run | list | quiet | debug]`);
 | 
			
		||||
            console.warn("sm unknown command", message_parts);
 | 
			
		||||
            break;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
}
 | 
			
		||||
function load(config) {
 | 
			
		||||
    webserver = cfg.statemachine.webserver = config.statemachine?.webserver || webserver;
 | 
			
		||||
    config.statemachine = cfg.statemachine || {
 | 
			
		||||
        webserver: null,
 | 
			
		||||
        // quiet: true,
 | 
			
		||||
        quiet: false,
 | 
			
		||||
        list: {},
 | 
			
		||||
        running: {},
 | 
			
		||||
        draft: null,
 | 
			
		||||
        recent: null,
 | 
			
		||||
        // debug: null,
 | 
			
		||||
        debug: true,
 | 
			
		||||
        updateRate: updateRate
 | 
			
		||||
    };
 | 
			
		||||
    cfg = config;
 | 
			
		||||
    bot = cfg.bot;
 | 
			
		||||
    // pathfinder = bot.pathfinder || bot.loadPlugin(require('mineflayer-pathfinder').pathfinder)
 | 
			
		||||
    // mcData = bot.mcData || (bot.mcData = require('minecraft-data')(bot.version))
 | 
			
		||||
    init(undefined, webserver);
 | 
			
		||||
    updateRate = cfg.statemachine.updateRate || updateRate;
 | 
			
		||||
    bot.on('time', tickSM);
 | 
			
		||||
    // bot.once('time', tickSM, 5)
 | 
			
		||||
    console.log("sm load", cfg.statemachine);
 | 
			
		||||
}
 | 
			
		||||
function unload() {
 | 
			
		||||
    const { list, running } = cfg.statemachine;
 | 
			
		||||
    bot.off('time', tickSM);
 | 
			
		||||
    Object.keys(running).forEach(sm => {
 | 
			
		||||
        stopSM(sm);
 | 
			
		||||
    });
 | 
			
		||||
    // delete cfg.statemachine;
 | 
			
		||||
    cfg.statemachine = null;
 | 
			
		||||
    console.log("sm unload: deleted", cfg.statemachine);
 | 
			
		||||
}
 | 
			
		||||
module.exports = {
 | 
			
		||||
    load, unload, command, init,
 | 
			
		||||
    newSM, saveSM, loadSM, runSM, stopSM, actionSM, stepSM, debugSM
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										20
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								package.json
									
									
									
									
									
								
							@@ -30,24 +30,26 @@
 | 
			
		||||
  },
 | 
			
		||||
  "homepage": "https://github.com/PrismarineJS/prismarine-template#readme",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/node": "^14.14.35",
 | 
			
		||||
    "jest": "^26.6.3",
 | 
			
		||||
    "require-self": "^0.2.3"
 | 
			
		||||
    "require-self": "^0.2.3",
 | 
			
		||||
    "typescript": "^4.2.3"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "dotenv-packed": "^1.2.1",
 | 
			
		||||
    "minecraft-data": "^2.73.1",
 | 
			
		||||
    "mineflayer": "^2.40.1",
 | 
			
		||||
    "minecraft-data": "^2.80.0",
 | 
			
		||||
    "mineflayer": "^3.4.0",
 | 
			
		||||
    "mineflayer-armor-manager": "^1.4.0",
 | 
			
		||||
    "mineflayer-pathfinder": "^1.3.6",
 | 
			
		||||
    "mineflayer-pathfinder": "^1.6.1",
 | 
			
		||||
    "mineflayer-pvp": "^1.0.2",
 | 
			
		||||
    "prismarine-block": "^1.7.3",
 | 
			
		||||
    "prismarine-block": "^1.8.0",
 | 
			
		||||
    "prismarine-chat": "^1.0.3",
 | 
			
		||||
    "prismarine-entity": "^1.1.0",
 | 
			
		||||
    "prismarine-item": "^1.5.0",
 | 
			
		||||
    "prismarine-nbt": "^1.4.0",
 | 
			
		||||
    "prismarine-item": "^1.8.0",
 | 
			
		||||
    "prismarine-nbt": "^1.5.0",
 | 
			
		||||
    "prismarine-recipe": "^1.1.0",
 | 
			
		||||
    "typescript": "^4.1.3",
 | 
			
		||||
    "vec3": "^0.1.7"
 | 
			
		||||
    "vec3": "^0.1.7",
 | 
			
		||||
    "xstate": "^4.17.0"
 | 
			
		||||
  },
 | 
			
		||||
  "files": [
 | 
			
		||||
    "lib/**/*"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user