Compare commits

...

114 Commits

Author SHA1 Message Date
jay
feb0b0927a feat(informer): add villager profession details
Provide names in place of numbers for villager profession.
Implementation uses an enum
2021-05-15 18:37:24 +05:00
jay
8276e68489 feat(informer): add block info based on relative position for players 2021-05-11 17:59:26 +05:00
jay
2b7163bef3 feat(informer): add block info based on other players
Now should work for position of players other than the callee.

Alternative implementation could use `entity` function.
This allows to also work for named entities as a bonus.
2021-05-11 17:54:19 +05:00
jay
a0b4641f37 feat(informer): add entity detail by type and kind for some entities
So far:
- mobs
 - villager
- object
 - drops
2021-05-11 17:42:31 +05:00
jay
186d6ac3d2 feat(informer): 🚸 add more detail to entity info 2021-05-11 17:36:41 +05:00
jay
984c9490c3 fix(informer): 🥅 make player undefined if nul
This allows to fallback to defaults in functions
2021-05-11 17:34:25 +05:00
jay
3379f75ab9 feat(informer): 🚸 make entity search case insensitive 2021-05-11 17:32:38 +05:00
jay
69b1ab0b0b feat(informer): add reverse mcdata lookup by block or item id 2021-05-11 14:44:22 +05:00
jay
3c9d62441f feat(informer): add block info based on player's relative position
Relative position includes simple words like "feet", "standing", "head", etc
2021-05-11 10:39:53 +05:00
jay
665102e19c fix(informer): 🚑 remove extra let cfg declaration left over from refactor 2021-05-11 09:46:07 +05:00
jay
db459f52e6 fix(informer): ✏️ fix typo in isVec3 check function 2021-05-11 09:32:54 +05:00
jay
360eeff02f feat(informer): add item info based on player
Change the parameter order of `item` function to accomodate this
2021-05-11 07:32:05 +05:00
jay
25faac2f4c feat(informer): add command me for block info based on player position 2021-05-10 18:37:48 +05:00
jay
ccf0c598e8 feat(informer): add block info based on player position or manually provided coords
This paves the way to get block info based on positions relative from the player
2021-05-07 16:27:44 +05:00
jay
050e2b3bd9 feat(informer): 🚸 add sub-command for module level quiet 2021-05-07 12:53:24 +05:00
jay
e879d1f4ad refactor(informer): ♻️ reorder config loading 2021-05-07 12:40:17 +05:00
jay
df193da779 feat(command): 🚧 add stub for bot._client events
Not sure how these are different:
- `bot._client.on("chat")`
- `bot.on("message(str)")`
2021-05-06 13:15:38 +05:00
jay
269d258763 feat(command): use a simpler text based system for receiving server commands 2021-05-06 13:11:31 +05:00
jay
cbb105fe49 fix(command): 🚸 make bot address regex (prefix) more lenient
Should also accept server commands now on address
2021-05-03 07:20:33 +05:00
jay
356f83e39b fix(command): 👽 fix whisper event api change with an extra parameter
Whisper also now includes who it was whispered to
2021-05-03 07:14:45 +05:00
jay
5b3804893b feat(command): fix to show web (including extra [MODE]) messages
There's no way to remove `bot.addChatPattern`, so only does once on load
2021-04-29 08:37:46 +05:00
jay
9f6fea2423 feat(command): 🔊 add system (non-chat) messages to console log
This includes anything that doesn't return a `.text`.
Such as an `extra`, `translate` or other `json` formatted message.
Ex:
- afk
- errors
- player leave and joins
2021-04-29 08:24:51 +05:00
jay
3686bab167 fix: 🐛 fix plugin reload for multi-file/dir based plugins
Now, it also (un-)loads the whole plugin (sub and master).

This appears to be the most stable option.
Loading behaviour is unchanged from before (load only top level plugin).
However as a precaution, loading tries to unload bad plugins on error.
This helps to not end up in a bad state during reload.
2021-04-29 08:14:40 +05:00
jay
e0c477a46f feat: 🔨 make fs.watch recursive, so it reloadplugin works also on directories 2021-04-27 11:47:25 +05:00
jay
6046123074 build: ⬆️ update deps 2021-04-25 07:13:55 +05:00
jay
ae1f7cf269 feat: 🔧 extract the hardcoded admin and mod list out into environment variables 2021-04-23 06:07:29 +05:00
jay
afd2e002df feat(mover): add new pathfinder Movements
- safeMove without parkour
- aggresiveMove that digs
2021-04-19 13:34:22 +05:00
jay
3c5ec6b360 fix(mover): ✏️ fix !go up moving left instead 2021-04-19 13:31:36 +05:00
jay
b71728e503 feat(mover): add Mule to ridable mobs 2021-04-19 13:25:04 +05:00
jay
d001280383 fix: 🥅 fix a plugin loading error
While reloading, plugin cache may not be deleted.
This happens when a loaded plugin encounters an error during reload.
This causes a deadlock in the unloading code.

So make sure cache is always deleted:
- ignore when an `unload()` function doesn't exist
- delete cache even if there are no `exports`
2021-04-19 13:15:25 +05:00
jay
f50e388c39 feat: 🔊 fix path in plugin reload log so that it actually works 2021-04-14 09:59:48 +05:00
jay
135ce6567b fix(mover): 🐛 attempt to fix move while riding vehicle
Try to give a larger range and more freedom to see if other values work.
Specifically, attempt turning.
No test on vehicles or mobs appear to behave consistently so far.
Tried: pigs, minecarts, horse.
2021-04-14 09:57:36 +05:00
jay
e5803eee59 fix(mover): ✏️ fix missed variable during rename 2021-04-11 23:30:34 +05:00
jay
1b21fcb096 refactor(mover): ♻️ use a better name for default move 2021-04-07 07:02:40 +05:00
jay
4b3e58ceb3 fix(mover): 🐛 attempt to fix move while riding vehicle
This never worked previously.
Using one of `-1 0 1` instead of a larger number appears to work.
2021-04-07 06:42:11 +05:00
jay
8e854a0a2f refactor(mover): 🔊 improve debug logging code 2021-04-07 06:37:26 +05:00
jay
c42a3a2304 feat(mover): add an action to move / run away from an entity or goal
based on `pathfinder` goal `GoalInvert`; behaves slightly strange
2021-04-07 06:26:16 +05:00
jay
b453b7d6bd feat(mover): add shortcuts and aliases for the jump command 2021-04-05 18:16:13 +05:00
jay
6b79f1fc60 refactor(plugins): ♻️ change plugin load order 2021-04-05 17:47:06 +05:00
jay
6e1ef5aada refactor(statemachine): 🔇 remove debug
code
2021-04-05 17:43:48 +05:00
jay
e2ae7e5ad2 fix(statemachine): ✏️ fix SM action function sending Lower instead of Upper case commands 2021-04-03 15:16:46 +05:00
jay
42138a421b fix(statemachine): 🥅 fix for lookatPlayer when no or invalid player
Fixes the case when player isn't nearby or entity is invalid due to teleport
2021-04-03 15:15:08 +05:00
jay
aded1e4193 style: 🎨 fix crlf -> lf for real this time, using .gitattributes
Add a .gitattributes file to permanentyl fix crlf issues.

For explanantion:
https://adaptivepatchwork.com/2012/03/01/mind-the-end-of-your-line/#the-new-system
2021-03-24 15:07:45 +05:00
jay
60394e38eb refactor: ♻️ replace add with vec3.offset
This avoids having to import and creat a `new vec3.v()`
2021-03-24 00:40:54 +05:00
jay
22490f7ec1 fix(command): 🐛 fix bot address regex returning undefined
Waits till spawn to make sure `bot.username` is definitely initialized
2021-03-23 23:10:58 +05:00
jay
72c4622091 feat(informer): various improvements and fixes
Feat:
- Letter aliases for subcommands.
- Most recent command when no input.
- Start player relative info stub.

Fix:
- Player username not shown.
2021-03-23 15:14:13 +05:00
jay
0757776d8b feat(command): 🎨 make botaddress prefix and regex more flexible and configurable 2021-03-23 14:39:07 +05:00
jay
a0ffaf1654 build(typescript): 🚨 add @types dep to satisfy typescript errors
Typescript linter had an error that `require` was undefined.
This happened after updating Nodejs from 14.15 to 14.16.
Not sure if this should be a global dep or local
2021-03-23 13:25:26 +05:00
jay
1597acca72 fix(eater): 🥅 catch async error on full food
Attempt to fix async error returned by `bot.consume`.
Fixed by wrapping in a `try{}` block and using `.catch`.
Still don't know why or how this works 🤷.
2021-03-22 17:32:56 +05:00
jay
1e82045221 build: ⬆️ update deps 2021-03-22 15:59:22 +05:00
jay
33c4fc0c77 ci: 🔧 add vscode conventional commits scopes 2021-03-22 14:51:02 +05:00
jay
33c4233223 feat(statemachine): 🚧 first draft of new xstate based statemachine implementation
Replaces the old statemachine.
Done so far:
- Basic command interface
- Machine saving and loading
- Sample dummy machine
2021-03-22 14:32:08 +05:00
jay
3f3ebbae10 build: ⬆️ update deps 2021-03-07 12:10:14 +05:00
jay
fd0e1e1347 build(statemachine): add xstate for state machines 2021-01-30 23:08:20 +05:00
jay
2f88eedce9 fix(mover): lessen move duration for more precise control 2021-01-28 03:52:12 +05:00
jay
a0893f2b29 feat(mover): allow riding specific entity by name 2021-01-28 02:50:08 +05:00
jay
b1a592dbbd fix(mover): 🐛 workaround for vehicle not being removed in api on dismount
`bot.vehicle` isn't removed on dismount, so this done manually
2021-01-28 02:24:59 +05:00
jay
f336e3d736 feat(mover): switch between moving and riding automatically for manual movement 2021-01-28 02:18:42 +05:00
jay
d953bd4cf6 fix(mover): 🎨 fix quiet not being "followed" 2021-01-28 02:10:52 +05:00
jay
fc43985337 fix(mover): 🐛 fix crash when no mobs nearby while searching for rides 2021-01-28 01:28:37 +05:00
jay
6b1157147d feat(mover): implement more featureful and robust ride command
Now does the following:
- finds entities (both vehicles and animals) which are suitable for riding.
- moves to get into range.

However:
- while in a vehicle, pathfinder doesn't appear to detec coords.
- `bot.moveVehicle` doesn't work, so bot doesn't move when riding
2021-01-28 00:48:45 +05:00
jay
e6d29576e7 refactor(command): 🚚 move vehicle commands near other moves
Move the following:
- vehicle commands next to other movement related commands like `go`.
- move `ride` / `mount` command functionality into mover plugin
2021-01-26 22:49:23 +05:00
jay
974d460061 feat(mover): add basic movement commands
Forward, left, etc. Uses bot.controlState.
But left and right does the opposite of expectations.
2021-01-18 15:13:53 +05:00
jay
1d361e04a6 build: ⬆️ update deps 2021-01-18 01:54:05 +05:00
jay
7597620626 ci: 🔧 add vscode conventional commits scopes 2021-01-18 01:47:32 +05:00
jay
e5faa6f022 feat(informer): add more detail to item info
Gives detail of traversing the item's nbt data
2021-01-18 01:24:47 +05:00
jay
cc18ac5c2e refactor(mover): 🚚 move commands inside mover plugin itself
This reduces the code inside the command plugin.
Most of the logic was only relevant to moving anyway.
Command aliases like `come` and `follow` still remain in command plugin.
2021-01-17 23:59:39 +05:00
jay
7050a1621b fix(informer): 🐛 add missing name for entity info when entity is a player
Uses `username` when `entity.name` is missing; should check other cases
2021-01-17 18:32:05 +05:00
jay
7cbfa16476 feat(informer): add case for when sub command is passed a single param
Currently returns:
- Item at given slot number
- Entity that matches given name
2021-01-17 16:20:20 +05:00
jay
63849e0729 fix(informer): 🥅 catch and report when objects are missing 2021-01-17 16:16:41 +05:00
jay
4e7f8d59fd feat(informer): add more detailed block metadata info
Uses `block.getProperties()`. Thanks to a [comment][1]

[1]: https://github.com/PrismarineJS/mineflayer-pathfinder/pull/84/files#r541196424
2021-01-17 15:54:31 +05:00
jay
7b2b936f81 feat(mover): implement moveY (vertical move up or down)
Doesn't appear to be working properly, bot assumes XZ is goal reached
2021-01-17 13:56:18 +05:00
jay
8a39596b1d feat(informer): add info for block at given position 2021-01-17 13:02:05 +05:00
jay
2601b7cfb1 fix(informer): 🥅 fix crash for block info when no block or an empty block is found 2021-01-17 12:20:49 +05:00
jay
67932b2f6a fix(sleeper): 🥅 catch sleeping edge case errors
Happens when trying to sleep while previously unable to move.
Or maybe when trying to sleep during dawn.
2021-01-16 16:39:42 +05:00
jay
9a6e684b11 feat(informer): add info about nearest entities 2021-01-16 16:17:42 +05:00
jay
3488a94233 feat(informer): info about held item(s) 2021-01-16 14:51:49 +05:00
jay
4d21327086 fix(mover): 🚸 better messages 2021-01-16 13:57:21 +05:00
jay
e74d796124 fix(command): 🐛 make follow command work again without params 2021-01-16 13:50:35 +05:00
jay
3d5ffe38cd feat(mover): inform when goal reached 2021-01-16 13:25:26 +05:00
jay
b519913355 feat(mover): implement moving to X Z goal (without y) 2021-01-15 00:36:17 +05:00
jay
b1dab1968c fix(command): ✏️ fix follow command not working 2021-01-05 11:34:49 +05:00
jay
3219ec6155 feat(informer): actually add a command for info plugin 2021-01-05 11:32:33 +05:00
jay
f38ad8c819 fix(command): 🚧 fix and workaround bot look at this
Workaround being unable to look at what player is looking at.
Currently looks at what player is standing on.
2021-01-05 09:30:14 +05:00
jay
4b8a39d38c feat(informer): add optional metadata to block info display 2021-01-05 09:20:35 +05:00
jay
6b71de0356 refactor(informer): 🚚 proper name to block info function
Be more specific: `block` instead of `info`.
In anticipation of future functions in this module.
2021-01-05 08:52:38 +05:00
jay
96214ffe37 fix(informer): 🐛 use block.type instead of block.id
`block.id` is nonexistent when tested live
2021-01-05 08:32:30 +05:00
jay
034f8d331a fix(informer): 🐛 convert array to string for chat
`bot.chat` only supports strings
2021-01-05 08:18:21 +05:00
jay
69d0f5830d feat(mover): add close(er) command to follow / come closer 2021-01-05 08:11:11 +05:00
jay
8e719d5ccf feat(sleeper): add and adjust functionality to properly sleep
Can now use beds in inventory but with a hacky block placing workaround.
`findBlock` returns a null position.
So it uses the closest adjacent block and assumes it'll work.

Bot also is more robust at sleeping,
2020-12-27 05:50:16 +05:00
jay
112eb04a8d refactor(sleeper): ♻️ simplify and make sleeping code more robust 2020-12-27 01:58:17 +05:00
jay
ba7c53be0c refactor(command): ♻️ make toss exit early if non-existent block 2020-12-27 00:30:46 +05:00
jay
5b4718fa5d fix(command): 🐛 add missing return 2020-12-25 07:29:06 +05:00
jay
902732c6dd chore: ⬆️ update deps 2020-12-25 07:20:05 +05:00
jay
65d13a3379 style: fix crlf -> lf 2020-12-25 07:17:54 +05:00
jay
288b7045b6 feat: add informer plugin to show info
Data and information plugin.
Expose methods in mcData with acceptable ux.
Also central place for all kinds of debug features.
2020-12-24 21:39:50 +05:00
jay
94574a4296 refactor: ♻️ reorder plugin loading
Load in order of simplicity and dependance.

TODO: Use `bot.waitForChunksToLoad`:
Split plugins based on whether it requires blocks to be loaded or no.
2020-12-24 21:32:08 +05:00
jay
68e60921b1 refactor(mover): 🔥 remove unused code + comments, minor fixes
Refactoring and fixing code.
 No major functionality change.
 Probably will have less bugs.
 - carry over how mcData is loaded
 - stop bot on unload
 - fix wrong magma block name
2020-12-24 19:57:45 +05:00
jay
f2281a7cb3 feat(command): temp. move inventory chat handling to command
This is temporary, and only the `toss` function.
Old functionality is still intact.
New code will eventually move back as a subcommand to replace the old.
2020-12-24 19:41:52 +05:00
jay
e9f2080556 refactor(command): ♻️ refactor how mcData is loaded
`mcData` is now directly put on bot and loaded once.
It can be accessed from anywhere `bot` is accessible.
2020-12-24 17:51:44 +05:00
jay
086251bce6 feat: add a basic feature to rejoin on server restart 2020-12-24 13:38:16 +05:00
jay
0ae961521f feat(command): expand follow to include rest of the !go follow sub commands 2020-12-24 11:57:23 +05:00
jay
eae4e95803 Merge branch 'nogameplay' into master 2020-12-24 11:28:22 +05:00
jay
fb066ee8a5 refactor: ♻️ use array in pathfinder movements list 2020-12-24 11:26:19 +05:00
jay
f4445749e6 fix: ✏️ add missing plugin name 2020-12-24 11:23:16 +05:00
jay
bf45a53f08 refactor: remove gameplay
temporarily remove gameplay until it is fixed and more stable

miner plugin won't work now
2020-12-24 10:40:46 +05:00
jay
a7ccb08d43 refactor: remove sleeper's prismarine-gameplay dependence 2020-12-24 10:35:01 +05:00
jay
8e4eb7748f style: fix crlf to lf 2020-12-24 09:33:08 +05:00
jay
47a944fe2a fix: 🐛 compat: don't use ?. for compat with older node.js 2020-12-24 09:19:32 +05:00
jay
1a3c345017 chore: 🙈 ignore .env 2020-12-24 09:16:11 +05:00
jay
de0af4d2ac feat: 🚀 add and use dotenv-packed
Use dotenv for more convenient credential management
2020-12-24 08:52:50 +05:00
jay
73d5f43ad3 feat: actually enable mover and eater plugins 2020-12-23 13:28:34 +05:00
jay
b970231519 feat: add mover plugin
General purpose mover / goto plugin.
Replaces the old solver based `comehere` in miner.js.
Instead, directly based on mineflayer bot apis and pathfinder.
This makes it simpler and easier to debug.
While less general, pathfinder is sophisticated enough for most cases.

For anything that needs moving from point A to point B.
Such as:
- following
- go to a location

Not in scope: locations and places. Would be a separate plugin.
2020-12-22 15:38:45 +05:00
jay
787d08dd31 feat: add automatic eater plugin 2020-12-22 15:29:06 +05:00
14 changed files with 1681 additions and 419 deletions

16
.gitattributes vendored Normal file
View 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

58
.gitignore vendored
View File

@@ -1,29 +1,31 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
/node_modules /node_modules
/.pnp /.pnp
.pnp.js .pnp.js
# testing # testing
/coverage /coverage
# production # production
/build /build
/data
# misc
.DS_Store # misc
.env.local .DS_Store
.env.development.local .env
.env.test.local .env.local
.env.production.local .env.development.local
/lib/**/*.old .env.test.local
/lib/**/*.bak .env.production.local
/lib/**/*.old
npm-debug.log* /lib/**/*.bak
yarn-debug.log*
yarn-error.log* npm-debug.log*
yarn-debug.log*
# Editor yarn-error.log*
*.swp
# Editor
*.swp
*.swo *.swo

56
.vscode/launch.json vendored
View File

@@ -1,29 +1,29 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "protospace", "name": "protospace",
"skipFiles": [ "skipFiles": [
"<node_internals>/**" "<node_internals>/**"
], ],
"program": "${workspaceFolder}/lib/index.js", "program": "${workspaceFolder}/lib/index.js",
"args": ["games.protospace.ca"] "args": ["games.protospace.ca"]
}, },
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Launch Program", "name": "Launch Program",
"skipFiles": [ "skipFiles": [
"<node_internals>/**" "<node_internals>/**"
], ],
"program": "${workspaceFolder}/lib/index.js", "program": "${workspaceFolder}/lib/index.js",
// port may need to be changed for each session // port may need to be changed for each session
"args": ["localhost", "56901"] "args": ["localhost", "56901"]
} }
] ]
} }

10
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,10 @@
{
"conventionalCommits.scopes": [
"command",
"mover",
"sleeper",
"informer",
"statemachine",
"builder"
]
}

View File

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

View File

@@ -1,5 +1,5 @@
const v = require('vec3') const v = require('vec3')
let mcData = require('minecraft-data') let mcData
let cfg = {} let cfg = {}
let bot = {} let bot = {}
@@ -27,14 +27,13 @@ function checkItemExists(name) {
} }
const events = { const events = {
whisper: function command_whisper(username, message) { whisper: function command_whisper(username, _botusername, message, ...history) {
if ([bot.username, "me"].includes(username)) return if ([bot.username, "me"].includes(username)) return // whisper back from server (afk msges, etc)
if (/^gossip/.test(message)) return if (/^gossip/.test(message)) return
// if (/^!/.test(message) && username === cfg.admin){
if (username === cfg.admin) { if (username === cfg.admin) {
console.info("whispered command", _botusername, message)
message = message.replace("\\", "@") message = message.replace("\\", "@")
console.info("whispered command", message) if (message.startsWith(cfg.botAddressPrefix)) {
if (/^!/.test(message)) {
command(username, message) command(username, message)
} else { } else {
bot.chat(message) bot.chat(message)
@@ -42,16 +41,53 @@ const events = {
} }
} else { } else {
bot.whisper(cfg.admin, `gossip ${username}: ${message}`) bot.whisper(cfg.admin, `gossip ${username}: ${message}`)
console.info(username, "whispered", message) console.info(username, "whispered", _botusername, message)
} }
} }
, chat: command , chat: command
, kicked: (reason, loggedIn) => console.warn(reason, loggedIn) , kicked: function rejoin(reason, loggedIn) {
console.warn(reason, loggedIn && "logged_in")
if (reason.extra && reason.extra[0].text === "Server closed") {
bot.quit()
bot.end()
// TODO implement all startup features (maybe refactor all into a single function / module?)
setTimeout((bot, cfg) => {
bot = mineflayer.createBot(cfg.botOptions)
}, 15 * 60 * 1000, bot, cfg);
}
}
// , message: systemMessage
, messagestr: function systemMessageStr(...args) { console.log("cmd msgstr:", ...args) }
, "chat:web": commandWeb
}
function systemMessage(...args) {
if (args[0]?.text) return
const metadata = (args[0]?.extra || args[0]?.with)?.map(v => v.text)
console.log(
"cmd msg:",
args[0]?.text || args[0]?.translate,
args[0]?.extra?.length || args[0]?.with?.length || Object.keys(args[0]?.json).length - 1,
metadata.length > 0 && metadata || args[0],
args.slice(1)
)
}
function commandWeb([[mode, username, message]]) {
// console.log("web msg:", mode, username, message)
message && command(username, message)
}
function _clientSystemMessage(...args) {
console.log("cmd chat:", args[0]?.message, args[0]?.extra?.length, args[0]?.extra, args[0], args.slice(1))
} }
const events_registered = [] const events_registered = []
function command(username, message) { function command(username, message) {
// TODO better name, maybe an array?
cfg.botAddressPrefix = cfg.botAddressPrefix || "!"
function fuzzyRespond(responses, probability = 1, timeout = 1) { function fuzzyRespond(responses, probability = 1, timeout = 1) {
if (Math.random() < probability) { if (Math.random() < probability) {
const response = responses[Math.floor(Math.random() * responses.length)] const response = responses[Math.floor(Math.random() * responses.length)]
@@ -66,7 +102,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 const player = bot.players[username] ? bot.players[username].entity : null
@@ -78,11 +114,12 @@ function command(username, message) {
} }
if (message.startsWith("!") || cfg.botAddress.test(message)) { if (message.startsWith(cfg.botAddressPrefix) || cfg.botAddressRegex.test(message)) {
message = cfg.botAddress.test(message) ? cfg.botAddress.exec(message)[1] : message message = cfg.botAddressRegex.test(message) ? cfg.botAddressRegex.exec(message)[1] : message
console.log(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 // TODO command dispatchEvent, for aliases
function subcommand(message) { function subcommand(message) {
const message_parts = message.split(/\s+/) const message_parts = message.split(/\s+/)
@@ -183,105 +220,45 @@ function command(username, message) {
break; break;
case "follow": case "follow":
subcommand("go follow me") switch (message_parts.length) {
break; case 1:
case "come": subcommand("go follow me")
subcommand("go follow once") break;
break;
case "move":
case "go":
// TODO move most of the subcommands into mover.js?
const message_parts2 = message_parts.slice(2)
switch (message_parts[1]) {
case "init":
cfg.plugins.mover.initMoves()
break
case "near":
// message_parts2 = message_parts.slice(2)
switch (message_parts2.length) {
case 0:
cfg.plugins.mover.moveNear(bot.nearestEntity().position)
break
case 1:
switch (message_parts2[0]) {
case "me":
if (player) {
cfg.plugins.mover.moveNear(player.position)
} else {
cfg.quiet || bot.chat("can't see you")
}
break;
default:
const aPlayer = bot.players[message_parts[2]] ? bot.players[message_parts[2]].entity : null
if (aPlayer) {
cfg.plugins.mover.moveNear(aPlayer.position)
} else {
cfg.quiet || bot.chat(`can't see ${message_parts[2]}`)
}
break;
}
break
case 2:
todo()
// bot.lookAt({}) goalxz?
break
case 3:
//TODO more checks
cfg.plugins.mover.moveNear(message_parts2)
break
default:
break
}
break
case "follow":
// message_parts2 = message_parts.slice(2)
switch (message_parts2.length) {
case 0:
cfg.plugins.mover.follow(bot.nearestEntity())
break
case 1:
switch (message_parts2[0]) {
case "me":
case "once":
if (player) {
cfg.plugins.mover.follow(player, message_parts2[0] !== "once")
} else {
cfg.quiet || bot.chat("can't see you")
}
break;
default:
const aPlayer = bot.players[message_parts[2]] ? bot.players[message_parts[2]].entity : null
if (aPlayer) {
cfg.plugins.mover.follow(aPlayer)
} else {
cfg.quiet || bot.chat(`can't see ${message_parts[2]}`)
}
break;
}
break
// case 2:
// bot.lookAt({}) goalxz?
// break
// case 3:
//TODO more checks
// cfg.plugins.mover.moveNear(message_parts2)
// break
default:
todo()
break
}
break
case "stop":
cfg.plugins.mover.stop()
break
default: default:
return todo() subcommand("go " + message)
break; break;
} }
break; break;
case "come":
switch (message_parts[1]) {
case "close":
case "closer":
subcommand("go follow close")
break
case "up":
case "down":
cfg.plugins.mover.moveY(player.position)
break
default:
subcommand("go follow once")
}
break;
case "ride":
case "mount":
cfg.plugins.mover.command(message_parts)
break
case "unride":
case "getoff":
case "unmount":
case "dismount":
bot.dismount()
bot.vehicle = void 0
break
case "move":
case "go":
cfg.plugins.mover.command(message_parts.slice(1), player)
break;
case "attack": case "attack":
case "rage": case "rage":
@@ -331,6 +308,8 @@ function command(username, message) {
break; break;
} }
break break
// TODO move look (and maybe find) to informer plugin?
case "look":
case "lookat": case "lookat":
// const coords = v(message_parts.splice(1)) // const coords = v(message_parts.splice(1))
switch (message_parts.length) { switch (message_parts.length) {
@@ -341,7 +320,7 @@ function command(username, message) {
switch (message_parts[1]) { switch (message_parts[1]) {
case "me": case "me":
if (player) { if (player) {
bot.lookAt((new v.Vec3(0, 1, 0)).add(player.position)) bot.lookAt(player.position.offset(0, 1, 0))
} else { } else {
cfg.quiet || bot.chat("can't see you") cfg.quiet || bot.chat("can't see you")
} }
@@ -349,17 +328,16 @@ function command(username, message) {
case "this": case "this":
// TODO lookat the block the user is looking at // TODO lookat the block the user is looking at
// Currently looks player position
if (player) { if (player) {
bot.lookAt((new v.Vec3(0, 1, 0)).add(player.position)) bot.lookAt(player.position)
todo()
} else { } else {
cfg.quiet || bot.chat("can't see you") cfg.quiet || bot.chat("can't see you")
} }
break;
break break
default: default:
const aPlayer = bot.players[message_parts[2]] ? bot.players[message_parts[2]].entity : null 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;
} }
break break
@@ -375,17 +353,8 @@ function command(username, message) {
break break
} }
break break
case "ride": case "info":
case "mount": cfg.plugins.informer.command(message_parts.splice(1), player)
bot.mount(bot.nearestEntity())
break
case "getoff":
case "unmount":
case "dismount":
bot.dismount()
break
case "go":
bot.moveVehicle(0, 10)
break break
// case "use": // case "use":
// bot.useOn(bot.nearestEntity()) // bot.useOn(bot.nearestEntity())
@@ -416,29 +385,62 @@ function command(username, message) {
// case "take": // case "take":
// // TODO take only what's requested, then throw all the rest // // TODO take only what's requested, then throw all the rest
// // TODO take all // // TODO take all
// case "toss":
// case "drop": // TODO move subcommands to cfg.plugins.inventory.itemByName
// if (!message_parts[1]) { return false } // FIXME, works but ugly case "toss":
// if (!checkItemExists(message_parts[1])) { return false } case "drop":
// switch (message_parts.length) { if (!message_parts[1]) { return false } // FIXME, works but ugly
// case 2: if (!mcData.findItemOrBlockByName(message_parts[1])) {
// bot.toss(mcData.blocksByName[message_parts[1]].id) console.log("doesn't exist:", message_parts[1])
// break cfg.quiet || bot.chat(`item doesn't exist: ${message_parts[1]}`)
// case 3: return false
// bot.tossStack( }
// mcData.itemsByName[message_parts[1]].id, const item = cfg.plugins.inventory.itemByName(message_parts[1])
// (err) => { if (!item) {
// if (err) { console.log("don't have:", message_parts[1])
// console.log(err) cfg.quiet || bot.chat(`don't have item: ${message_parts[1]}`)
// bot.chat(err) return false
// } }
// } switch (message_parts.length) {
// ) case 2:
// break bot.tossStack(
// default: item,
// break (err) => {
// } if (err) {
// break; console.error(err)
cfg.quiet || bot.chat(err.message)
}
}
)
break
case 3:
const amount = parseInt(message_parts[2])
bot.toss(
item.type,
null, //metadata
amount,
(err) => {
if (err) {
console.error(err)
cfg.quiet || bot.chat(err.message)
}
}
)
break
default:
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": case "location":
// TODO put in /lib/location // TODO put in /lib/location
switch (message_parts[1]) { switch (message_parts[1]) {
@@ -496,7 +498,7 @@ function command(username, message) {
case "howdy": case "howdy":
case "heyo": case "heyo":
case "yo": 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 // TODO sneak
// function swingArm() { // function swingArm() {
@@ -537,13 +539,14 @@ const load = (config) => {
cfg = config cfg = config
bot = cfg.bot bot = cfg.bot
mcData = mcData(bot.version) mcData = bot.mcData || (bot.mcData = require('minecraft-data')(bot.version))
for (const [key, fn] of Object.entries(events)) { for (const [key, fn] of Object.entries(events)) {
events_registered.push( events_registered.push(
bot.on(key, fn) bot.on(key, fn)
) )
} }
mcData = require('minecraft-data')(bot.version)
// bot._client.on("chat", _clientSystemMessage)
} }
const unload = () => { const unload = () => {
@@ -553,6 +556,8 @@ const unload = () => {
events_registered.shift() events_registered.shift()
} }
console.log("events_registered:", bot._eventsCount) console.log("events_registered:", bot._eventsCount)
// bot._client.off("chat", _clientSystemMessage)
} }
module.exports = { load, unload, events_registered, command } module.exports = { load, unload, events_registered, command }

109
lib/plugins/eater.js Normal file
View File

@@ -0,0 +1,109 @@
let cfg = {}
let bot = {}
let isEating = false
function eat(callback = e => e && console.error(e)) {
isEating = true
const foodNames = require('minecraft-data')(bot.version).foodsArray.map((item) => item.name)
let available_food = bot.inventory
.items()
.filter((item) => foodNames.includes(item.name))
if (available_food.length === 0 || !available_food) {
isEating = false
return callback(new Error('No food found.'))
}
if (cfg.eat.bannedFood.length > 0) {
available_food = available_food.filter(
(item) => !cfg.eat.bannedFood.includes(item.name)
)
}
let priority = cfg.eat.priority
let best_food = available_food.reduce((prev, current) => (prev[priority] > current[priority]) ? prev : current)
if (!best_food) {
isEating = false
return callback(new Error('No best food has been found.'))
}
bot.emit('eat_start')
bot.equip(best_food, 'hand', function (error) {
if (error) {
console.warn(error, best_food)
isEating = false
bot.emit('eat_stop')
} else {
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')
})
} catch { }
if (bot.food !== 20) eat(callback)
}
})
}
function checkFood() {
console.info("eater: "
// , " status: ", !isEating
, cfg.eat.auto && "auto"
, bot.food < cfg.eat.startAt && "hungry"
, "hunger:", bot.food
, "at:", cfg.eat.startAt)
if (
!isEating
&& cfg.eat.auto
&& bot.food < cfg.eat.startAt
) {
if (
(bot.pathfinder
&& !(bot.pathfinder.isMining() || bot.pathfinder.isBuilding())
// TODO implement better idle state
) || true // idle most likely
) {
eat()
}
}
}
function resetEat(value) {
// to prevent the plugin from breaking if the bot gets killed while eating btw
isEating = !!value // false
}
const load = (config) => {
cfg = config
bot = cfg.bot
cfg.eat = {
priority: 'saturation', //'foodPoints', //
// startAt: 19, //anarchy
// startAt: 18,
startAt: 14,
bannedFood: [
"enchanted_golden_apple", "golden_apple", "pufferfish", "chorus_fruit"
],
auto: true
}
bot.on('health', checkFood)
bot.on('spawn', resetEat)
}
const unload = () => {
bot.off('health', checkFood)
bot.off('spawn', resetEat)
}
module.exports = { load, unload, eat, resetEat }

View File

@@ -71,9 +71,7 @@ function lookForMobs() {
bot.pvp.attack(entityEnemy) bot.pvp.attack(entityEnemy)
} else if (entityEnemy) { } else if (entityEnemy) {
bot.lookAt( bot.lookAt(
// (new v.Vec3(0, 1, 0)).add(
entityEnemy.position entityEnemy.position
// )
) )
cfg.quiet || bot.chat("AH! A creeper! They creep me out!") cfg.quiet || bot.chat("AH! A creeper! They creep me out!")
} }

342
lib/plugins/informer.js Normal file
View File

@@ -0,0 +1,342 @@
let bot
let mcData
let quiet
// import v from 'vec3'
const v = require('vec3')
let cfg = {
info: {
quiet: quiet,
recentCommand: null,
},
// to satisfy typescript
quiet: null,
bot: bot
}
var RelativePosEnum
(function (RelativePosEnum) {
RelativePosEnum[RelativePosEnum["feet"] = 0] = "feet"
RelativePosEnum[RelativePosEnum["standing"] = 1] = "standing"
RelativePosEnum[RelativePosEnum["head"] = 2] = "head"
RelativePosEnum[RelativePosEnum["looking"] = 3] = "looking"
RelativePosEnum[RelativePosEnum["infront"] = 4] = "infront"
RelativePosEnum[RelativePosEnum["behind"] = 5] = "behind"
})(RelativePosEnum || (RelativePosEnum = {}))
function relPosToBlock(entity, relPos) {
let pos
if (!(relPos in RelativePosEnum)) return console.warn("info: not a relative position:", relPos)
relPos = typeof relPos === "number" ? RelativePosEnum[relPos] : relPos
switch (relPos) {
case "feet":
pos = entity.position;
break
case "standing":
pos = entity.position.offset(0, -1, 0)
break
// todo use CARDINAL directions api from pathfinder
// case "behind":
// entity.yaw
// pos = entity.position
// break
// case "front":
// case "infront":
// pos = entity.position
// break
case "head":
pos = entity.position.offset(0, 1.85, 0)
break
case "looking":
if (entity === bot.entity) {
return bot.blockAtCursor()
// return bot.blockInSight(128, 128)
}
default:
quiet || bot.chat(`info: pos '${relPos}' not implemented`)
break
}
if (pos) {
// nearest block
return bot.blockAt(pos)
}
}
function isVec3(vec) {
return vec?.length === 3 || vec.x && vec.y && vec.z
}
function block(entity = bot.entity, pos = entity?.position?.offset(0, -1, 0)) {
console.assert(pos || entity)
const block = isVec3(pos) ? bot.blockAt(v(pos))
: typeof pos === "string" ? relPosToBlock(entity, RelativePosEnum[pos]) : pos
console.log(block, block?.getProperties && block.getProperties())
if (!block) {
quiet || bot.chat("empty block")
return block
}
bot.lookAt(block?.position)
let info = [block.type, block.name]
if (block.metadata) info.push(Object.entries(block.getProperties()))
quiet || bot.chat(info.join(": "))
}
function item(
entity = bot.entity,
slot = entity.heldItem
) {
const item = typeof slot === "number" ?
bot.inventory.slots[slot + bot.QUICK_BAR_START] :
slot
console.log("info item:", item)
if (!item) {
quiet || bot.chat("no item")
return item
}
let info = [item.type, item.name, item.count]
if (item.metadata) info.push("meta: " + item.metadata.length)
if (item.nbt) {
info.push(compound_value(item.nbt))
}
quiet || bot.chat(info.join("; "))
function compound_value(obj) {
if (typeof obj.value == "object") {
return compound_value(obj.value)
} else if (obj.value) {
return obj.value
} else if (typeof obj == "object") {
const keys = Object.keys(obj)
return keys.map(key => {
return `${key}: ${compound_value(obj[key])}`
});
} else {
return obj
}
}
return item
}
var VillagerProfession
(function (VillagerProfession) {
VillagerProfession[VillagerProfession["Unemployed"] = 0] = "Unemployed"
VillagerProfession[VillagerProfession["Armourer"] = 1] = "Armourer"
VillagerProfession[VillagerProfession["Butcher"] = 2] = "Butcher"
VillagerProfession[VillagerProfession["Cartographer"] = 3] = "Cartographer"
VillagerProfession[VillagerProfession["Cleric"] = 4] = "Cleric"
VillagerProfession[VillagerProfession["Farmer"] = 5] = "Farmer"
VillagerProfession[VillagerProfession["Fisherman"] = 6] = "Fisherman"
VillagerProfession[VillagerProfession["Fletcher"] = 7] = "Fletcher"
VillagerProfession[VillagerProfession["Leatherworker"] = 8] = "Leatherworker"
VillagerProfession[VillagerProfession["Librarian"] = 9] = "Librarian"
VillagerProfession[VillagerProfession["Mason"] = 10] = "Mason"
VillagerProfession[VillagerProfession["Nitwit"] = 11] = "Nitwit"
VillagerProfession[VillagerProfession["Shepherd"] = 12] = "Shepherd"
VillagerProfession[VillagerProfession["Toolsmith"] = 13] = "Toolsmith"
VillagerProfession[VillagerProfession["Weaponsmith"] = 14] = "Weaponsmith"
})(VillagerProfession || (VillagerProfession = {}))
function entity(name = bot.nearestEntity()) {
const entity = typeof name === "string" ? (name = name.toLowerCase()) && bot.nearestEntity((entity) => {
const enames = [entity.username?.toLowerCase(), entity.name, entity.displayName?.toLowerCase()]
return enames.includes(name)
}) : name
console.log("info entity:", entity)
if (!entity) {
quiet || bot.chat("no entity")
return entity
}
entity?.position && bot.lookAt(entity.position)
let info = [(entity.entityType && entity.entityType + ":" || "") + (entity.type === "object" ? entity.kind : entity.type)]
info.push(entity.username || entity.name)
// TODO various info depending on the type of entity; player, villager, etc
// TODO refactor and combine with compound_value()
switch (entity.type) {
case "object":
switch (entity.kind) {
case "Drops":
const item = entity.metadata[7];
entity.metadata;
if (item.present) {
info.push("item: " + item.itemId + ": "
+ mcData.items[item.itemId].name
+ `x${item.itemCount}`);
}
else {
console.warn("info entity: metadata expected item, got:", entity.metadata[7]);
}
break;
default:
}
break;
case "mob":
switch (name) {
case 'villager':
const { villagerProfession, ...otherProps } = entity.metadata[17]
info.push(VillagerProfession[villagerProfession])
info.push(Object.entries(otherProps).toString())
break
default:
break
}
entity.metadata[8] && info.push("health:" + entity.metadata[8])
entity.metadata[15] && info.push("baby")
if (entity.heldItem) {
info.push("holding")
item(entity)
}
case "global":
case "orb":
case "player":
break;
case "other":
info.push("kind:" + entity.kind)
default:
}
if (entity.metadata) info.push("len: " + entity.metadata.length)
quiet || bot.chat(info.join("; "))
return entity
}
function blockOrItemFromId(num, quiet = cfg.info.quiet) {
const block = mcData?.blocks[num]
const item = mcData?.items[num]
// const entity = mcData?.entities[num]
if (block || item) {
quiet || bot.chat(
(block && `block: ${block.name}, ` || "")
+ (item && `item: ${item.name}, ` || "")
// + (entity && `entity: ${entity.name}, ` || "")
)
} else {
quiet || bot.chat("info: nonexistent block or item")
}
return { block, item }
}
function command(message_parts, player) {
if (message_parts.length > 0) {
cfg.info.recentCommand = message_parts
}
if (player === null)
player = void 0
switch (message_parts.length) {
case 0:
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()
}
break;
case 1:
switch (message_parts[0]) {
case "quiet":
cfg.info.quiet = quiet = !quiet;
quiet || bot.chat(`info: ${quiet ? "" : "not "}being quiet`);
break;
case "i":
case "item":
item()
break
case "e":
case "entity":
entity()
break
case "me":
block(player)
break
case "b":
case "block":
block()
break
default:
const num = parseInt(message_parts[0])
if (isFinite(num)) {
blockOrItemFromId(num)
} else {
quiet || bot.chat("info usage: `!info [me|i|e|b|<num>|quiet]`")
}
break;
}
break;
case 2:
switch (message_parts[0]) {
case "i":
case "item":
switch (message_parts[1]) {
case "me":
item(player)
break
default:
const slot = parseInt(message_parts[1])
slot && item(undefined, slot)
break
}
break
case "b":
case "block":
switch (message_parts[1]) {
case "me":
block(player)
break
default:
const aPlayer = bot.players[message_parts[1]]
if (message_parts[1] in RelativePosEnum) {
block(undefined, message_parts[1])
} else if (aPlayer) {
if (aPlayer.entity) {
block(aPlayer.entity)
} else {
quiet || bot.chat(`info: player ${aPlayer.username} too far!`)
}
} else {
// or entity(message_parts[1]).position
console.log(bot.players[message_parts[1]])
quiet || bot.chat("info: not yet implemented")
}
}
break
case "me":
if (message_parts[1] in RelativePosEnum) {
block(player, message_parts[1])
}
break
case "e":
case "entity":
default:
entity(message_parts[1])
break;
}
break
case 4:
switch (message_parts[0]) {
case "b":
case "block":
default:
block(undefined, message_parts.slice(1))
break;
}
break;
default:
cfg.quiet || bot.chat("info: unknown command")
break;
}
}
const load = (config) => {
config.info = cfg.info
cfg = config
bot = cfg.bot
mcData = bot.mcData || (bot.mcData = require('minecraft-data')(bot.version))
}
const unload = () => {}
module.exports = { load, unload, command, block, item, entity }

View File

@@ -225,4 +225,4 @@ const unload = () => {
bot.off('chat', inventory) bot.off('chat', inventory)
} }
module.exports = { load, unload, equipItem, craftItem } module.exports = { load, unload, equipItem, craftItem, itemByName }

383
lib/plugins/mover.js Normal file
View File

@@ -0,0 +1,383 @@
const { Movements } = require('mineflayer-pathfinder')
const v = require('vec3')
let cfg = {}
let bot = {}
// let moving
let pathfinder
let mcData
let movements = []
function initMoves(bot = bot, mcData = bot.mcData) {
if (movements.length > 0) {
bot.pathfinder.setMovements(movements.defaultMove)
return console.warn("go init: movements already initialized!", movements)
}
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]
const aggresiveMove = new Movements(bot, mcData)
//Object.create or assign?
Object.assign(aggresiveMove, normalMove)
aggresiveMove.canDig = true
movements.push(aggresiveMove)
const safeMove = new Movements(bot, mcData)
Object.assign(safeMove, normalMove)
safeMove.allowParkour = false
safeMove.canDig = false
movements.push(safeMove)
// console.info("go init: moves:", movements)
bot.pathfinder.setMovements(normalMove)
}
function moveNear(pos, distance = 3) {
const { GoalNear } = require('mineflayer-pathfinder').goals
pos = v(pos)
cfg.quiet || bot.chat(`moving to ${pos.floored()}`)
bot.pathfinder.setMovements(movements.defaultMove)
bot.pathfinder.setGoal(new GoalNear(pos.x, pos.y, pos.z, distance))
}
function moveXZ(pos) {
const { GoalXZ } = require('mineflayer-pathfinder').goals
if (Array.isArray(pos) && pos.length == 2) {
pos = v(pos[0], 0, pos[1])
}
pos = v(pos)
console.log(pos)
cfg.quiet || bot.chat(`moving to ${pos.floored()}`)
bot.pathfinder.setMovements(movements.defaultMove)
bot.pathfinder.setGoal(new GoalXZ(pos.x, pos.z))
}
function moveY(pos) {
const { GoalY } = require('mineflayer-pathfinder').goals
if (Array.isArray(pos) && pos.length == 1) {
pos = v(null, pos[0], null)
}
pos = v(pos)
console.log(pos)
cfg.quiet || bot.chat(`moving to ${pos.floored()}`)
bot.pathfinder.setMovements(movements.defaultMove)
bot.pathfinder.setGoal(new GoalY(pos.y))
}
function follow(entity, dynamic = true, distance = 3) {
console.assert(entity)
const { GoalFollow } = require('mineflayer-pathfinder').goals
// console.log(entity)
cfg.quiet || bot.chat(
`following ${entity.type
}: ${entity.username || entity.displayName
}${dynamic ? "" : " once"}`
)
entity = entity.entity ? entity.entity : entity
// console.log(entity)
bot.pathfinder.setMovements(movements.defaultMove)
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", "Mule"]
const vehicle = entity && typeof entity !== "string" ? entity : bot.nearestEntity(e => {
if (typeof entity === "string") return e.name === entity
const maybeRidableMob = e.mobType?.split(" ")
return e.kind == "Vehicles"
|| ridableMobs.includes(e.mobType)
|| maybeRidableMob && ridableMobs.includes(maybeRidableMob[maybeRidableMob.length - 1])
})
if (!vehicle) {
return cfg.quiet || bot.chat(`nothing to ride!`)
} else if ((dist = bot.entity.position.distanceSquared(vehicle.position)) > 36) {
bot.lookAt(vehicle.position)
follow(vehicle, false)
bot.once('goal_reached', ride)
return cfg.quiet || bot.chat(`${vehicle.name} bit far`)
}
console.log("vehicle:", vehicle)
bot.mount(vehicle)
}
function moveOrRide(turn = false, reverse = -1, directionLabel, message_parts2) {
// bot.once("attach", state = "vehiccel")
if (bot.vehicle) {
// FIXME moveVehicle should be +-1 or 0?
const amount = parseInt(message_parts2[0]) * -reverse
bot.moveVehicle(turn && Math.sign(amount) || 0, !turn && amount || 0)
} else {
command([directionLabel].concat(message_parts2))
}
}
function hit(blockOrEntity) {
bot.chat(`hitting ${entity.name || entity.type}`)
}
function goalReached(goal) {
console.log(goal)
const entity = goal?.entity
let entityInfo = ""
if (entity) {
entityInfo += entity.type + ": "
switch (entity.type) {
case "player":
entityInfo += entity.username
break;
default:
break;
}
}
cfg.quiet || bot.chat(`goal reached: ${entityInfo}; pos: [x:${goal?.x}, y:${goal?.y}, z:${goal?.z}]`)
}
function stop() {
bot.pathfinder.setGoal(null)
bot.stopDigging()
}
function command(message_parts, player) {
const message_parts2 = message_parts.slice(1)
switch (message_parts[0]) {
case "init":
initMoves()
break
case "near":
switch (message_parts2.length) {
case 0:
moveNear(bot.nearestEntity().position)
break
case 1:
switch (message_parts2[0]) {
case "me":
if (player) {
moveNear(player.position)
} else {
cfg.quiet || bot.chat("can't see you")
}
break;
default:
const aPlayer = bot.players[message_parts2[0]] ? bot.players[message_parts2[0]].entity : null
if (aPlayer) {
moveNear(aPlayer.position)
} else {
cfg.quiet || bot.chat(`can't see ${message_parts2[0]}`)
}
break;
}
break
case 2:
//TODO this isn't near
moveXZ(message_parts2)
break
case 3:
//TODO more checks
moveNear(message_parts2)
break
default:
break
}
break
case "follow":
// message_parts2 = message_parts.slice(2)
switch (message_parts2.length) {
case 0:
follow(bot.nearestEntity())
break
case 1:
let dist = 3
switch (message_parts2[0]) {
case "close":
dist = 1
case "me":
case "once":
if (player) {
follow(player, message_parts2[0] === "me", dist)
} else {
cfg.quiet || bot.chat("can't see you")
}
break;
default:
const aPlayer = bot.players[message_parts2[0]] ? bot.players[message_parts2[0]].entity : null
if (aPlayer) {
follow(aPlayer)
} else {
cfg.quiet || bot.chat(`can't see ${message_parts2[0]}`)
}
break;
}
break
// case 2:
// bot.lookAt({}) goalxz?
// break
// case 3:
//TODO more checks
// moveNear(message_parts2)
// break
default:
cfg.quiet || bot.chat("unknown or bad command")
break
}
break
case "ride":
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)
break
case "s":
case "b":
moveOrRide(0, 1, "back", message_parts2)
break
case "a":
case "l":
moveOrRide(1, -1, "right", message_parts2)
break
case "d":
case "r":
moveOrRide(1, 1, "left", message_parts2)
break
case "up":
case "u":
case "j":
moveOrRide(1, 1, "jump", message_parts2)
break
case "back":
case "forward":
case "jump":
case "left":
case "right":
case "sneak":
case "sprint":
console.info(bot.controlState[message_parts[0]], bot.entity.position.floored())
bot.setControlState(message_parts[0], true)
console.info(bot.controlState[message_parts[0]])
setTimeout(bot.setControlState, 100 * (message_parts[1] || 2), message_parts[0], false)
setTimeout(console.info, 5000, bot.controlState[message_parts[0]], bot.entity.position.floored())
break
case "stop":
stop()
break
default:
return cfg.quiet || bot.chat(`unknown command ${message_parts[0]}`)
}
}
const load = (config) => {
cfg = config
bot = cfg.bot
cfg.move = {
// auto: true,
canDig: false,
// list: ["hello", "wassup"],
quiet: !!cfg.quiet,
movements: []
}
mcData = bot.mcData || (bot.mcData = require('minecraft-data')(bot.version))
pathfinder = bot.pathfinder || bot.loadPlugin(require('mineflayer-pathfinder').pathfinder)
// initMoves(bot, mcData)
setTimeout(initMoves, 500, bot, mcData)
bot.on('goal_reached', goalReached)
}
const unload = () => {
stop()
bot.off('goal_reached', goalReached)
}
module.exports = {
load, unload, command,
stop, initMoves,
moveNear, moveXZ, moveY, follow,
ride
}

View File

@@ -1,74 +1,87 @@
let pathfinder let pathfinder
//TODO replace with simple pathfinder motions //TODO replace with simple pathfinder motions
const {
gameplay,
MoveTo,
MoveToInteract,
ObtainItem,
// Craft
} = require('prismarine-gameplay')
let cfg = {} let cfg = {}
let bot = {} let bot = {}
let inv let inv
// cfg.autosleep = false // cfg.autosleep = false
function sleep(quiet) { function sleep(quiet = cfg.sleep.quiet) {
quiet = quiet !== undefined ? quiet : cfg.sleep.quiet
if(bot.game.dimension !== "minecraft:overworld" || cfg.sleep.force){ if(bot.game.dimension !== "minecraft:overworld" || cfg.sleep.force){
!quiet && bot.chat("can't sleep, not in overworld now") !quiet && bot.chat("can't sleep, not in overworld now")
return return
} }
if (bot.isSleeping && !cfg.sleep.force) {
!quiet && bot.chat("already in bed!")
return
}
let bed = bot.findBlock({ let bed = bot.findBlock({
matching: block => bot.isABed(block) matching: block => bot.isABed(block)
}) })
let bedstatus = bed && bot.parseBedMetadata(bed).occupied ? "n unoccupied" : "" let bed_occupied = bed && bot.parseBedMetadata(bed).occupied
if(bed && bedstatus == "n unoccupied"){ if (bed && bed_occupied) {
bot.lookAt(bed.position) bot.lookAt(bed.position)
bed = bot.findBlock({ bed = bot.findBlock({
matching: block => bot.isABed(block) && !bot.parseBedMetadata(block).occupied matching: block => bot.isABed(block) && !bot.parseBedMetadata(block).occupied
}) || bed }) || bed
bedstatus = bot.parseBedMetadata(bed).occupied ? "n unoccupied" : "" bed_occupied = bot.parseBedMetadata(bed).occupied
} }
if (bed && bedstatus == "") { if (bed && !bed_occupied) {
bot.lookAt(bed.position) bot.lookAt(bed.position)
// const nearbed = bot.waitForChunksToLoad(() => {
bot.gameplay.solveFor( cfg.plugins.mover && cfg.plugins.mover.moveNear(bed.position, 2)
new MoveTo((bed.position.range = 2) && bed.position), (err) => { bot.once('goal_reached', (goal) => {
// new MoveTo(bed.position), (err) => { console.info(goal)
// new MoveToInteract(bed.position), (err) => { try {
if (err) { bot.sleep(bed, (err) => {
!quiet && bot.chat(`can't reach bed: ${err.message}`) if (err) {
} else { !quiet && bot.chat(`can't sleep: ${err.message}`)
bot.waitForChunksToLoad(() => { } else {
bot.sleep(bed, (err) => { !quiet && bot.chat("zzz")
if (err) { // apparently, `bot.isSleeping = true` takes a while
!quiet && bot.chat(`can't sleep: ${err.message}`) // maybe it's async
} else { console.log("sleeping? ", bot.isSleeping)
!quiet && bot.chat("zzz") }
console.log("sleeping? ", bot.isSleeping)
// hack until this is fixed
// bot.isSleeping = bot.isSleeping ? bot.isSleeping : true
bot.isSleeping = true
}
})
}) })
} catch (error) {
console.error(error)
} }
}) })
// } else if (bed){ })
} else if (inv && inv.equipItem("red_bed", "hand", true)) { } else if (bed = bot.inventory.items().filter(bot.isABed)[0]) {
// doesn't work fortunately const v = require('vec3')
// FIXME: DONT IMPLEMENT until it is detected as NOT NETHER bot.equip(bed, "hand", (err) => { if (err) console.error(err) })
bot.placeBlock() bot.waitForChunksToLoad(() => {
let refBlock =
// FIXME hack to get around findBlock returning null
bot.blockAt(bot.entity.position.offset(1, 0, 1), false)
// bot.findBlock({
// matching: (block) => {
// // if (block && block.type !== 0 && block.position) {
// if (block && block.position) {
// console.info("found", block)
// const blockAbove = bot.blockAt(block.position.offset(0, 1, 0))
// return !blockAbove || blockAbove.type === 0
// }
// // console.info("not found", block)
// return false
// }
// , maxDistance: 10
// })
console.log(refBlock)
bot.placeBlock(refBlock, new v.Vec3(0, 1, 0), console.error)
setTimeout(sleep, 3000, true)
})
} else { } else {
bot.gameplay.solveFor( // TODO: use mover
new ObtainItem("bed"), (err) => { // bot.gameplay.solveFor(
if (err) { // new ObtainItem("bed"), (err) => {
!quiet && bot.chat(`need a${bedstatus} bed: may not see if just placed`) // if (err) {
} !quiet && bot.chat(`need a${bed_occupied ? "n unoccupied" : ""} bed: may not see if just placed`)
} // }
) // }
// )
// bot.chat('/afk') // bot.chat('/afk')
} }
bot.pathfinder.movements bot.pathfinder.movements
@@ -88,7 +101,7 @@ function autoSleep() {
if (!bot.time.isDay && !cfg.sleep.timeoutFn && cfg.sleep.auto && !bot.isSleeping) { if (!bot.time.isDay && !cfg.sleep.timeoutFn && cfg.sleep.auto && !bot.isSleeping) {
sleep() sleep()
cfg.sleep.timeoutFn = setTimeout(() => { cfg.sleep.timeoutFn = null }, cfg.sleep.timeout) cfg.sleep.timeoutFn = setTimeout(() => { cfg.sleep.timeoutFn = null }, cfg.sleep.timeout)
console.log("sleeping?", bot.isSleeping, bot.time) console.log("sleeping?", bot.isSleeping, bot.time.isDay, bot.time.timeOfDay)
} }
} }
@@ -104,7 +117,6 @@ const load = (config) => {
pathfinder = bot.pathfinder || require('mineflayer-pathfinder').pathfinder pathfinder = bot.pathfinder || require('mineflayer-pathfinder').pathfinder
// bot.loadPlugin(pathfinder) // bot.loadPlugin(pathfinder)
bot.loadPlugin(gameplay)
inv = cfg.plugins["inventory"] inv = cfg.plugins["inventory"]
bot.on("time", autoSleep) bot.on("time", autoSleep)

View File

@@ -1,115 +1,479 @@
// Load your dependency plugins. // import { createMachine, interpret, InterpreterStatus } from "xstate";
const { createMachine, interpret, InterpreterStatus } = require('xstate');
const {pathfinder} = require('mineflayer-pathfinder') // import { access, mkdir, writeFile, readFile } from "fs";
// bot.loadPlugin(require('prismarine-viewer').mineflayer) const { access, mkdir, writeFile, readFile } = require('fs');
// const mineflayerViewer = require('prismarine-viewer').mineflayer // ANGRAM_PREFIX='MINECRAFT'
const { MINECRAFT_DATA_FOLDER } = process.env || require("dotenv-packed").parseEnv().parsed;
// Import required behaviors. const storage_dir = MINECRAFT_DATA_FOLDER || './data/' + "/sm/";
const { // import { createBot } from "mineflayer"
StateTransition, // let { pathfinder, Movements, goals } = require('mineflayer-pathfinder')
BotStateMachine, // let cfg
EntityFilters, // let bot = createBot({ username: 'statebot' })
BehaviorFollowEntity, let bot;
BehaviorLookAtEntity, let quiet;
BehaviorGetClosestEntity, let updateRate = 20;
NestedStateMachine, const machines = {
BehaviorIdle, list: {},
StateMachineWebserver, running: {},
} = require("mineflayer-statemachine"); };
let webserver;
// TODO chat let cfg = {
statemachine: {
webserver: null,
// quiet: true,
// wait for our bot to login. quiet: quiet,
function statemachineInit() { list: {},
cfg.botAddress = new RegExp(`^${bot.username} (!.+)`) running: {},
// This targets object is used to pass data between different states. It can be left empty. draft: null,
const targets = new Object(); recent: null,
// debug: null,
// Create our states debug: true,
const getClosestPlayer = new BehaviorGetClosestEntity( updateRate: updateRate
bot, targets, entity => EntityFilters().PlayersOnly(entity) && bot.entity.position.distanceTo(entity.position) <= 50 ); // && a.username !== bot.username); }
const followPlayer = new BehaviorFollowEntity(bot, targets); // FIXME temp variables to satisfy typescript autocomplete
const lookAtPlayer = new BehaviorLookAtEntity(bot, targets); // , quiet: null
const stay = new BehaviorIdle(); ,
bot: bot,
// Create our transitions plugins: { statemachine: null }
const transitions = [ };
// Edit your machine(s) here
// We want to start following the player immediately after finding them. function init(smName = "dummy", webserver) {
// Since getClosestPlayer finishes instantly, shouldTransition() should always return true. access(storage_dir, err => {
new StateTransition({ if (err?.code === 'ENOENT') {
parent: getClosestPlayer, mkdir(storage_dir, e => e && console.warn("sm init: create dir", e));
child: followPlayer, }
onTransition: (quiet) => quiet || bot.chat(`Hi ${targets.entity.username}!`), else if (err) {
shouldTransition: () => bot.entity.position.distanceTo(targets.entity.position) <= 50, console.warn("sm init: create dir", err);
// shouldTransition: () => getClosestPlayer.distanceToTarget() < 100 || console.info("player too far!") && false, }
}), });
// const machine = newSM(smName)
// If the distance to the player is less than two blocks, switch from the followPlayer // machine.states.idle.on.TOGGLE = "start"
// state to the lookAtPlayer state. // machine.states.start.on.TOGGLE = "idle"
new StateTransition({ const machine = createMachine({
parent: followPlayer, id: smName,
child: lookAtPlayer, initial: "idle",
// onTransition: () => console.log(targets), states: {
shouldTransition: () => followPlayer.distanceToTarget() < 2, idle: {
}), on: { TOGGLE: "start", NEXT: "start", PREV: "look", STOP: "finish" }
},
// If the distance to the player is more than two blocks, switch from the lookAtPlayer start: {
// state to the followPlayer state. on: { TOGGLE: "look", NEXT: "look", PREV: "idle" },
new StateTransition({ entry: 'lookAtPlayerOnce',
parent: lookAtPlayer, },
child: followPlayer, look: {
shouldTransition: () => lookAtPlayer.distanceToTarget() >= 5, on: { TOGGLE: "idle", NEXT: "idle", PREV: "start" },
}), // entry: ['look', 'blah']
new StateTransition({ // entry: 'lookAtPlayerOnce',
parent: lookAtPlayer, activities: 'lookAtPlayer',
child: stay, meta: { debug: true }
onTransition: () => bot.chat("ok, staying"), },
// shouldTransition: () => true, finish: {
}), type: 'final'
new StateTransition({ },
parent: stay, },
child: getClosestPlayer, on: { START: '.start', STOP: '.idle' },
// shouldTransition: () => Math.random() > 0.01, meta: { debug: true },
// shouldTransition: () => Math.random() > 0.1 && getClosestPlayer.distanceToTarget() < 2, context: { player: null, rate: updateRate },
}), }, {
]; actions: {
// action implementation
// Now we just wrap our transition list in a nested state machine layer. We want the bot lookAtPlayerOnce: (context, event) => {
// to start on the getClosestPlayer state, so we'll specify that here. const player = context?.player && (context?.player?.isValid || context?.player?.entity?.isValid)
const rootLayer = new NestedStateMachine(transitions, getClosestPlayer, stay); || bot.nearestEntity(entity => entity.type === 'player');
if (player?.position || player?.entity) {
// We can start our state machine simply by creating a new instance. context.player = player;
cfg.stateMachines.follow = new BotStateMachine(bot, rootLayer); bot.lookAt((player.entity || player).position.offset(0, 1, 0));
const webserver = new StateMachineWebserver(bot, cfg.stateMachines.follow); }
webserver.startServer(); }
},
// mineflayerViewer(bot, { port: 3000 }) activities: {
// const path = [bot.entity.position.clone()] lookAtPlayer: (context, event) => {
// bot.on('move', () => { const player = (context?.player?.isValid || context?.player?.entity?.isValid) && context?.player
// if (path[path.length - 1].distanceTo(bot.entity.position) > 1) { || bot.nearestEntity(entity => entity.type === 'player' && entity.isValid);
// path.push(bot.entity.position.clone()) // TODO check pos every event?
// bot.viewer.drawLine('path', path) 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);
}
} }
function newSM(smName = "sm_" + Object.keys(cfg.statemachine.list).length) {
const load = (config) => { smName = smName.replace(/\s+/, '_');
cfg = config if (cfg.statemachine.list[smName]) {
bot = cfg.bot console.warn("sm exists", smName);
// cfg.inventory = { quiet || bot.chat(`sm ${smName} already exists, edit or use another name instead`);
// auto: true, return;
// quiet: false }
// } const machine = createMachine({
// bot.on('chat', inventory) id: smName,
bot.loadPlugin(pathfinder) initial: "start",
// statemachineInit() // 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) {
const unload = () => { if (!machine?.id) {
// bot.off('chat', inventory) 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) {
module.exports = { load, unload } //: 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
};

View File

@@ -30,25 +30,26 @@
}, },
"homepage": "https://github.com/PrismarineJS/prismarine-template#readme", "homepage": "https://github.com/PrismarineJS/prismarine-template#readme",
"devDependencies": { "devDependencies": {
"@types/node": "^14.14.41",
"jest": "^26.6.3", "jest": "^26.6.3",
"require-self": "^0.2.3" "require-self": "^0.2.3",
"typescript": "^4.2.4"
}, },
"dependencies": { "dependencies": {
"minecraft-data": "^2.70.2", "dotenv-packed": "^1.2.2",
"mineflayer": "^2.34.0", "minecraft-data": "^2.84.0",
"mineflayer-armor-manager": "^1.3", "mineflayer": "^3.6.0",
"mineflayer-pathfinder": "^1.1", "mineflayer-armor-manager": "^1.4.1",
"mineflayer-pvp": "^1", "mineflayer-pathfinder": "^1.6.1",
"prismarine-block": "^1", "mineflayer-pvp": "^1.0.2",
"prismarine-chat": "^1", "prismarine-block": "^1.8.0",
"prismarine-chat": "^1.0.3",
"prismarine-entity": "^1.1.0", "prismarine-entity": "^1.1.0",
"prismarine-gameplay": "github:TheDudeFromCI/prismarine-gameplay#crafting", "prismarine-item": "^1.8.0",
"prismarine-item": "^1.5.0", "prismarine-nbt": "^1.5.0",
"prismarine-nbt": "^1.3", "prismarine-recipe": "^1.1.0",
"prismarine-recipe": "^1", "vec3": "^0.1.7",
"typescript": "^4", "xstate": "^4.18.0"
"vec3": "^0.1",
"dotenv-packed": "^1.2"
}, },
"files": [ "files": [
"lib/**/*" "lib/**/*"