// 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 || 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 || bot . nearestEntity ( entity => entity . type === 'player' ) ;
// TODO check 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 ) ;
}
}
} ,
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 ) ;
}
}
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 ;
}
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 ;
}
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 ( ) ;
// // TODO check if idle state is different (maybe?)
console . log ( "sm run" , service . state . value === machine . initialState . value , service . state ) ;
console . log ( "sm run" , service . state !== machine . initialState , machine . initialState ) ;
// 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 . toLowerCase ( ) ) ;
}
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
} ;