")
+ ) {
+ const { file, line, pos } = f_line.groups;
+ let source =
+ "Your code" +
+ `:${match_line}\n${code.split("\n")[match_line - 1].trim()}\n `;
+ let code_source = "";
+ if (line < programs_length) {
+ source =
+ "In your program code: " +
+ programs.split("\n")[line - 1].trim() +
+ "\n";
+ code_source = `at line ${match_line}:${code
+ .split("\n")
+ [match_line - 1].trim()} in your code`;
+ }
+ return source + err.message + "\n" + code_source;
+ }
+ return err.message;
+ }
+});
+
+app.post("/stop", (req, res) => {
+ bot.end();
+ res.json({
+ message: "Bot stopped",
+ });
+});
+
+app.post("/pause", (req, res) => {
+ if (!bot) {
+ res.status(400).json({ error: "Bot not spawned" });
+ return;
+ }
+ bot.chat("/pause");
+ bot.waitForTicks(bot.waitTicks).then(() => {
+ res.json({ message: "Success" });
+ });
+});
+
+// Server listening to PORT 3000
+
+const DEFAULT_PORT = 3000;
+const PORT = process.argv[2] || DEFAULT_PORT;
+app.listen(PORT, () => {
+ console.log(`Server started on port ${PORT}`);
+});
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/.gitignore b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/.gitignore
new file mode 100644
index 000000000..0578fdca3
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/.gitignore
@@ -0,0 +1,107 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env.test
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+lib/
+package-lock.json
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/LICENSE b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/LICENSE
new file mode 100644
index 000000000..f2896b56e
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 TheDudeFromCI
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/README.md b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/README.md
new file mode 100644
index 000000000..555acb761
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/README.md
@@ -0,0 +1,89 @@
+mineflayer-collectblock
+A small utility plugin for allowing users to collect blocks using a higher level API.
+
+
+
+
+
+
+
+
+
+
+---
+## This is a modified version to better support Voyager
+
+## Showcase
+
+You can see a video of the plugin in action, [here.](https://youtu.be/5T_rcCnNnf4)
+The source code of the bot in the video can be seen in the examples folder, [here.](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/examples/collector.js)
+
+### Description
+
+This plugin is a wrapper for mineflayer that allows for easier API usage when collecting blocks or item drops. This plugin is designed to reduce some of the boilerplate code based around the act of pathfinding to a block _(handled by_ ***mineflayer-pathfinder***_)_, selecting the best tool to mine that block _(handled by_ ***mineflayer-tool***_)_, actually mining it, then moving to collect the item drops from that block. This plugin allows for all of that basic concept to be wrapped up into a single API function.
+
+In addition to the usage above, some additional quality of life features are available in this plugin. These include the ability to automatically deposit items into a chest when the bot's inventory is full, collecting new tools from a chest if the bot doesn't currently have a required tool _(also handled by_ ***mineflayer-tool***_)_, and allowing for queueing of multiple blocks or item drops to the collection task, so they can be processed later.
+
+### Getting Started
+
+This plugin is built using Node and can be installed using:
+```bash
+npm install --save mineflayer-collectblock
+```
+
+### Simple Bot
+
+The brief description goes here.
+
+```js
+// Create your bot
+const mineflayer = require("mineflayer")
+const bot = mineflayer.createBot({
+ host: 'localhost',
+ username: 'Player',
+})
+let mcData
+
+// Load collect block
+bot.loadPlugin(require('mineflayer-collectblock').plugin)
+
+async function collectGrass() {
+ // Find a nearby grass block
+ const grass = bot.findBlock({
+ matching: mcData.blocksByName.grass_block.id,
+ maxDistance: 64
+ })
+
+ if (grass) {
+ // If we found one, collect it.
+ try {
+ await bot.collectBlock.collect(grass)
+ collectGrass() // Collect another grass block
+ } catch (err) {
+ console.log(err) // Handle errors, if any
+ }
+ }
+}
+
+// On spawn, start collecting all nearby grass
+bot.once('spawn', () => {
+ mcData = require('minecraft-data')(bot.version)
+ collectGrass()
+})
+```
+
+### Documentation
+
+[API](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/docs/api.md)
+
+[Examples](https://github.com/TheDudeFromCI/mineflayer-collectblock/tree/master/examples)
+
+### License
+
+This project uses the [MIT](https://github.com/TheDudeFromCI/mineflayer-collectblock/blob/master/LICENSE) license.
+
+### Contributions
+
+This project is accepting PRs and Issues. See something you think can be improved? Go for it! Any and all help is highly appreciated!
+
+For larger changes, it is recommended to discuss these changes in the issues tab before writing any code. It's also preferred to make many smaller PRs than one large one, where applicable.
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/_config.yml b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/_config.yml
new file mode 100644
index 000000000..c4192631f
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/_config.yml
@@ -0,0 +1 @@
+theme: jekyll-theme-cayman
\ No newline at end of file
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/docs/api.md b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/docs/api.md
new file mode 100644
index 000000000..66d8a3ecc
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/docs/api.md
@@ -0,0 +1,52 @@
+# API
+
+Welcome to the *mineflayer-collectblock* API documentation page.
+
+## Table of Contents
+
+- [1. Summary](#1-summary)
+- [Properties](#properties)
+ - [`bot.collectblock.movements: Movements`](#botcollectblockmovements-movements)
+- [Functions](#functions)
+ - [collect](#collect)
+ - [Options:](#options)
+
+## 1. Summary
+
+The collect block plugin is a utility plugin that can be used to help make collecting blocks and item drops very easy, using only a single API call. No need to worry about pathfinding to the block, selecting the right tool, or moving to pick up the item drop after mining.
+
+## Properties
+
+### `bot.collectblock.movements: Movements`
+
+The movements object used by the pathfinder plugin to define the movement configuration. This object is passed to the pathfinder plugin when any API from this plugin is called in order to control how pathfinding should work when collecting the given blocks or item.
+
+If set to null, the pathfinder plugin movements is not updated.
+
+Defaults to a new movements object instance.
+
+## Functions
+
+### collect
+
+Usage: `bot.collectblock.collect(target: Collectable | Collectable[], options?: CollectOptions, cb: (err?: Error) => void): void`
+
+Causes the bot to collect the given block, item drop, or list of those. If the target is a block, the bot will move to the block, mine it, and pick up the item drop. If the target is an item drop, the bot will move to the item drop and pick it up. If the target is a list of collectables, the bot will move from target to target in order of closest to furthest and collect each target in turn.
+
+#### Options:
+
+ * `append: boolean`
+
+ If true, the target(s) will be appended to the existing target list instead of starting a new task. Defaults to false.
+
+ * `ignoreNoPath: boolean`
+
+ If true, errors will not be thrown when a path to the target block cannot be found. The bot will attempt to choose the best available position it can find, instead. Errors are still thrown if the bot cannot interact with the block from it's final location. Defaults to false.
+
+ * `chestLocations: Vec3[]`
+
+ Gets the list of chest locations to use when storing items after the bot's inventory becomes full. If undefined, it defaults to the chest location list on the bot.collectBlock plugin.
+
+ * `itemFilter: ItemFilter`
+
+ When transferring items to a chest, this filter is used to determine what items are allowed to be moved, and what items aren't allowed to be moved. Defaults to the item filter specified on the bot.collectBlock plugin.
\ No newline at end of file
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/collector.js b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/collector.js
new file mode 100644
index 000000000..b9bb8faf9
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/collector.js
@@ -0,0 +1,70 @@
+/**
+ * This bot example show how to direct a bot to collect a specific block type
+ * or a group of nearby blocks of that type.
+ */
+
+const mineflayer = require('mineflayer')
+const collectBlock = require('mineflayer-collectblock').plugin
+
+if (process.argv.length < 4 || process.argv.length > 6) {
+ console.log('Usage : node collector.js [] []')
+ process.exit(1)
+}
+
+const bot = mineflayer.createBot({
+ host: process.argv[2],
+ port: process.argv[3],
+ username: process.argv[4] || 'collector',
+ password: process.argv[5]
+})
+
+bot.loadPlugin(collectBlock)
+
+let mcData
+bot.once('spawn', () => {
+ mcData = require('minecraft-data')(bot.version)
+})
+
+bot.on('chat', async (username, message) => {
+ const args = message.split(' ')
+ if (args[0] !== 'collect') return
+
+ let count = 1
+ if (args.length === 3) count = parseInt(args[1])
+
+ let type = args[1]
+ if (args.length === 3) type = args[2]
+
+ const blockType = mcData.blocksByName[type]
+ if (!blockType) {
+ return
+ }
+
+ const blocks = bot.findBlocks({
+ matching: blockType.id,
+ maxDistance: 64,
+ count: count
+ })
+
+ if (blocks.length === 0) {
+ bot.chat("I don't see that block nearby.")
+ return
+ }
+
+ const targets = []
+ for (let i = 0; i < Math.min(blocks.length, count); i++) {
+ targets.push(bot.blockAt(blocks[i]))
+ }
+
+ bot.chat(`Found ${targets.length} ${type}(s)`)
+
+ try {
+ await bot.collectBlock.collect(targets)
+ // All blocks have been collected.
+ bot.chat('Done')
+ } catch (err) {
+ // An error occurred, report it.
+ bot.chat(err.message)
+ console.log(err)
+ }
+})
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/oreMiner.js b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/oreMiner.js
new file mode 100644
index 000000000..6accac88f
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/oreMiner.js
@@ -0,0 +1,59 @@
+/**
+ * This bot example shows how to collect a vein of ores quickly after only finding a single block.
+ * This makes it easy to collect a vein of ores or mine a tree without looking for every block in the
+ * area.
+ */
+
+const mineflayer = require('mineflayer')
+const collectBlock = require('mineflayer-collectblock').plugin
+
+if (process.argv.length < 4 || process.argv.length > 6) {
+ console.log('Usage : node oreMiner.js [] []')
+ process.exit(1)
+}
+
+const bot = mineflayer.createBot({
+ host: process.argv[2],
+ port: process.argv[3],
+ username: process.argv[4] || 'oreMiner',
+ password: process.argv[5]
+})
+
+bot.loadPlugin(collectBlock)
+
+let mcData
+bot.once('spawn', () => {
+ mcData = require('minecraft-data')(bot.version)
+})
+
+bot.on('chat', async (username, message) => {
+ const args = message.split(' ')
+ if (args[0] !== 'collect') return
+
+ const blockType = mcData.blocksByName[args[1]]
+ if (!blockType) {
+ bot.chat(`I don't know any blocks named ${args[1]}.`)
+ return
+ }
+
+ const block = bot.findBlock({
+ matching: blockType.id,
+ maxDistance: 64
+ })
+
+ if (!block) {
+ bot.chat("I don't see that block nearby.")
+ return
+ }
+
+ const targets = bot.collectBlock.findFromVein(block)
+ try {
+ await bot.collectBlock.collect(targets)
+ // All blocks have been collected.
+ bot.chat('Done')
+ } catch (err) {
+ // An error occurred, report it.
+ bot.chat(err.message)
+ console.log(err)
+ }
+})
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/storageBot.js b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/storageBot.js
new file mode 100644
index 000000000..b6f9971f2
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/examples/storageBot.js
@@ -0,0 +1,107 @@
+/**
+ * This bot example shows how to use the chest filling mechanic of the plugin.
+ * Simply provide a given storage chest, and the bot will automatically try and
+ * store it's inventory in that chest when the bot's inventory becomes full.
+ */
+
+if (process.argv.length < 4 || process.argv.length > 6) {
+ console.log('Usage : node storageBot.js [] []')
+ process.exit(1)
+}
+
+// Load your libraries
+const mineflayer = require('mineflayer')
+const collectBlock = require('mineflayer-collectblock').plugin
+
+// Create your bot
+const bot = mineflayer.createBot({
+ host: process.argv[2],
+ port: parseInt(process.argv[3]),
+ username: process.argv[4] ? process.argv[4] : 'storageBot',
+ password: process.argv[5]
+})
+
+// Load the collect block plugin
+bot.loadPlugin(collectBlock)
+
+// Load mcData on login
+let mcData
+bot.once('login', () => {
+ mcData = require('minecraft-data')(bot.version)
+})
+
+// On spawn, try to find any nearby chests and save those as storage locations.
+// When the bot's inventory becomes too full, it will empty it's inventory into
+// these chests before collecting more resources. If a chest gets full, it moves
+// to the next one in order until it's inventory is empty or it runs out of chests.
+bot.once('spawn', () => {
+ bot.collectBlock.chestLocations = bot.findBlocks({
+ matching: mcData.blocksByName.chest.id,
+ maxDistance: 16,
+ count: 999999 // Get as many chests as we can
+ })
+
+ if (bot.collectBlock.chestLocations.length === 0) {
+ bot.chat("I don't see any chests nearby.")
+ } else {
+ for (const chestPos of bot.collectBlock.chestLocations) {
+ bot.chat(`I found a chest at ${chestPos}`)
+ }
+ }
+})
+
+// Wait for someone to say something
+bot.on('chat', async (username, message) => {
+ // If the player says something start starts with "collect"
+ // Otherwise, do nothing
+ const args = message.split(' ')
+ if (args[0] !== 'collect') return
+
+ // If the player specifies a number, collect that many. Otherwise, default to 1.
+ let count = 1
+ if (args.length === 3) count = parseInt(args[1])
+
+ // If a number was given the item number is the 3rd arg, not the 2nd.
+ let type = args[1]
+ if (args.length === 3) type = args[2]
+
+ // Get the id of that block type for this version of Minecraft.
+ const blockType = mcData.blocksByName[type]
+ if (!blockType) {
+ bot.chat(`I don't know any blocks named ${type}.`)
+ return
+ }
+
+ // Find all nearby blocks of that type, up to the given count, within 64 blocks.
+ const blocks = bot.findBlocks({
+ matching: blockType.id,
+ maxDistance: 64,
+ count: count
+ })
+
+ // Complain if we can't find any nearby blocks of that type.
+ if (blocks.length === 0) {
+ bot.chat("I don't see that block nearby.")
+ return
+ }
+
+ // Convert the block position array into a block array to pass to collect block.
+ const targets = []
+ for (let i = 0; i < Math.min(blocks.length, count); i++) {
+ targets.push(bot.blockAt(blocks[i]))
+ }
+
+ // Announce what we found.
+ bot.chat(`Found ${targets.length} ${type}(s)`)
+
+ // Tell the bot to collect all of the given blocks in the block list.
+ try {
+ await bot.collectBlock.collect(targets)
+ // All blocks have been collected.
+ bot.chat('Done')
+ } catch (err) {
+ // An error occurred, report it.
+ bot.chat(err.message)
+ console.log(err)
+ }
+})
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/package.json b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/package.json
new file mode 100644
index 000000000..0f59e7aa6
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/package.json
@@ -0,0 +1,44 @@
+{
+ "name": "mineflayer-collectblock",
+ "version": "1.4.1",
+ "description": "A simple utility plugin for Mineflayer that add a higher level API for collecting blocks.",
+ "main": "lib/index.js",
+ "types": "lib/index.d.ts",
+ "scripts": {
+ "build": "ts-standard && tsc && require-self",
+ "clean": "rm -rf lib",
+ "test": "test"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/TheDudeFromCI/mineflayer-collectblock.git"
+ },
+ "keywords": [
+ "mineflayer",
+ "plugin",
+ "api",
+ "utility",
+ "helper",
+ "collect"
+ ],
+ "author": "TheDudeFromCI",
+ "license": "MIT",
+ "bugs": {
+ "url": "https://github.com/TheDudeFromCI/mineflayer-collectblock/issues"
+ },
+ "homepage": "https://github.com/TheDudeFromCI/mineflayer-collectblock#readme",
+ "dependencies": {
+ "mineflayer": "^4.0.0",
+ "mineflayer-pathfinder": "^2.1.1",
+ "mineflayer-tool": "^1.1.0"
+ },
+ "devDependencies": {
+ "@types/node": "^18.6.4",
+ "require-self": "^0.2.3",
+ "ts-standard": "^11.0.0",
+ "typescript": "^4.1.3"
+ },
+ "files": [
+ "lib/**/*"
+ ]
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts
new file mode 100644
index 000000000..ae5542ce3
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/BlockVeins.ts
@@ -0,0 +1,35 @@
+import { Bot } from 'mineflayer'
+import { Block } from 'prismarine-block'
+
+export function findFromVein (bot: Bot, block: Block, maxBlocks: number, maxDistance: number, floodRadius: number): Block[] {
+ const targets: Block[] = []
+ const open: Block[] = [block]
+ const type = block.type
+ const center = block.position
+
+ for (let i = 0; i < maxBlocks; i++) {
+ const next = open.pop()
+ if (next == null) break
+
+ targets.push(next)
+
+ for (let x = -floodRadius; x <= floodRadius; x++) {
+ for (let y = -floodRadius; y <= floodRadius; y++) {
+ for (let z = -floodRadius; z <= floodRadius; z++) {
+ const neighborPos = next.position.offset(x, y, z)
+ if (neighborPos.manhattanDistanceTo(center) > maxDistance) continue
+
+ const neighbor = bot.blockAt(neighborPos)
+ if (neighbor == null || neighbor.type !== type) continue
+
+ if (targets.includes(neighbor)) continue
+ if (open.includes(neighbor)) continue
+
+ open.push(neighbor)
+ }
+ }
+ }
+ }
+
+ return targets
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts
new file mode 100644
index 000000000..d2be87822
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/CollectBlock.ts
@@ -0,0 +1,451 @@
+import { Bot } from "mineflayer";
+import { Block } from "prismarine-block";
+import { Movements, goals } from "mineflayer-pathfinder";
+import { TemporarySubscriber } from "./TemporarySubscriber";
+import { Entity } from "prismarine-entity";
+import { error } from "./Util";
+import { Vec3 } from "vec3";
+import { emptyInventoryIfFull, ItemFilter } from "./Inventory";
+import { findFromVein } from "./BlockVeins";
+import { Collectable, Targets } from "./Targets";
+import { Item } from "prismarine-item";
+import mcDataLoader from "minecraft-data";
+import { once } from "events";
+import { callbackify } from "util";
+
+export type Callback = (err?: Error) => void;
+
+async function collectAll(
+ bot: Bot,
+ options: CollectOptionsFull
+): Promise {
+ let success_count = 0;
+ while (!options.targets.empty) {
+ await emptyInventoryIfFull(
+ bot,
+ options.chestLocations,
+ options.itemFilter
+ );
+ const closest = options.targets.getClosest();
+ if (closest == null) break;
+ switch (closest.constructor.name) {
+ case "Block": {
+ try {
+ if (success_count >= options.count) {
+ break;
+ }
+ await bot.tool.equipForBlock(
+ closest as Block,
+ equipToolOptions
+ );
+ const goal = new goals.GoalLookAtBlock(
+ closest.position,
+ bot.world
+ );
+ await bot.pathfinder.goto(goal);
+ await mineBlock(bot, closest as Block, options);
+ success_count++;
+ // TODO: options.ignoreNoPath
+ } catch (err) {
+ // @ts-ignore
+ // console.log(err.stack)
+ // bot.pathfinder.stop()
+ // bot.waitForTicks(10)
+ try {
+ bot.pathfinder.setGoal(null);
+ } catch (err) {}
+ if (options.ignoreNoPath) {
+ // @ts-ignore
+ if (err.name === "Invalid block") {
+ console.log(
+ `Block ${closest.name} at ${closest.position} is not valid! Skip it!`
+ );
+ } // @ts-ignore
+ else if (err.name === "Unsafe block") {
+ console.log(
+ `${closest.name} at ${closest.position} is not safe to break! Skip it!`
+ );
+ // @ts-ignore
+ } else if (err.name === "NoItem") {
+ const properties =
+ bot.registry.blocksByName[closest.name];
+ const leastTool = Object.keys(
+ properties.harvestTools
+ )[0];
+ const item = bot.registry.items[leastTool];
+ bot.chat(
+ `I need at least a ${item.name} to mine ${closest.name}! Skip it!`
+ );
+ return;
+ } else if (
+ // @ts-ignore
+ err.name === "NoPath" ||
+ // @ts-ignore
+ err.name === "Timeout"
+ ) {
+ if (
+ bot.entity.position.distanceTo(
+ closest.position
+ ) < 0.5
+ ) {
+ await mineBlock(bot, closest as Block, options);
+ break;
+ }
+ console.log(
+ `No path to ${closest.name} at ${closest.position}! Skip it!`
+ );
+ // @ts-ignore
+ } else if (err.message === "Digging aborted") {
+ console.log(`Digging aborted! Skip it!`);
+ } else {
+ // @ts-ignore
+ bot.chat(`Error: ${err.message}`);
+ }
+ break;
+ }
+ throw err;
+ }
+ break;
+ }
+ case "Entity": {
+ // Don't collect any entities that are marked as 'invalid'
+ if (!(closest as Entity).isValid) break;
+ try {
+ const tempEvents = new TemporarySubscriber(bot);
+ const waitForPickup = new Promise(
+ (resolve, reject) => {
+ const timeout = setTimeout(() => {
+ // After 10 seconds, reject the promise
+ clearTimeout(timeout);
+ tempEvents.cleanup();
+ reject(new Error("Failed to pickup item"));
+ }, 10000);
+ tempEvents.subscribeTo(
+ "entityGone",
+ (entity: Entity) => {
+ if (entity === closest) {
+ clearTimeout(timeout);
+ tempEvents.cleanup();
+ resolve();
+ }
+ }
+ );
+ }
+ );
+ bot.pathfinder.setGoal(
+ new goals.GoalFollow(closest as Entity, 0)
+ );
+ // await bot.pathfinder.goto(new goals.GoalBlock(closest.position.x, closest.position.y, closest.position.z))
+ await waitForPickup;
+ } catch (err) {
+ // @ts-ignore
+ console.log(err.stack);
+ try {
+ bot.pathfinder.setGoal(null);
+ } catch (err) {}
+ if (options.ignoreNoPath) {
+ // @ts-ignore
+ if (err.message === "Failed to pickup item") {
+ bot.chat(`Failed to pickup item! Skip it!`);
+ }
+ break;
+ }
+ throw err;
+ }
+ break;
+ }
+ default: {
+ throw error(
+ "UnknownType",
+ `Target ${closest.constructor.name} is not a Block or Entity!`
+ );
+ }
+ }
+ options.targets.removeTarget(closest);
+ }
+ bot.chat(`Collect finish!`);
+}
+
+const equipToolOptions = {
+ requireHarvest: true,
+ getFromChest: false,
+ maxTools: 2,
+};
+
+async function mineBlock(
+ bot: Bot,
+ block: Block,
+ options: CollectOptionsFull
+): Promise {
+ if (
+ bot.blockAt(block.position)?.type !== block.type ||
+ bot.blockAt(block.position)?.type === 0
+ ) {
+ options.targets.removeTarget(block);
+ throw error("Invalid block", "Block is not valid!");
+ // @ts-expect-error
+ } else if (!bot.pathfinder.movements.safeToBreak(block)) {
+ options.targets.removeTarget(block);
+ throw error("Unsafe block", "Block is not safe to break!");
+ }
+
+ await bot.tool.equipForBlock(block, equipToolOptions);
+
+ if (!block.canHarvest(bot.heldItem ? bot.heldItem.type : bot.heldItem)) {
+ options.targets.removeTarget(block);
+ throw error("NoItem", "Bot does not have a harvestable tool!");
+ }
+
+ const tempEvents = new TemporarySubscriber(bot);
+ tempEvents.subscribeTo("itemDrop", (entity: Entity) => {
+ if (
+ entity.position.distanceTo(block.position.offset(0.5, 0.5, 0.5)) <=
+ 0.5
+ ) {
+ options.targets.appendTarget(entity);
+ }
+ });
+ try {
+ await bot.dig(block);
+ // Waiting for items to drop
+ await new Promise((resolve) => {
+ let remainingTicks = 10;
+ tempEvents.subscribeTo("physicTick", () => {
+ remainingTicks--;
+ if (remainingTicks <= 0) {
+ tempEvents.cleanup();
+ resolve();
+ }
+ });
+ });
+ } finally {
+ tempEvents.cleanup();
+ }
+}
+
+/**
+ * A set of options to apply when collecting the given targets.
+ */
+export interface CollectOptions {
+ /**
+ * If true, the target(s) will be appended to the existing target list instead of
+ * starting a new task. Defaults to false.
+ */
+ append?: boolean;
+
+ /**
+ * If true, errors will not be thrown when a path to the target block cannot
+ * be found. The bot will attempt to choose the best available position it
+ * can find, instead. Errors are still thrown if the bot cannot interact with
+ * the block from it's final location. Defaults to false.
+ */
+ ignoreNoPath?: boolean;
+
+ /**
+ * Gets the list of chest locations to use when storing items after the bot's
+ * inventory becomes full. If undefined, it defaults to the chest location
+ * list on the bot.collectBlock plugin.
+ */
+ chestLocations?: Vec3[];
+
+ /**
+ * When transferring items to a chest, this filter is used to determine what
+ * items are allowed to be moved, and what items aren't allowed to be moved.
+ * Defaults to the item filter specified on the bot.collectBlock plugin.
+ */
+ itemFilter?: ItemFilter;
+
+ /**
+ * The total number of items to collect
+ */
+ count?: number;
+}
+
+/**
+ * A version of collect options where all values are assigned.
+ */
+interface CollectOptionsFull {
+ append: boolean;
+ ignoreNoPath: boolean;
+ chestLocations: Vec3[];
+ itemFilter: ItemFilter;
+ targets: Targets;
+ count: number;
+}
+
+/**
+ * The collect block plugin.
+ */
+export class CollectBlock {
+ /**
+ * The bot.
+ */
+ private readonly bot: Bot;
+
+ /**
+ * The list of active targets being collected.
+ */
+ private readonly targets: Targets;
+
+ /**
+ * The movements configuration to be sent to the pathfinder plugin.
+ */
+ movements?: Movements;
+
+ /**
+ * A list of chest locations which the bot is allowed to empty their inventory into
+ * if it becomes full while the bot is collecting resources.
+ */
+ chestLocations: Vec3[] = [];
+
+ /**
+ * When collecting items, this filter is used to determine what items should be placed
+ * into a chest if the bot's inventory becomes full. By default, returns true for all
+ * items except for tools, weapons, and armor.
+ *
+ * @param item - The item stack in the bot's inventory to check.
+ *
+ * @returns True if the item should be moved into the chest. False otherwise.
+ */
+ itemFilter: ItemFilter = (item: Item) => {
+ if (item.name.includes("helmet")) return false;
+ if (item.name.includes("chestplate")) return false;
+ if (item.name.includes("leggings")) return false;
+ if (item.name.includes("boots")) return false;
+ if (item.name.includes("shield")) return false;
+ if (item.name.includes("sword")) return false;
+ if (item.name.includes("pickaxe")) return false;
+ if (item.name.includes("axe")) return false;
+ if (item.name.includes("shovel")) return false;
+ if (item.name.includes("hoe")) return false;
+ return true;
+ };
+
+ /**
+ * Creates a new instance of the create block plugin.
+ *
+ * @param bot - The bot this plugin is acting on.
+ */
+ constructor(bot: Bot) {
+ this.bot = bot;
+ this.targets = new Targets(bot);
+ // @ts-ignore
+ this.movements = new Movements(bot, mcDataLoader(bot.version));
+ }
+
+ /**
+ * If target is a block:
+ * Causes the bot to break and collect the target block.
+ *
+ * If target is an item drop:
+ * Causes the bot to collect the item drop.
+ *
+ * If target is an array containing items or blocks, preforms the correct action for
+ * all targets in that array sorting dynamically by distance.
+ *
+ * @param target - The block(s) or item(s) to collect.
+ * @param options - The set of options to use when handling these targets
+ * @param cb - The callback that is called finished.
+ */
+ async collect(
+ target: Collectable | Collectable[],
+ options: CollectOptions | Callback = {},
+ cb?: Callback
+ ): Promise {
+ if (typeof options === "function") {
+ cb = options;
+ options = {};
+ }
+ // @ts-expect-error
+ if (cb != null) return callbackify(this.collect)(target, options, cb);
+
+ const optionsFull: CollectOptionsFull = {
+ append: options.append ?? false,
+ ignoreNoPath: options.ignoreNoPath ?? false,
+ chestLocations: options.chestLocations ?? this.chestLocations,
+ itemFilter: options.itemFilter ?? this.itemFilter,
+ targets: this.targets,
+ count: options.count ?? Infinity,
+ };
+
+ if (this.bot.pathfinder == null) {
+ throw error(
+ "UnresolvedDependency",
+ "The mineflayer-collectblock plugin relies on the mineflayer-pathfinder plugin to run!"
+ );
+ }
+
+ if (this.bot.tool == null) {
+ throw error(
+ "UnresolvedDependency",
+ "The mineflayer-collectblock plugin relies on the mineflayer-tool plugin to run!"
+ );
+ }
+
+ if (this.movements != null) {
+ this.bot.pathfinder.setMovements(this.movements);
+ }
+
+ if (!optionsFull.append) await this.cancelTask();
+ if (Array.isArray(target)) {
+ this.targets.appendTargets(target);
+ } else {
+ this.targets.appendTarget(target);
+ }
+
+ try {
+ await collectAll(this.bot, optionsFull);
+ this.targets.clear();
+ } catch (err) {
+ this.targets.clear();
+ // Ignore path stopped error for cancelTask to work properly (imo we shouldn't throw any pathing errors)
+ // @ts-expect-error
+ if (err.name !== "PathStopped") throw err;
+ } finally {
+ // @ts-expect-error
+ this.bot.emit("collectBlock_finished");
+ }
+ }
+
+ /**
+ * Loads all touching blocks of the same type to the given block and returns them as an array.
+ * This effectively acts as a flood fill algorithm to retrieve blocks in the same ore vein and similar.
+ *
+ * @param block - The starting block.
+ * @param maxBlocks - The maximum number of blocks to look for before stopping.
+ * @param maxDistance - The max distance from the starting block to look.
+ * @param floodRadius - The max distance distance from block A to block B to be considered "touching"
+ */
+ findFromVein(
+ block: Block,
+ maxBlocks = 100,
+ maxDistance = 16,
+ floodRadius = 1
+ ): Block[] {
+ return findFromVein(
+ this.bot,
+ block,
+ maxBlocks,
+ maxDistance,
+ floodRadius
+ );
+ }
+
+ /**
+ * Cancels the current collection task, if still active.
+ *
+ * @param cb - The callback to use when the task is stopped.
+ */
+ async cancelTask(cb?: Callback): Promise {
+ if (this.targets.empty) {
+ if (cb != null) cb();
+ return await Promise.resolve();
+ }
+ this.bot.pathfinder.stop();
+ if (cb != null) {
+ // @ts-expect-error
+ this.bot.once("collectBlock_finished", cb);
+ }
+ await once(this.bot, "collectBlock_finished");
+ }
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Inventory.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Inventory.ts
new file mode 100644
index 000000000..6a17d0cc5
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Inventory.ts
@@ -0,0 +1,87 @@
+import { Bot } from 'mineflayer'
+import { Callback } from './CollectBlock'
+import { Vec3 } from 'vec3'
+import { error } from './Util'
+import { Item } from 'prismarine-item'
+import { goals } from 'mineflayer-pathfinder'
+import { callbackify } from 'util'
+
+export type ItemFilter = (item: Item) => boolean
+
+function getClosestChest (bot: Bot, chestLocations: Vec3[]): Vec3 | null {
+ let chest = null
+ let distance = 0
+
+ for (const c of chestLocations) {
+ const dist = c.distanceTo(bot.entity.position)
+ if (chest == null || dist < distance) {
+ chest = c
+ distance = dist
+ }
+ }
+
+ if (chest != null) {
+ chestLocations.splice(chestLocations.indexOf(chest), 1)
+ }
+
+ return chest
+}
+
+export async function emptyInventoryIfFull (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise {
+ // @ts-expect-error
+ if (cb != null) return callbackify(emptyInventoryIfFull)(bot, chestLocations, cb)
+ if (bot.inventory.emptySlotCount() > 0) return
+ return await emptyInventory(bot, chestLocations, itemFilter)
+}
+
+export async function emptyInventory (bot: Bot, chestLocations: Vec3[], itemFilter: ItemFilter, cb?: Callback): Promise {
+ // @ts-expect-error
+ if (cb != null) return callbackify(emptyInventory)(bot, chestLocations, cb)
+ if (chestLocations.length === 0) {
+ throw error('NoChests', 'There are no defined chest locations!')
+ }
+
+ // Shallow clone so we can safely remove chests from the list that are full.
+ chestLocations = [...chestLocations]
+
+ while (true) {
+ const chest = getClosestChest(bot, chestLocations)
+ if (chest == null) {
+ throw error('NoChests', 'All chests are full.')
+ }
+ const hasRemaining = await tryEmptyInventory(bot, chest, itemFilter)
+ if (!hasRemaining) return
+ }
+}
+
+async function tryEmptyInventory (bot: Bot, chestLocation: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise {
+ // @ts-expect-error
+ if (cb != null) return callbackify(tryEmptyInventory)(bot, chestLocation, itemFilter, cb)
+ await gotoChest(bot, chestLocation)
+ return await placeItems(bot, chestLocation, itemFilter)
+}
+
+async function gotoChest (bot: Bot, location: Vec3, cb?: Callback): Promise {
+ // @ts-expect-error
+ if (cb != null) return callbackify(gotoChest)(bot, location)
+ await bot.pathfinder.goto(new goals.GoalGetToBlock(location.x, location.y, location.z))
+}
+
+async function placeItems (bot: Bot, chestPos: Vec3, itemFilter: ItemFilter, cb?: (err: Error | undefined, hasRemaining: boolean) => void): Promise {
+ // @ts-expect-error
+ if (cb != null) return callbackify(placeItems)(bot, chestPos, itemFilter, cb)
+ const chestBlock = bot.blockAt(chestPos)
+ if (chestBlock == null) {
+ throw error('UnloadedChunk', 'Chest is in an unloaded chunk!')
+ }
+ const chest = await bot.openChest(chestBlock)
+ for (const item of bot.inventory.items()) {
+ if (!itemFilter(item)) continue
+ if (chest.firstEmptyContainerSlot() === null) {
+ // We have items that didn't fit.
+ return true
+ }
+ await chest.deposit(item.type, item.metadata, item.count)
+ }
+ return false
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Targets.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Targets.ts
new file mode 100644
index 000000000..568d07ad9
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Targets.ts
@@ -0,0 +1,60 @@
+import { Bot } from 'mineflayer'
+import { Block } from 'prismarine-block'
+import { Entity } from 'prismarine-entity'
+
+export type Collectable = Block | Entity
+
+export class Targets {
+ private readonly bot: Bot
+ private targets: Collectable[] = []
+
+ constructor (bot: Bot) {
+ this.bot = bot
+ }
+
+ appendTargets (targets: Collectable[]): void {
+ for (const target of targets) {
+ this.appendTarget(target)
+ }
+ }
+
+ appendTarget (target: Collectable): void {
+ if (this.targets.includes(target)) return
+ this.targets.push(target)
+ }
+
+ /**
+ * Gets the closest target to the bot in this list.
+ *
+ * @returns The closest target, or null if there are no targets.
+ */
+ getClosest (): Collectable | null {
+ let closest: Collectable | null = null
+ let distance: number = 0
+
+ for (const target of this.targets) {
+ const dist = target.position.distanceTo(this.bot.entity.position)
+
+ if (closest == null || dist < distance) {
+ closest = target
+ distance = dist
+ }
+ }
+
+ return closest
+ }
+
+ get empty (): boolean {
+ return this.targets.length === 0
+ }
+
+ clear (): void {
+ this.targets.length = 0
+ }
+
+ removeTarget (target: Collectable): void {
+ const index = this.targets.indexOf(target)
+ if (index < 0) return
+ this.targets.splice(index, 1)
+ }
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts
new file mode 100644
index 000000000..81fe3bc5a
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/TaskQueue.ts
@@ -0,0 +1,77 @@
+import type { Callback } from './index'
+export type Task = (cb: Callback) => void
+export type SyncTask = () => void
+
+/**
+ * A simple utility class for queuing up a series of async tasks to execute.
+ */
+export class TaskQueue {
+ private tasks: Task[] = []
+
+ /**
+ * If true, the task list will stop executing if one of the tasks throws an error.
+ */
+ readonly stopOnError: boolean = true
+
+ /**
+ * Adds a new async task to this queue. The provided callback should be executed when
+ * the async task is complete.
+ *
+ * @param task - The async task to add.
+ */
+ add (task: Task): void {
+ this.tasks.push(task)
+ }
+
+ /**
+ * Adds a synchronous task toi this queue.
+ *
+ * @param task - The sync task to add.
+ */
+ addSync (task: SyncTask): void {
+ this.add((cb) => {
+ try {
+ task()
+ cb()
+ } catch (err: any) {
+ cb(err)
+ }
+ })
+ }
+
+ /**
+ * Runs all tasks currently in this queue and empties the queue.
+ *
+ * @param cb - The optional callback to be executed when all tasks in this queue have
+ * finished executing.
+ */
+ runAll (cb?: Callback): void {
+ const taskList = this.tasks
+ this.tasks = []
+
+ let index = -1
+ const runNext: () => void = () => {
+ index++
+ if (index >= taskList.length) {
+ if (cb !== undefined) cb()
+ return
+ }
+
+ try {
+ taskList[index]((err) => {
+ if (err !== undefined) {
+ if (cb !== undefined) cb(err)
+
+ if (this.stopOnError) return
+ }
+
+ runNext()
+ })
+ } catch (err: any) {
+ if (cb !== undefined) cb(err)
+ }
+ }
+
+ runNext()
+ }
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts
new file mode 100644
index 000000000..3f14a607d
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/TemporarySubscriber.ts
@@ -0,0 +1,34 @@
+import { Bot } from 'mineflayer'
+
+class Subscription {
+ constructor (readonly eventName: string, readonly callback: Function) {}
+}
+
+export class TemporarySubscriber {
+ private readonly subscriptions: Subscription[] = []
+
+ constructor (readonly bot: Bot) {}
+
+ /**
+ * Adds a new temporary event listener to the bot.
+ *
+ * @param event - The event to subscribe to.
+ * @param callback - The function to execute.
+ */
+ subscribeTo (event: string, callback: Function): void {
+ this.subscriptions.push(new Subscription(event, callback))
+
+ // @ts-expect-error
+ this.bot.on(event, callback)
+ }
+
+ /**
+ * Removes all attached event listeners from the bot.
+ */
+ cleanup (): void {
+ for (const sub of this.subscriptions) {
+ // @ts-expect-error
+ this.bot.removeListener(sub.eventName, sub.callback)
+ }
+ }
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Util.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Util.ts
new file mode 100644
index 000000000..ee0f29e0c
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/Util.ts
@@ -0,0 +1,13 @@
+/**
+ * Creates a new error object with the given type and message.
+ *
+ * @param type - The error type.
+ * @param message - The error message.
+ *
+ * @returns The error object.
+ */
+export function error (type: string, message: string): Error {
+ const e = new Error(message)
+ e.name = type
+ return e
+}
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/index.ts b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/index.ts
new file mode 100644
index 000000000..45c9a8508
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/src/index.ts
@@ -0,0 +1,25 @@
+import { Bot } from 'mineflayer'
+import { CollectBlock } from './CollectBlock'
+import { pathfinder as pathfinderPlugin } from 'mineflayer-pathfinder'
+import { plugin as toolPlugin } from 'mineflayer-tool'
+
+export function plugin (bot: Bot): void {
+ // @ts-expect-error
+ bot.collectBlock = new CollectBlock(bot)
+
+ // Load plugins if not loaded manually.
+ setTimeout(() => loadPathfinderPlugin(bot), 0)
+ setTimeout(() => loadToolPlugin(bot), 0)
+}
+
+function loadPathfinderPlugin (bot: Bot): void {
+ if (bot.pathfinder != null) return
+ bot.loadPlugin(pathfinderPlugin)
+}
+
+function loadToolPlugin (bot: Bot): void {
+ if (bot.tool != null) return
+ bot.loadPlugin(toolPlugin)
+}
+
+export { CollectBlock, Callback, CollectOptions } from './CollectBlock'
diff --git a/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/tsconfig.json b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/tsconfig.json
new file mode 100644
index 000000000..a6076bc0c
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/mineflayer-collectblock/tsconfig.json
@@ -0,0 +1,69 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig.json to read more about this file */
+ /* Basic Options */
+ // "incremental": true, /* Enable incremental compilation */
+ "target": "ES2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
+ "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
+ // "lib": [], /* Specify library files to be included in the compilation. */
+ "allowJs": true, /* Allow javascript files to be compiled. */
+ "checkJs": true, /* Report errors in .js files. */
+ // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
+ "declaration": true,
+ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
+ // "sourceMap": true, /* Generates corresponding '.map' file. */
+ // "outFile": "./", /* Concatenate and emit output to single file. */
+ "outDir": "./lib",
+ // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
+ // "composite": true, /* Enable project compilation */
+ // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
+ // "removeComments": true, /* Do not emit comments to output. */
+ // "noEmit": true, /* Do not emit outputs. */
+ // "importHelpers": true, /* Import emit helpers from 'tslib'. */
+ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
+ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
+ /* Strict Type-Checking Options */
+ "strict": true, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
+ "strictNullChecks": true, /* Enable strict null checks. */
+ // "strictFunctionTypes": true, /* Enable strict checking of function types. */
+ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
+ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
+ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
+ "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
+ /* Additional Checks */
+ "noUnusedLocals": true, /* Report errors on unused locals. */
+ // "noUnusedParameters": true, /* Report errors on unused parameters. */
+ "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
+ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
+ /* Module Resolution Options */
+ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
+ // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
+ // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
+ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
+ // "typeRoots": [], /* List of folders to include type definitions from. */
+ // "types": [], /* Type declaration files to be included in compilation. */
+ // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+ // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ /* Source Map Options */
+ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
+ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
+ /* Experimental Options */
+ // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
+ // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
+ /* Advanced Options */
+ "skipLibCheck": true, /* Skip type checking of declaration files. */
+ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
+ },
+ "include": [
+ "src"
+ ],
+ "exclude": [
+ "node_modules",
+ "**/__tests__/*"
+ ]
+}
\ No newline at end of file
diff --git a/metagpt/mineflayer_env/mineflayer/package.json b/metagpt/mineflayer_env/mineflayer/package.json
new file mode 100644
index 000000000..9e389d268
--- /dev/null
+++ b/metagpt/mineflayer_env/mineflayer/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "voyager",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "body-parser": "^1.20.2",
+ "express": "^4.18.2",
+ "magic-string": "^0.30.0",
+ "minecraft-data": "^3.31.0",
+ "minecrafthawkeye": "^1.3.6",
+ "mineflayer": "^4.8.1",
+ "mineflayer-collectblock": "file:mineflayer-collectblock",
+ "mineflayer-pathfinder": "^2.4.2",
+ "mineflayer-pvp": "^1.3.2",
+ "mineflayer-tool": "^1.2.0",
+ "mocha": "^10.2.0",
+ "prismarine-biome": "^1.3.0",
+ "prismarine-block": "=1.16.3",
+ "prismarine-entity": "^2.2.0",
+ "prismarine-item": "^1.12.1",
+ "prismarine-nbt": "^2.2.1",
+ "prismarine-recipe": "^1.3.1",
+ "prismarine-viewer": "^1.24.0",
+ "typescript": "^4.9.5",
+ "vec3": "^0.1.8",
+ "graceful-fs": "^4.2.11"
+ },
+ "devDependencies": {
+ "prettier": "2.8.5"
+ }
+}
diff --git a/metagpt/mineflayer_environment.py b/metagpt/mineflayer_environment.py
new file mode 100644
index 000000000..1b208c8d5
--- /dev/null
+++ b/metagpt/mineflayer_environment.py
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+# @Date : 2023/09/25 22:13
+# @Author : yuymf
+# @Desc : @https://github.com/MineDojo/Voyager/blob/main/voyager/env/bridge.py
+import os
+import time
+import json
+import requests
+
+from metagpt.logs import logger
+import metagpt.utils.minecraft as U
+from metagpt.utils.minecraft.process_monitor import SubprocessMonitor
+
+class MineflayerEnv:
+ def __init__(
+ self,
+ mc_port=None,
+ server_host="http://127.0.0.1",
+ server_port=3000,
+ request_timeout=600,
+ ):
+ self.mc_port = mc_port
+ self.server = f"{server_host}:{server_port}"
+ self.server_port = server_port
+ self.request_timeout = request_timeout
+ self.mineflayer = self.get_mineflayer_process(server_port)
+ self.has_reset = False
+ self.reset_options = None
+ self.connected = False
+ self.server_paused = False
+
+ def set_mc_port(self, mc_port):
+ self.mc_port = mc_port
+
+ def get_mineflayer_process(self, server_port):
+ U.f_mkdir("./logs", "mineflayer")
+ file_path = os.path.abspath(os.path.dirname(__file__))
+ return SubprocessMonitor(
+ commands=[
+ "node",
+ U.f_join(file_path, "mineflayer_env/mineflayer/index.js"),
+ str(server_port),
+ ],
+ name="mineflayer",
+ ready_match=r"Server started on port (\d+)",
+ log_path=U.f_join("./logs", "mineflayer"),
+ )
+
+ def check_process(self):
+ retry = 0
+ while not self.mineflayer.is_running:
+ logger.info("Mineflayer process has exited, restarting")
+ self.mineflayer.run()
+ if not self.mineflayer.is_running:
+ if retry > 3:
+ logger.error("Mineflayer process failed to start")
+ raise {}
+ else:
+ retry += 1
+ continue
+ logger.info(self.mineflayer.ready_line)
+ res = requests.post(
+ f"{self.server}/start",
+ json=self.reset_options,
+ timeout=self.request_timeout,
+ )
+ if res.status_code != 200:
+ self.mineflayer.stop()
+ logger.error(
+ f"Minecraft server reply with code {res.status_code}"
+ )
+ raise {}
+ return res.json()
+
+ def reset(self, *, seed=None, options=None, ):
+ if options is None:
+ options = {}
+ if options.get("inventory", {}) and options.get("mode", "hard") != "hard":
+ logger.error("inventory can only be set when options is hard")
+ raise{}
+
+ self.reset_options = {
+ "port": self.mc_port,
+ "reset": options.get("mode", "hard"),
+ "inventory": options.get("inventory", {}),
+ "equipment": options.get("equipment", []),
+ "spread": options.get("spread", False),
+ "waitTicks": options.get("wait_ticks", 5),
+ "position": options.get("position", None),
+ }
+
+ self.unpause()
+ self.mineflayer.stop()
+ time.sleep(1) # wait for mineflayer to exit
+
+ returned_data = self.check_process()
+ self.has_reset = True
+ self.connected = True
+ # All the reset in step will be soft
+ self.reset_options["reset"] = "soft"
+ self.pause()
+ return json.loads(returned_data)
+
+ def close(self):
+ self.unpause()
+ if self.connected:
+ res = requests.post(f"{self.server}/stop")
+ if res.status_code == 200:
+ self.connected = False
+ self.mineflayer.stop()
+ return not self.connected
+
+ def pause(self):
+ if self.mineflayer.is_running and not self.server_paused:
+ res = requests.post(f"{self.server}/pause")
+ if res.status_code == 200:
+ self.server_paused = True
+ return self.server_paused
+
+ def unpause(self):
+ if self.mineflayer.is_running and self.server_paused:
+ res = requests.post(f"{self.server}/pause")
+ if res.status_code == 200:
+ self.server_paused = False
+ else:
+ print(res.json())
+ return self.server_paused
diff --git a/metagpt/utils/minecraft/__init__.py b/metagpt/utils/minecraft/__init__.py
index b6d3ec8fd..4c7624d55 100644
--- a/metagpt/utils/minecraft/__init__.py
+++ b/metagpt/utils/minecraft/__init__.py
@@ -2,3 +2,6 @@
# @Date : 2023/9/24 0:32
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
+from .load_prompts import load_prompt
+from .json_utils import *
+from .file_utils import *
\ No newline at end of file
diff --git a/metagpt/utils/minecraft/file_utils.py b/metagpt/utils/minecraft/file_utils.py
new file mode 100644
index 000000000..8f199329b
--- /dev/null
+++ b/metagpt/utils/minecraft/file_utils.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# @Date : 2023/09/25 16:13
+# @Author : yuymf
+# @Desc : Temp Using :File system utils.@ https://github.com/MineDojo/Voyager/blob/main/voyager/utils/file_utils.py
+import collections
+import os
+
+f_ext = os.path.splitext
+
+f_size = os.path.getsize
+
+is_file = os.path.isfile
+
+is_dir = os.path.isdir
+
+get_dir = os.path.dirname
+
+def is_sequence(obj):
+ """
+ Returns:
+ True if the sequence is a collections.Sequence and not a string.
+ """
+ return isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str)
+
+
+def pack_varargs(args):
+ """
+ Pack *args or a single list arg as list
+
+ def f(*args):
+ arg_list = pack_varargs(args)
+ # arg_list is now packed as a list
+ """
+ assert isinstance(args, tuple), "please input the tuple `args` as in *args"
+ if len(args) == 1 and is_sequence(args[0]):
+ return args[0]
+ else:
+ return args
+
+
+def f_expand(fpath):
+ return os.path.expandvars(os.path.expanduser(fpath))
+
+
+def f_exists(*fpaths):
+ return os.path.exists(f_join(*fpaths))
+
+
+def f_join(*fpaths):
+ """
+ join file paths and expand special symbols like `~` for home dir
+ """
+ fpaths = pack_varargs(fpaths)
+ fpath = f_expand(os.path.join(*fpaths))
+ if isinstance(fpath, str):
+ fpath = fpath.strip()
+ return fpath
+
+
+def f_mkdir(*fpaths):
+ """
+ Recursively creates all the subdirs
+ If exist, do nothing.
+ """
+ fpath = f_join(*fpaths)
+ os.makedirs(fpath, exist_ok=True)
+ return fpath
+
+
+def load_text(*fpaths, by_lines=False):
+ with open(f_join(*fpaths), "r") as fp:
+ if by_lines:
+ return fp.readlines()
+ else:
+ return fp.read()
+
+
+def load_text_lines(*fpaths):
+ return load_text(*fpaths, by_lines=True)
+
+# aliases to be consistent with other load_* and dump_*
+text_load = load_text
+read_text = load_text
+read_text_lines = load_text_lines
\ No newline at end of file
diff --git a/metagpt/utils/minecraft/json_utils.py b/metagpt/utils/minecraft/json_utils.py
new file mode 100644
index 000000000..2814a5887
--- /dev/null
+++ b/metagpt/utils/minecraft/json_utils.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+# @Date : 2023/09/25 19:27
+# @Author : yuymf
+# @Desc : Temp using @https://github.com/MineDojo/Voyager/blob/main/voyager/utils/json_utils.py
+
+import json
+import re
+from typing import Any, Dict, Union
+from .file_utils import f_join
+
+def json_load(*file_path, **kwargs):
+ file_path = f_join(file_path)
+ with open(file_path, "r") as fp:
+ return json.load(fp, **kwargs)
+
+
+def json_loads(string, **kwargs):
+ return json.loads(string, **kwargs)
+
+
+def json_dump(data, *file_path, **kwargs):
+ file_path = f_join(file_path)
+ with open(file_path, "w") as fp:
+ json.dump(data, fp, **kwargs)
+
+
+def json_dumps(data, **kwargs):
+ """
+ Returns: string
+ """
+ return json.dumps(data, **kwargs)
+
+
+# ---------------- Aliases -----------------
+# add aliases where verb goes first, json_load -> load_json
+load_json = json_load
+loads_json = json_loads
+dump_json = json_dump
+dumps_json = json_dumps
+
+
+def extract_char_position(error_message: str) -> int:
+ """Extract the character position from the JSONDecodeError message.
+ Args:
+ error_message (str): The error message from the JSONDecodeError
+ exception.
+ Returns:
+ int: The character position.
+ """
+ import re
+
+ char_pattern = re.compile(r"\(char (\d+)\)")
+ if match := char_pattern.search(error_message):
+ return int(match[1])
+ else:
+ raise ValueError("Character position not found in the error message.")
+
+
+def add_quotes_to_property_names(json_string: str) -> str:
+ """
+ Add quotes to property names in a JSON string.
+ Args:
+ json_string (str): The JSON string.
+ Returns:
+ str: The JSON string with quotes added to property names.
+ """
+
+ def replace_func(match):
+ return f'"{match.group(1)}":'
+
+ property_name_pattern = re.compile(r"(\w+):")
+ corrected_json_string = property_name_pattern.sub(replace_func, json_string)
+
+ try:
+ json.loads(corrected_json_string)
+ return corrected_json_string
+ except json.JSONDecodeError as e:
+ raise e
+
+
+def balance_braces(json_string: str) -> str:
+ """
+ Balance the braces in a JSON string.
+ Args:
+ json_string (str): The JSON string.
+ Returns:
+ str: The JSON string with braces balanced.
+ """
+
+ open_braces_count = json_string.count("{")
+ close_braces_count = json_string.count("}")
+
+ while open_braces_count > close_braces_count:
+ json_string += "}"
+ close_braces_count += 1
+
+ while close_braces_count > open_braces_count:
+ json_string = json_string.rstrip("}")
+ close_braces_count -= 1
+
+ try:
+ json.loads(json_string)
+ return json_string
+ except json.JSONDecodeError as e:
+ raise e
+
+
+def fix_invalid_escape(json_str: str, error_message: str) -> str:
+ while error_message.startswith("Invalid \\escape"):
+ bad_escape_location = extract_char_position(error_message)
+ json_str = json_str[:bad_escape_location] + json_str[bad_escape_location + 1 :]
+ try:
+ json.loads(json_str)
+ return json_str
+ except json.JSONDecodeError as e:
+ error_message = str(e)
+ return json_str
+
+
+def correct_json(json_str: str) -> str:
+ """
+ Correct common JSON errors.
+ Args:
+ json_str (str): The JSON string.
+ """
+
+ try:
+ json.loads(json_str)
+ return json_str
+ except json.JSONDecodeError as e:
+ error_message = str(e)
+ if error_message.startswith("Invalid \\escape"):
+ json_str = fix_invalid_escape(json_str, error_message)
+ if error_message.startswith(
+ "Expecting property name enclosed in double quotes"
+ ):
+ json_str = add_quotes_to_property_names(json_str)
+ try:
+ json.loads(json_str)
+ return json_str
+ except json.JSONDecodeError as e:
+ error_message = str(e)
+ if balanced_str := balance_braces(json_str):
+ return balanced_str
+ return json_str
+
+def fix_and_parse_json(
+ json_str: str, try_to_fix_with_gpt: bool = True
+) -> Union[str, Dict[Any, Any]]:
+ """Fix and parse JSON string"""
+ try:
+ json_str = json_str.replace("\t", "")
+ return json.loads(json_str)
+ except json.JSONDecodeError as _: # noqa: F841
+ json_str = correct_json(json_str)
+ try:
+ return json.loads(json_str)
+ except json.JSONDecodeError as _: # noqa: F841
+ pass
+ try:
+ brace_index = json_str.index("{")
+ json_str = json_str[brace_index:]
+ last_brace_index = json_str.rindex("}")
+ json_str = json_str[: last_brace_index + 1]
+ return json.loads(json_str)
+ except json.JSONDecodeError as e: # noqa: F841
+ raise e
\ No newline at end of file
diff --git a/metagpt/utils/minecraft/load_prompts.py b/metagpt/utils/minecraft/load_prompts.py
index 3af9c6c57..fe7c282b3 100644
--- a/metagpt/utils/minecraft/load_prompts.py
+++ b/metagpt/utils/minecraft/load_prompts.py
@@ -3,7 +3,8 @@
# @Author : stellahong (stellahong@fuzhi.ai)
# @Desc :
import pkg_resources
-
-
+from .file_utils import load_text
+
def load_prompt(prompt):
- pass
\ No newline at end of file
+ package_path = pkg_resources.resource_filename("metagpt", "")
+ return load_text(f"{package_path}/prompts/minecraft/{prompt}.txt")
\ No newline at end of file
diff --git a/metagpt/utils/minecraft/process_monitor.py b/metagpt/utils/minecraft/process_monitor.py
new file mode 100644
index 000000000..53ec1c132
--- /dev/null
+++ b/metagpt/utils/minecraft/process_monitor.py
@@ -0,0 +1,90 @@
+# -*- coding: utf-8 -*-
+# @Date : 2023/09/25 16:12
+# @Author : yuymf
+# @Desc : Temp using:@https://github.com/MineDojo/Voyager/blob/main/voyager/env/process_monitor.py
+import time
+import re
+import warnings
+from typing import List
+
+import psutil
+import subprocess
+import logging
+import threading
+
+import metagpt.utils.minecraft as U
+
+
+class SubprocessMonitor:
+ def __init__(
+ self,
+ commands: List[str],
+ name: str,
+ ready_match: str = r".*",
+ log_path: str = "logs",
+ callback_match: str = r"^(?!x)x$", # regex that will never match
+ callback: callable = None,
+ finished_callback: callable = None,
+ ):
+ self.commands = commands
+ start_time = time.strftime("%Y%m%d_%H%M%S")
+ self.name = name
+ self.logger = logging.getLogger(name)
+ handler = logging.FileHandler(U.f_join(log_path, f"{start_time}.log"))
+ formatter = logging.Formatter(
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
+ )
+ handler.setFormatter(formatter)
+ self.logger.addHandler(handler)
+ self.logger.setLevel(logging.INFO)
+ self.process = None
+ self.ready_match = ready_match
+ self.ready_event = None
+ self.ready_line = None
+ self.callback_match = callback_match
+ self.callback = callback
+ self.finished_callback = finished_callback
+ self.thread = None
+
+ def _start(self):
+ self.logger.info(f"Starting subprocess with commands: {self.commands}")
+
+ self.process = psutil.Popen(
+ self.commands,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ universal_newlines=True,
+ )
+ print(f"Subprocess {self.name} started with PID {self.process.pid}.")
+ for line in iter(self.process.stdout.readline, ""):
+ self.logger.info(line.strip())
+ if re.search(self.ready_match, line):
+ self.ready_line = line
+ self.logger.info("Subprocess is ready.")
+ self.ready_event.set()
+ if re.search(self.callback_match, line):
+ self.callback()
+ if not self.ready_event.is_set():
+ self.ready_event.set()
+ warnings.warn(f"Subprocess {self.name} failed to start.")
+ if self.finished_callback:
+ self.finished_callback()
+
+ def run(self):
+ self.ready_event = threading.Event()
+ self.ready_line = None
+ self.thread = threading.Thread(target=self._start)
+ self.thread.start()
+ self.ready_event.wait()
+
+ def stop(self):
+ self.logger.info("Stopping subprocess.")
+ if self.process and self.process.is_running():
+ self.process.terminate()
+ self.process.wait()
+
+ @property
+ def is_running(self):
+ if self.process is None:
+ return False
+ return self.process.is_running()
diff --git a/minecraft_run.py b/minecraft_run.py
index 65db2b98d..50eada1fc 100644
--- a/minecraft_run.py
+++ b/minecraft_run.py
@@ -13,6 +13,7 @@ from metagpt.minecraft_team import MinecraftPlayer
async def learn(task="Start", investment: float = 50.0, n_round: int = 3):
mc_player = MinecraftPlayer()
+ mc_player.set_port(2253) # Modify this to your LAN port
mc_player.hire(
[
CurriculumDesigner(),